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