【问题标题】:How can I merge and then distinct collections with the Stream API? [duplicate]如何使用 Stream API 合并然后区分集合? [复制]
【发布时间】:2026-01-03 08:55:01
【问题描述】:

让我们在对象前面加上前缀 equals 实现不是我需要过滤的方式,所以distinct 本身不起作用。

class MyObject {
  String foo;
  MyObject( String foo ) {
    this.foo = foo;
  }
  public String getFoo() { return foo; }
}


Collection<MyObject> listA = Arrays.asList("a", "b", "c").stream().map(MyObject::new)
        .collect(Collectors.toList());

Collection<MyObject> listB = Arrays.asList("b", "d").stream().map(MyObject::new)
        .collect(Collectors.toList());


// magic

如何合并和去重列表,以便结果列表应该是包含“a”、“b”、“c”、“d”的 MyObjects?

注意:这是对我们实际需要去重的方法的简化,这些方法实际上是由 hibernate 加载的实体的复杂 DTO,但此示例应充分展示目标。

【问题讨论】:

  • 在映射值之前获取不同的集合?
  • @biziclop 认为这些是从休眠加载的实体列表。我把它省略了,因为我认为它给问题的声明增加了不必要的复杂性。我实际上没有要映射到一组复杂对象然后尝试区分的字符串列表。我只有复杂的对象。
  • 您是否考虑过使用 Guava 及其等价物?
  • @fge 我没有,我们确实有番石榴……尽管在某种程度上,我们正试图摆脱番石榴。我们知道如何用循环来做到这一点......
  • @TagirValeev 因为 80% 的问题是重复数据删除的,所以我不会做太多的斗争,但我的问题和被骗者之间的区别包括“合并多个列表”,我们是处理多个属性(尽管我不确定这种区别是否相关)。

标签: java java-8 java-stream


【解决方案1】:

JDK 开发人员讨论了此类功能(请参阅JDK-8072723),并且可能包含在 Java-9 中(尽管不能保证)。我开发的StreamEx库已经有这个功能了,大家可以使用:

List<MyObject> distinct = StreamEx.of(listA).append(listB)
                                  .distinct(MyObject::getFoo).toList();

StreamEx 类是一个增强的Stream,它与 JDK Stream 完全兼容,但有许多额外的操作,包括 distinct(Function),它允许您为不同的操作指定密钥提取器。在内部,它与@fge 提出的解决方案非常相似。

您还可以考虑编写自定义收集器,它将获取不同的对象并将它们存储到列表中:

public static <T> Collector<T, ?, List<T>> distinctBy(Function<? super T, ?> mapper) {
    return Collector.<T, Map<Object, T>, List<T>> of(LinkedHashMap::new,
        (map, t) -> map.putIfAbsent(mapper.apply(t), t), (m1, m2) -> {
            for(Entry<Object, T> e : m2.entrySet()) {
                m1.putIfAbsent(e.getKey(), e.getValue());
            }
            return m1;
        }, map -> new ArrayList<>(map.values()));
}

这个收集器中间将结果收集到Map&lt;Key, Element&gt;,其中Key是提取的Key,Element是对应的流元素。为了确保在所有重复元素中保留第一个出现的元素,使用LinkedHashMap。最后你只需要获取这张地图的values() 并将它们转储到列表中。所以现在你可以写:

List<MyObject> distinct = Stream.concat(listA.stream(), listB.stream())
                                .collect(distinctBy(MyObject::getFoo));

如果您不关心生成的集合是否为列表,您甚至可以删除new ArrayList&lt;&gt;() 步骤(只需使用Map::values 作为完成器)。如果您不关心订单,也可以进行更多简化:

public static <T> Collector<T, ?, Collection<T>> distinctBy(Function<? super T, ?> mapper) {
    return Collector.<T, Map<Object, T>, Collection<T>> of(HashMap::new,
            (map, t) -> map.put(mapper.apply(t), t), 
            (m1, m2) -> { m1.putAll(m2); return m1; }, 
            Map::values);
}

这样的收集器(保留顺序并返回List)在StreamEx 库中也是available

【讨论】:

  • 问题是,这么大的枪是否真的有必要。如果你知道,你不会使用并行处理,一个简单的.filter(new TreeSet&lt;&gt;(Comparator.comparing(MyObject::getFoo))::add) 就可以了……
  • @Holger,问题是枪是否大。如果您已经在使用我的库,.distinct(MyObject::getFoo) 看起来比您提出的解决方案更清晰、更容易理解。如果不是并且不想添加新的依赖项,那就是另一回事了。虽然在这种情况下我仍然更喜欢自定义收集器(如果在不同的操作之后需要收集到列表),因为它可能更快并且不会违反filter 合同。
【解决方案2】:

如果.equals() 不适合您,那么您可能想尝试使用Guava's Equivalence

假设你的类型是T,你需要实现一个Equivalence&lt;T&gt;;一旦你有了这个,你需要创建一个:

Set<Equivalence.Wrapper<T>>

您将在其中收集您的价值观。然后,假设您的Equivalence&lt;T&gt; 实现是一个名为EQ 的静态变量,添加到这个集合很简单:

coll1.stream().map(EQ::wrap).forEach(set::add);
coll2.stream().map(EQ::wrap).forEach(set::add);

然后要从该集合中获取List&lt;T&gt;,您可以:

final Set<T> unwrapped = set.stream().map(Equivalence.Wrapper::get)
    .collect(Collectors.toSet());

当然,既然在你的 cmets 中你说你可以用一个循环来做到这一点,那么……为什么不继续使用那个循环呢?

如果它有效,请不要修复它......

【讨论】:

  • 那么,Equivalence 很合适;嗯,很抱歉它将你绑定到番石榴,但这个类真的是一个宝石;)
  • 请注意,Equivalence&lt;T&gt; 的一个廉价、简单的替代品是某个类 D,它接受 T 作为参数,并且在正确实现 equals/hashcode 合约时仍然可以一直返回它.
  • Stream.of( coll1, coll2 ).map(EQ::wrap).distinct().map(equivalence::get).collect(Collectors.asList()); 看起来可能比中间集更好。
  • 除非你不能new Stream() 因为Stream 是一个接口...
  • 嗯,是的,这也有效。无论如何,正如您所看到的,您有很多选择,只需选择最适合您的一个:)
【解决方案3】:
Collection<MyObject> result = Stream.concat(listA.stream(), listB.stream())
                              .filter(distinct(MyObject::getFoo))
                              .collect(Collectors.toList());

public static <T> Predicate<T> distinct(Function<? super T, Object> keyExtractor) {
        Map<Object, String> seen = new ConcurrentHashMap<>();
        return t -> seen.put(keyExtractor.apply(t), "") == null;
    }

我曾在博客中发现过这个distinct 函数(不记得链接atm)。

【讨论】:

  • 它可能是从Stuart Marks答案中复制的。此解决方案不保留并行顺序,并且顺序具有不必要的 CHM 开销。不过这很简单。