Java 8按属性区分


454

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

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

persons.stream().distinct();

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

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

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

Answers:


554

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

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


27
我想为了更好的兼容性,该参数应该Function<? super T, ?>不是Function<? super T, Object>。还应注意,对于有序并行流,此解决方案不能保证将提取哪个对象(与normal不同distinct())。同样对于顺序流,使用CHM会有额外的开销(@nosid解决方案中不存在)。最后,该解决方案违反了filterJavaDoc中所述谓词必须为无状态的方法的约定。然而,赞成。
塔吉尔·瓦列夫2015年

3
@java_newbie由返回的Predicate实例distinctByKey不知道是否在并行流中使用它。它在并行使用的情况下会使用CHM,尽管如上文Tagir Valeev所述,在顺序情况下这会增加开销。
斯图尔特(Stuart Marks)

5
@holandaGo如果您保存并重复使用所返回的Predicate实例,它将失败distinctByKey。但是,如果您distinctByKey每次都调用它就可以工作,因此每次都可以创建一个新的Predicate实例。
斯图尔特(Stuart Marks)

3
@Chinmay不,不应该。如果使用.filter(distinctByKey(...))。它将执行一次该方法并返回谓词。因此,如果您在流中正确使用该地图,则基本上该地图已经在重复使用。如果将地图设为静态,则该地图将共享所有用途。因此,如果您有两个使用this的流distinctByKey(),那么两个都将使用同一张地图,这不是您想要的。
g00glen00b'7

3
这太聪明了,而且完全不明显。一般来说,这是一个有状态的λ和底层CallSite将被链接到的get$Lambda方法-它返回的一个新的实例Predicate所有的时间,但这些实例将共享相同的map,并function尽可能我明白了。非常好!
尤金(Eugene)

151

一种替代方法是使用姓名作为关键字将人员放置在地图中:

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

请注意,如果姓名重复,则被保住的人将是第一个进入的人。


23
@skiwi:您认为有distinct()没有这种开销的实现方法吗?任何实现如何在不实际记住它已经看到的所有不同值的情况下知道它是否曾经看到过一个对象?所以的开销toMap,并distinct极有可能是相同的。
Holger 2014年

1
@Holger我可能在这里是错的,因为我没想到开销distinct()本身会带来麻烦。
2014年

2
显然,它弄乱了列表的原始顺序
Philipp

10
@Philipp:可以通过更改为persons.collect(toMap(Person::getName, p -> p, (p, q) -> p, LinkedHashMap::new)).values();
Holger

1
@DanielEarwicker这个问题是关于“财产区别”的。它将需要使用相同的属性对流进行排序,以便能够利用它。首先,OP从未声明流已完全排序。其次,流无法检测它们是否按特定属性排序。第三,没有真正的“按属性区分”流操作来执行您建议的操作。第四,在实践中,只有两种方法来获得这种分类的流。排序后的源(TreeSet)无论如何还是sorted在流上都已经是不同的,该源还缓冲所有元素。
Holger

101

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

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


5
@StuartCaie并不是真的……没有备忘录,关键不是性能,而是对现有API的适应。
Marko Topolnik 2014年

6
com.google.common.base.Equivalence.wrap(S)和com.google.common.base.Equivalence.Wrapper.get()也可以提供帮助。
bjmi '17

您可以使包装类具有通用性,并通过键提取函数对其进行参数化。

equals方法可以简化为return other instanceof Wrapper && ((Wrapper) other).person.getName().equals(person.getName());
Holger

55

另一种解决方案,使用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()));

2
如果您不使用任何第三方库,这是最佳答案!
Manoj Shrestha

5
使用精巧的想法,即Set.add如果此集合尚未包含指定的元素,则返回true。+1
Luvie '19

我认为此方法不适用于并行流处理,因为它不是线程安全的。
LoBo

@LoBo可能不是。这只是一个想法,适用于简单的情况。用户可以扩展它以确保线程安全/并行性。
Santhosh

有趣的方法,但看上去有点像反模式,可以修改外部集合(集合),同时在另一个集合(人)上过滤流...
贾斯汀·罗

31

将TreeSet与自定义比较器一起使用是一种更简单的方法。

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

4
我认为您的回答有助于排序而不是唯一性。但是,它帮助我对如何进行设置提出了自己的想法。查看此处:stackoverflow.com/questions/1019854/...
janagn

