Java 8不同属性

在Java 8中,如何通过检查每个对象的属性的独特性来使用Stream API过滤集合?

例如,我有一个Person对象的列表,我想删除具有相同名称的人,

 persons.stream().distinct(); 

将使用Person对象的默认相等性检查,所以我需要类似的东西,

 persons.stream().distinct(p -> p.getName()); 

不幸的是, distinct()方法没有这样的重载。 在不修改Person类中的相等性检查的情况下,可以简单地做到这一点?

我终于想出了一个很好的方法来做到这一点。 考虑distinct的是一个有状态的filter 。 编写函数返回一个谓词,该谓词还保持之前所看到的状态,并返回给定元素是否第一次被查看:

 public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } 

那么你可以写:

 persons.stream().filter(distinctByKey(p -> p.getName()); 

注意:这与我对这个问题的回答基本相同: 在任意键上的Java Lambda Stream Distinct()?

另一种方法是将人员放在地图上,并用名称作为关键字:

 persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values(); 

请注意,如果名称重复,则保留的人员将成为第一名。

您可以将人物对象包装到另一个类中,只比较人物的名字。 之后,您打开包装物件以再次获取人员stream。 stream操作可能如下所示:

 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(); } } 

我们也可以使用RxJava (非常强大的反应扩展库)

 Observable.from(persons).distinct(Person::getName) 

要么

 Observable.from(persons).distinct(p -> p.getName()) 

使用具有自定义比较器的TreeSet有一个更简单的方法。

 persons.stream() .collect(Collectors.toCollection( () -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName())) )); 

您可以在Eclipse集合中使用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集合的提交者。

扩展斯图尔特·马克斯的答案,这可以做一个更短的方式,没有并发的地图(如果你不需要并行stream):

 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()); 

如果可以的话,我推荐使用Vavr 。 有了这个库,你可以做到以下几点:

 io.vavr.collection.List.ofAll(persons) .distinctBy(Person::getName) .toJavaSet() // or any another Java 8 Collection 

我做了一个通用版本:

 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()) 

实现这个最简单的方法是跳转sortingfunction,因为它已经提供了一个可选的Comparator ,可以使用元素的属性创build。 那么你必须过滤重复出来,这可以做一个statefull Predicate使用的事实,对于一个sortingstream所有相等的元素是相邻的:

 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让stream处理线程安全。 这取决于你想要做什么与你没有告诉我们在你的问题的不同元素stream。

你可以使用groupingBy收集器:

 persons.collect(groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId())); 

如果你想有另一个stream,你可以使用这个:

 persons.collect(groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0))); 

Saeed Zarinfam使用了类似的方法,但更多的Java 8风格:)

 persons.collect(groupingBy(p -> p.getName())).values().stream() .map(plans -> plans.stream().findFirst().get()) .collect(toList()); 

您可以使用StreamEx库:

 StreamEx.of(persons) .distinct(Person::getName) .toList() 

基于@ josketres的答案,我创build了一个通用的实用程序方法:

您可以通过创build一个收集器来使这个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; } } 

另一个解决scheme,使用Set 。 可能不是理想的解决scheme,但它的工作原理

 Set<String> set = new HashSet<>(persons.size()); persons.stream() .filter(p -> set.contains(p.getName()) ? false : set.add(p.getName())) .collect(Collectors.toList()); 

或者,如果您可以修改原始列表,则可以使用removeIf方法

 persons.removeIf(p -> set.contains(p.getName()) ? true : !set.add(p.getName())); 

最简单的代码,你可以写:

  persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());