在 Java 8 中,如何使用 Stream
API 通过检查每个对象的属性的独特性来过滤集合?
例如,我有一个 Person
对象列表,我想删除同名的人,
persons.stream().distinct();
将对 Person
对象使用默认相等检查,所以我需要类似的东西,
persons.stream().distinct(p -> p.getName());
不幸的是,distinct()
方法没有这样的重载。如果不修改 Person
类中的相等检查,是否可以简洁地做到这一点?
将 distinct
视为一个有状态过滤器。这是一个返回谓词的函数,该谓词维护先前看到的状态,并返回是否第一次看到给定元素:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
然后你可以写:
persons.stream().filter(distinctByKey(Person::getName))
请注意,如果流是有序的并且并行运行,这将在重复项中保留一个 任意 元素,而不是像 distinct()
那样保留第一个元素。
(这与此问题的 my answer 基本相同:Java Lambda Stream Distinct() on arbitrary key?)
另一种方法是使用名称作为键将人员放置在地图中:
persons.collect(Collectors.toMap(Person::getName, p -> p, (p, q) -> p)).values();
请注意,如果名称重复,则保留的 Person
将是第一个输入的。
distinct()
而没有这种开销?任何实现如何知道它以前是否见过一个对象而不实际记住它见过的所有不同的值?所以 toMap
和 distinct
的开销很可能是相同的。
distinct()
本身产生的开销。
persons.collect(toMap(Person::getName, p -> p, (p, q) -> p, LinkedHashMap::new)).values();
来修复
TreeSet
) 或在还缓冲所有元素的流上的 sorted
。
您可以将人员对象包装到另一个类中,该类仅比较人员的姓名。之后,您打开包装的对象以再次获取人员流。流操作可能如下所示:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
类 Wrapper
可能如下所示:
class Wrapper {
private final Person person;
public Wrapper(Person person) {
this.person = person;
}
public Person unwrap() {
return person;
}
public boolean equals(Object other) {
if (other instanceof Wrapper) {
return ((Wrapper) other).person.getName().equals(person.getName());
} else {
return false;
}
}
public int hashCode() {
return person.getName().hashCode();
}
}
persons.stream().map(Equivalence.equals().onResultOf(Person::getName)::wrap).distinct().map(Equivalence.Wrapper::get)....
另一种解决方案,使用 Set
。可能不是理想的解决方案,但它有效
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
或者如果您可以修改原始列表,您可以使用 removeIf 方法
persons.removeIf(p -> !set.add(p.getName()));
使用带有自定义比较器的 TreeSet 有一种更简单的方法。
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
我们还可以使用 RxJava(非常强大的 reactive extension 库)
Observable.from(persons).distinct(Person::getName)
或者
Observable.from(persons).distinct(p -> p.getName())
Flux.fromIterable(persons).distinct(p -> p.getName())
Stream
API”,而不是“不一定使用流”。也就是说,对于将流过滤为不同值的 XY 问题,这是一个很好的解决方案。
您可以使用 groupingBy
收集器:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
如果你想有另一个流,你可以使用这个:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
您可以使用 Eclipse Collections 中的 distinct(HashingStrategy)
方法。
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
如果您可以重构 persons
以实现一个 Eclipse Collections 接口,您可以直接调用列表中的方法。
MutableList<Person> persons = ...;
MutableList<Person> distinct =
persons.distinct(HashingStrategies.fromFunction(Person::getName));
HashingStrategy 只是一个策略接口,可让您定义 equals 和 hashcode 的自定义实现。
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
注意:我是 Eclipse Collections 的提交者。
Saeed Zarinfam 使用的类似方法,但更多的是 Java 8 风格:)
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
flatMap(plans -> plans.stream().findFirst().stream())
替换地图线它避免使用 get on Optional
您可以使用 StreamEx 库:
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
String
,但也可能不起作用。
如果可以的话,我建议使用 Vavr。使用此库,您可以执行以下操作:
io.vavr.collection.List.ofAll(persons)
.distinctBy(Person::getName)
.toJavaSet() // or any another Java 8 Collection
扩展 Stuart Marks 的答案,这可以在没有并发映射的情况下以更短的方式完成(如果您不需要并行流):
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return t -> seen.add(keyExtractor.apply(t));
}
然后调用:
persons.stream().filter(distinctByKey(p -> p.getName());
Collections.synchronizedSet(new HashSet<>())
,您的代码可能适用于并行集合。但它可能会比使用 ConcurrentHashMap
慢。
可以使用以下方法找到不同的对象列表:
List distinctPersons = persons.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
ArrayList::new));
我做了一个通用版本:
private <T, R> Collector<T, ?, Stream<T>> distinctByKey(Function<T, R> keyExtractor) {
return Collectors.collectingAndThen(
toMap(
keyExtractor,
t -> t,
(t1, t2) -> t1
),
(Map<R, T> map) -> map.values().stream()
);
}
一个例子:
Stream.of(new Person("Jean"),
new Person("Jean"),
new Person("Paul")
)
.filter(...)
.collect(distinctByKey(Person::getName)) // return a stream of Person with 2 elements, jean and Paul
.map(...)
.collect(toList())
另一个支持此功能的库是 jOOλ,它的 Seq.distinct(Function<T,U>)
方法:
Seq.seq(persons).distinct(Person::getName).toList();
Under the hood,但它实际上与 accepted answer 做同样的事情。
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);
我的方法是将具有相同属性的所有对象组合在一起,然后将组缩短为 1 的大小,最后将它们收集为 List
。
List<YourPersonClass> listWithDistinctPersons = persons.stream()
//operators to remove duplicates based on person name
.collect(Collectors.groupingBy(p -> p.getName()))
.values()
.stream()
//cut short the groups to size of 1
.flatMap(group -> group.stream().limit(1))
//collect distinct users as list
.collect(Collectors.toList());
虽然最高支持的答案绝对是 Java 8 的最佳答案,但同时在性能方面绝对是最差的。如果您真的想要一个性能不佳的低性能应用程序,那么请继续使用它。只需“For-Each”和“Set”即可实现提取唯一一组人名的简单要求。如果列表大小超过 10,情况会变得更糟。
假设您有 20 个对象的集合,如下所示:
public static final List<SimpleEvent> testList = Arrays.asList(
new SimpleEvent("Tom"), new SimpleEvent("Dick"),new SimpleEvent("Harry"),new SimpleEvent("Tom"),
new SimpleEvent("Dick"),new SimpleEvent("Huckle"),new SimpleEvent("Berry"),new SimpleEvent("Tom"),
new SimpleEvent("Dick"),new SimpleEvent("Moses"),new SimpleEvent("Chiku"),new SimpleEvent("Cherry"),
new SimpleEvent("Roses"),new SimpleEvent("Moses"),new SimpleEvent("Chiku"),new SimpleEvent("gotya"),
new SimpleEvent("Gotye"),new SimpleEvent("Nibble"),new SimpleEvent("Berry"),new SimpleEvent("Jibble"));
您反对 SimpleEvent
的位置如下所示:
public class SimpleEvent {
private String name;
private String type;
public SimpleEvent(String name) {
this.name = name;
this.type = "type_"+name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
为了测试,你有这样的 JMH 代码,(请注意,我使用了在接受的答案中提到的相同的 distinctByKey 谓词):
@Benchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public void aStreamBasedUniqueSet(Blackhole blackhole) throws Exception{
Set<String> uniqueNames = testList
.stream()
.filter(distinctByKey(SimpleEvent::getName))
.map(SimpleEvent::getName)
.collect(Collectors.toSet());
blackhole.consume(uniqueNames);
}
@Benchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public void aForEachBasedUniqueSet(Blackhole blackhole) throws Exception{
Set<String> uniqueNames = new HashSet<>();
for (SimpleEvent event : testList) {
uniqueNames.add(event.getName());
}
blackhole.consume(uniqueNames);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.mode(Mode.Throughput)
.warmupBatchSize(3)
.warmupIterations(3)
.measurementIterations(3)
.build();
new Runner(opt).run();
}
然后你会得到这样的基准测试结果:
Benchmark Mode Samples Score Score error Units
c.s.MyBenchmark.aForEachBasedUniqueSet thrpt 3 2635199.952 1663320.718 ops/s
c.s.MyBenchmark.aStreamBasedUniqueSet thrpt 3 729134.695 895825.697 ops/s
如您所见,与 Java 8 Stream 相比,一个简单的 For-Each 的吞吐量提高了 3 倍,并且错误分数更低。
吞吐量越高,性能越好
这就像一个魅力:
按唯一键对数据进行分组以形成地图。从地图的每个值返回第一个对象(可能有多个同名的人)。
persons.stream()
.collect(groupingBy(Person::getName))
.values()
.stream()
.flatMap(values -> values.stream().limit(1))
.collect(toList());
实现这一点的最简单方法是使用排序功能,因为它已经提供了一个可选的 Comparator
,可以使用元素的属性来创建它。然后,您必须过滤掉重复项,这可以使用 statefull Predicate
来完成,它使用的事实是,对于已排序的流,所有相等的元素都是相邻的:
Comparator<Person> c=Comparator.comparing(Person::getName);
stream.sorted(c).filter(new Predicate<Person>() {
Person previous;
public boolean test(Person p) {
if(previous!=null && c.compare(previous, p)==0)
return false;
previous=p;
return true;
}
})./* more stream operations here */;
当然,有状态的 Predicate
不是线程安全的,但是如果您需要,您可以将此逻辑移动到 Collector
中,并让流在使用 Collector
时负责线程安全。这取决于您想对您在问题中没有告诉我们的不同元素流做什么。
我想改进 Stuart Marks answer。如果键为空怎么办,它将通过NullPointerException
。在这里,我通过将另一项检查添加为 keyExtractor.apply(t)!=null
来忽略空键。
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> keyExtractor.apply(t)!=null && seen.add(keyExtractor.apply(t));
}
Here is the example
public class PayRoll {
private int payRollId;
private int id;
private String name;
private String dept;
private int salary;
public PayRoll(int payRollId, int id, String name, String dept, int salary) {
super();
this.payRollId = payRollId;
this.id = id;
this.name = name;
this.dept = dept;
this.salary = salary;
}
}
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class Prac {
public static void main(String[] args) {
int salary=70000;
PayRoll payRoll=new PayRoll(1311, 1, "A", "HR", salary);
PayRoll payRoll2=new PayRoll(1411, 2 , "B", "Technical", salary);
PayRoll payRoll3=new PayRoll(1511, 1, "C", "HR", salary);
PayRoll payRoll4=new PayRoll(1611, 1, "D", "Technical", salary);
PayRoll payRoll5=new PayRoll(711, 3,"E", "Technical", salary);
PayRoll payRoll6=new PayRoll(1811, 3, "F", "Technical", salary);
List<PayRoll>list=new ArrayList<PayRoll>();
list.add(payRoll);
list.add(payRoll2);
list.add(payRoll3);
list.add(payRoll4);
list.add(payRoll5);
list.add(payRoll6);
Map<Object, Optional<PayRoll>> k = list.stream().collect(Collectors.groupingBy(p->p.getId()+"|"+p.getDept(),Collectors.maxBy(Comparator.comparingInt(PayRoll::getPayRollId))));
k.entrySet().forEach(p->
{
if(p.getValue().isPresent())
{
System.out.println(p.getValue().get());
}
});
}
}
Output:
PayRoll [payRollId=1611, id=1, name=D, dept=Technical, salary=70000]
PayRoll [payRollId=1811, id=3, name=F, dept=Technical, salary=70000]
PayRoll [payRollId=1411, id=2, name=B, dept=Technical, salary=70000]
PayRoll [payRollId=1511, id=1, name=C, dept=HR, salary=70000]
派对迟到了,但我有时会用这个单线作为等价物:
((Function<Value, Key>) Value::getKey).andThen(new HashSet<>()::add)::apply
表达式是 Predicate<Value>
,但由于映射是内联的,因此它用作过滤器。这当然可读性较差,但有时避免该方法可能会有所帮助。
有很多方法,这个也有帮助
List<Employee> employees = new ArrayList<>();
employees.add(new Employee(11, "Ravi"));
employees.add(new Employee(12, "Stalin"));
employees.add(new Employee(23, "Anbu"));
employees.add(new Employee(24, "Yuvaraj"));
employees.add(new Employee(35, "Sena"));
employees.add(new Employee(36, "Antony"));
employees.add(new Employee(47, "Sena"));
employees.add(new Employee(48, "Ravi"));
List<Employee> empList = new ArrayList<>(employees.stream().collect(
Collectors.toMap(Employee::getName, obj -> obj,
(existingValue, newValue) -> existingValue))
.values());
empList.forEach(System.out::println);
// Collectors.toMap(
// Employee::getName, - key (the value by which you want to eliminate duplicate)
// obj -> obj, - value (entire employee object)
// (existingValue, newValue) -> existingValue) - to avoid illegalstateexception: duplicate key
输出 - toString() 重载
Employee{id=35, name='Sena'}
Employee{id=12, name='Stalin'}
Employee{id=11, name='Ravi'}
Employee{id=24, name='Yuvaraj'}
Employee{id=36, name='Antony'}
Employee{id=23, name='Anbu'}
基于@josketres 的回答,我创建了一个通用实用程序方法:
您可以通过创建 Collector 使其对 Java 8 更加友好。
public static <T> Set<T> removeDuplicates(Collection<T> input, Comparator<T> comparer) {
return input.stream()
.collect(toCollection(() -> new TreeSet<>(comparer)));
}
@Test
public void removeDuplicatesWithDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(7), new C(42), new C(42));
Collection<C> result = removeDuplicates(input, (c1, c2) -> Integer.compare(c1.value, c2.value));
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 7));
assertTrue(result.stream().anyMatch(c -> c.value == 42));
}
@Test
public void removeDuplicatesWithoutDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(1), new C(2), new C(3));
Collection<C> result = removeDuplicates(input, (t1, t2) -> Integer.compare(t1.value, t2.value));
assertEquals(3, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 1));
assertTrue(result.stream().anyMatch(c -> c.value == 2));
assertTrue(result.stream().anyMatch(c -> c.value == 3));
}
private class C {
public final int value;
private C(int value) {
this.value = value;
}
}
也许对某人有用。我还有一点要求。拥有来自第 3 方的对象列表 A
会删除所有具有相同 A.b
字段的所有对象(列表中具有相同 A.id
的多个 A
对象)。 Tagir Valeev 的 Stream partition 回答启发我使用返回 Map<A.id, List<A>>
的自定义 Collector
。简单的 flatMap
将完成剩下的工作。
public static <T, K, K2> Collector<T, ?, Map<K, List<T>>> groupingDistinctBy(Function<T, K> keyFunction, Function<T, K2> distinctFunction) {
return groupingBy(keyFunction, Collector.of((Supplier<Map<K2, T>>) HashMap::new,
(map, error) -> map.putIfAbsent(distinctFunction.apply(error), error),
(left, right) -> {
left.putAll(right);
return left;
}, map -> new ArrayList<>(map.values()),
Collector.Characteristics.UNORDERED)); }
我有一种情况,我想根据 2 个键从列表中获取不同的元素。如果您想基于两个键或复合键来区分,试试这个
class Person{
int rollno;
String name;
}
List<Person> personList;
Function<Person, List<Object>> compositeKey = personList->
Arrays.<Object>asList(personList.getName(), personList.getRollno());
Map<Object, List<Person>> map = personList.stream().collect(Collectors.groupingBy(compositeKey, Collectors.toList()));
List<Object> duplicateEntrys = map.entrySet().stream()`enter code here`
.filter(settingMap ->
settingMap.getValue().size() > 1)
.collect(Collectors.toList());
处理 null
的 the top answer 变体:
public static <T, K> Predicate<T> distinctBy(final Function<? super T, K> getKey) {
val seen = ConcurrentHashMap.<Optional<K>>newKeySet();
return obj -> seen.add(Optional.ofNullable(getKey.apply(obj)));
}
在我的测试中:
assertEquals(
asList("a", "bb"),
Stream.of("a", "b", "bb", "aa").filter(distinctBy(String::length)).collect(toList()));
assertEquals(
asList(5, null, 2, 3),
Stream.of(5, null, 2, null, 3, 3, 2).filter(distinctBy(x -> x)).collect(toList()));
val maps = asList(
hashMapWith(0, 2),
hashMapWith(1, 2),
hashMapWith(2, null),
hashMapWith(3, 1),
hashMapWith(4, null),
hashMapWith(5, 2));
assertEquals(
asList(0, 2, 3),
maps.stream()
.filter(distinctBy(m -> m.get("val")))
.map(m -> m.get("i"))
.collect(toList()));
就我而言,我需要控制前一个元素是什么。然后我创建了一个有状态的 Predicate 来控制前一个元素是否与当前元素不同,在这种情况下我保留它。
public List<Log> fetchLogById(Long id) {
return this.findLogById(id).stream()
.filter(new LogPredicate())
.collect(Collectors.toList());
}
public class LogPredicate implements Predicate<Log> {
private Log previous;
public boolean test(Log atual) {
boolean isDifferent = previouws == null || verifyIfDifferentLog(current, previous);
if (isDifferent) {
previous = current;
}
return isDifferent;
}
private boolean verifyIfDifferentLog(Log current, Log previous) {
return !current.getId().equals(previous.getId());
}
}
我在此清单中的解决方案:
List<HolderEntry> result ....
List<HolderEntry> dto3s = new ArrayList<>(result.stream().collect(toMap(
HolderEntry::getId,
holder -> holder, //or Function.identity() if you want
(holder1, holder2) -> holder1
)).values());
在我的情况下,我想找到不同的值并将它们放入列表中。
Function<? super T, ?>
,而不是Function<? super T, Object>
。还应注意,对于有序并行流,此解决方案不保证将提取哪个对象(与正常distinct()
不同)。同样对于顺序流,使用 CHM 会产生额外的开销(@nosid 解决方案中不存在)。最后,该解决方案违反了filter
方法的约定,即谓词必须是无状态的,如 JavaDoc 中所述。尽管如此,还是投了赞成票。distinctByKey
返回的 Predicate 实例不知道它是否在并行流中使用。如果它被并行使用,它会使用 CHM,尽管这会在顺序情况下增加开销,如上面 Tagir Valeev 所述。distinctByKey
返回的 Predicate 实例,它将失败。但如果您每次都调用distinctByKey
,它就会起作用,这样它每次都会创建一个新的 Predicate 实例。.filter(distinctByKey(...))
。它将执行一次方法并返回谓词。因此,如果您在流中正确使用它,基本上地图已经被重新使用。如果您将地图设为静态,则地图将为所有用途共享。因此,如果您有两个使用此distinctByKey()
的流,则两者都将使用相同的映射,这不是您想要的。CallSite
将链接到get$Lambda
方法 - 这将始终返回Predicate
的新实例,但这些实例将共享据我了解,map
和function
相同。非常好!