请记住,您将在这里为元素排序付出代价,我们不需要为了查找重复项甚至删除重复项就进行排序。
pisaruk '16

12
Comparator.comparing(人::的getName)
让·弗朗索瓦·Savard

24

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

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

要么

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

Rx很棒,但是答案很差。Observable基于推送,而Stream基于拉。stackoverflow.com/questions/30216979/...
sdgfsdh

4
这个问题要求一个不一定使用流的java8解决方案。我的回答表明,java8流api的性能不如RX api
frhack

1
使用反应堆,它将是Flux.fromIterable(persons).distinct(p -> p.getName())
Ritesh

该问题字面意思是“使用StreamAPI”,而不是“不一定使用流”。也就是说,这对于将流过滤为不同值的XY问题是一个很好的解决方案。
M. Justin

12

您可以使用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)));

11

您可以使用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的提交者。


在Eclipse Collections 9.0中添加了distinctBy方法,可以进一步简化该解决方案。 medium.com/@donraab/…–
唐纳德·拉布


9

您可以使用StreamEx库:

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

不幸的是,原本很棒的StreamEx库的方法设计欠佳-它比较对象相等性而不是使用相等性。String多亏了字符串实习,这可能对s有用,但也可能不行。
扭矩

7

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

2
这个没有考虑到流可能是并行的。
brunnsbe

感谢您的评论,我已经更新了答案。如果您不需要并行流,则不使用并发映射可以提供更好的性能。
WojciechGórski'16

如果创建了并行集合,则您的代码可能会适用于并行集合Collections.synchronizedSet(new HashSet<>())。但这可能会比使用慢ConcurrentHashMap
Lii

7

Saeed Zarinfam使用了类似的方法,但是使用了更多的Java 8样式:)

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

1
我想更换地图符合flatMap(plans -> plans.stream().findFirst().stream())它避免了可选使用get
安德鲁Sneck

也许这也行:flatMap(计划- > plans.stream()限制(1))
率Rrr

6

我做了一个通用版本:

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



5

我的方法是将具有相同属性的所有对象组合在一起,然后将组切成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());

3

可以使用以下命令找到不同的对象列表:

 List distinctPersons = persons.stream()
                    .collect(Collectors.collectingAndThen(
                            Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
                            ArrayList::new));

2

实现此功能最简单的方法是跳到sort功能,因为它已经提供了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不是线程安全的,但是如果您需要,可以将此逻辑移到a中,Collector并在使用your时让流负责线程安全Collector。这取决于您要如何处理不同元素的流,而您没有在问题中告诉我们。


1

以@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;
    }
}

1

也许对某人有用。我还有一点其他要求。具有A来自第三方的对象列表,将所有具有相同A.b字段的对象都删除A.id(列表中A具有相同对象的多个对象A.id)。Tagir Valeev对流分区的回答启发了我使用返回的自定义。简单将完成其余的工作。CollectorMap<A.id, List<A>>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)); }

1

我遇到了一种情况,当时我想根据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());

0

就我而言,我需要控制先前的内容。然后,我创建了一个有状态的谓词,在其中控制上一个元素与当前元素是否不同,在这种情况下,我将其保留。

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

}

0

我在此清单中的解决方案:

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

在我的情况下,我想找到不同的值并将其放在列表中。


0

尽管获得最高支持的答案绝对是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倍,错误分数降低了。

更高的吞吐量,更好的性能


1
谢谢,但是这个问题是在Stream API的背景下非常具体的
RichK,

是的,我同意,我已经提到过“虽然最高的答案绝对是Java 8的最佳答案”。可以用n种不同的方法解决问题,在这里我要强调的是,可以简单地解决当前的问题,而不是用Java 8 Streams来解决危险,因为Java 8 Streams可能会降低性能。:)
Abhinav Ganguly

-2

如果您想列出人员列表,这将是简单的方法

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

另外,如果要查找名称的唯一列表或唯一列表而不是Person,则也可以使用以下两种方法。

方法1:使用 distinct

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

方法2:使用 HashSet

Set<E> set = new HashSet<>();
set.addAll(person.stream().map(x->x.getName()).collect(Collectors.toList()));

2
这将产生一个名称列表,而不是Persons。
绿巨人

1
这正是我想要的。我需要一种单行方法来消除重复项,同时将一个集合彼此转换。谢谢。
拉吉

-3

您可以编写的最简单的代码:

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

12
不过,这将获得一个独特的名称列表,而不是按名称列出的人
RichK
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.