Java 8 按属性区分

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23699371/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-14 00:46:55  来源:igfitidea点击:

Java 8 Distinct by property

javacollectionsjava-8java-stream

提问by RichK

In Java 8 how can I filter a collection using the StreamAPI by checking the distinctness of a property of each object?

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

For example I have a list of Personobject and I want to remove people with the same name,

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

persons.stream().distinct();

Will use the default equality check for a Personobject, so I need something like,

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

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

Unfortunately the distinct()method has no such overload. Without modifying the equality check inside the Personclass is it possible to do this succinctly?

不幸的是,该distinct()方法没有这样的重载。不修改Person类内部的相等性检查是否可以简洁地做到这一点?

采纳答案by Stuart Marks

Consider distinctto be a stateful filter. Here is a function that returns a predicate that maintains state about what it's seen previously, and that returns whether the given element was seen for the first time:

考虑distinct是一个有状态的过滤器。这是一个函数,它返回一个谓词,该谓词维护关于它之前看到的内容的状态,并返回给定元素是否第一次被看到:

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}

Then you can write:

然后你可以写:

persons.stream().filter(distinctByKey(Person::getName))

Note that if the stream is ordered and is run in parallel, this will preserve an arbitraryelement from among the duplicates, instead of the first one, as distinct()does.

请注意,如果流是有序的并并行运行,这将保留重复项中的任意元素,而不是第一个,就像distinct()这样。

(This is essentially the same as my answerto this question: Java Lambda Stream Distinct() on arbitrary key?)

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

回答by nosid

You can wrap the person objects into another class, that only compares the names of the persons. Afterwards, you unwrap the wrapped objects to get a person stream again. The stream operations might look as follows:

您可以将人对象包装到另一个类中,该类只比较人的姓名。之后,您解开包装的对象以再次获得人员流。流操作可能如下所示:

persons.stream()
    .map(Wrapper::new)
    .distinct()
    .map(Wrapper::unwrap)
    ...;

The class Wrappermight look as follows:

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

回答by wha'eve'

An alternative would be to place the persons in a map using the name as a key:

另一种方法是使用名称作为键将人员放置在地图中:

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

Note that the Person that is kept, in case of a duplicate name, will be the first encontered.

请注意,如果名称重复,则保留的 Person 将是第一个遇到的人。

回答by Holger

The easiest way to implement this is to jump on the sort feature as it already provides an optional Comparatorwhich can be created using an element's property. Then you have to filter duplicates out which can be done using a statefull Predicatewhich uses the fact that for a sorted stream all equal elements are adjacent:

实现这一点的最简单方法是跳转到排序功能,因为它已经提供了一个Comparator可以使用元素属性创建的可选项。然后,您必须过滤掉重复项,这可以使用 statefull 来完成,该 statefullPredicate使用以下事实:对于已排序的流,所有相等的元素都是相邻的:

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 */;

Of course, a statefull Predicateis not thread-safe, however if that's your need you can move this logic into a Collectorand let the stream take care of the thread-safety when using your Collector. This depends on what you want to do with the stream of distinct elements which you didn't tell us in your question.

当然,有状态Predicate不是线程安全的,但是如果这是您的需要,您可以将此逻辑移入 aCollector并让流在使用Collector. 这取决于您想对您在问题中没有告诉我们的不同元素流做什么。

回答by josketres

There's a simpler approach using a TreeSet with a custom comparator.

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

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

回答by Garrett Smith

Building on @josketres's answer, I created a generic utility method:

基于@josketres 的回答,我创建了一个通用的实用方法:

You could make this more Java 8-friendly by creating a Collector.

您可以通过创建一个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;
    }
}

回答by frhack

We can also use RxJava(very powerful reactive extensionlibrary)

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

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

or

或者

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

回答by Craig P. Motlin

You can use the distinct(HashingStrategy)method in Eclipse Collections.

您可以distinct(HashingStrategy)Eclipse Collections 中使用该方法。

List<Person> persons = ...;
MutableList<Person> distinct =
    ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));

If you can refactor personsto implement an Eclipse Collections interface, you can call the method directly on the list.

如果可以重构persons实现一个Eclipse Collections接口,就可以直接调用列表上的方法。

MutableList<Person> persons = ...;
MutableList<Person> distinct =
    persons.distinct(HashingStrategies.fromFunction(Person::getName));

HashingStrategyis simply a strategy interface that allows you to define custom implementations of equals and hashcode.

HashingStrategy只是一个策略接口,允许您定义 equals 和 hashcode 的自定义实现。

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

Note: I am a committer for Eclipse Collections.

注意:我是 Eclipse Collections 的提交者。

回答by Wojciech Górski

Extending Stuart Marks's answer, this can be done in a shorter way and without a concurrent map (if you don't need parallel streams):

扩展 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));
}

Then call:

然后调用:

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

回答by Mateusz Rasiński

I recommend using Vavr, if you can. With this library you can do the following:

如果可以,我建议使用Vavr。使用此库,您可以执行以下操作:

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