如何使用 Java 8 个流从基于两个属性的列表中删除重复元素?

How to remove Duplicated elements from a List based on Two properties using Java 8 streams?

我正在尝试弄清楚如何在 Java 8 中编写一个流,该流根据 属性 和条件删除重复记录,例如:

+----+------------------+--------------+
| ID |       Name       |    Value     |
+----+------------------+--------------+
|  1 | Real Name        | Real Value   |
|  2 | Duplicate Name   | Real Value   |
|  3 | Duplicate Name   | NULL         |
|  4 | Duplicate Name   | NULL         |
|  5 | Real Name 2      | Real Value 2 |
|  6 | Duplicate Name 2 | Real Value   |
|  7 | Duplicate Name 2 | NULL         |
+----+------------------+--------------+

示例对象:

Class ExampleObject {
  int id;
  String name;
  String value;
}

它应该删除所有具有相同名称且 value == null.

的重复记录

所以在这种情况下,它应该删除 ID 为 347 的记录。

我可以通过做一些流和一些 for 循环来解决这个问题,但我不太喜欢这个实现,我知道只有一个流应该是可能的。

我该如何解决这个问题?

做我们想做的事情的一种可能性是

stream-based 实现可能如下所示:

final TreeSet<ExampleObject> deduped = objects.stream()
    .collect(Collectors.toCollection(() -> new TreeSet<>(
        Comparator.comparing(ExampleObject::getName)
            .thenComparing(ExampleObject::getValue))));

Ideone demo

如果我们不喜欢 stream-based 方法,我们也可以使用“传统的”命令式方法解决此问题:

final TreeSet<ExampleObject> deduped = new TreeSet<>(
        Comparator.comparing(ExampleObject::getName)
            .thenComparing(ExampleObject::getValue));
deduped.addAll(objects);

Ideone demo


关于性能的一句话:

重复数据删除不是“免费”的。在提供的解决方案中,我们用执行时间来支付费用。 TreeSet是一个有序的数据结构,因此每次插入的时间复杂度为O(log(n))。那么构造一个大小为n的集合,时间复杂度为O(n log(n)).

remove all the duplicate records with the same name and having value == null. So in this case, it should remove records with ID 3, 4, and 7

如果我对您的目标的理解正确,您只想删除 valuenull 的元素,前提是至少还有一个元素具有相同的 name

同时,不应丢弃所有名称为 non-null value 的元素(因此元素为 id 2 6 在你的例子中应该保留; 如果这个假设不正确,下面实现的这个行为可以很容易地改变).

此外,如果列表中只有一个元素具有特定 name 和值 null。这个假设是基于纯粹的逻辑:这样的元素不能被认为是重复的,因为它的 name 属性是唯一的。

收藏家描述

为了实现这一点,我编写了一个可以与Collectors.groupingBy模糊比较的自定义收集器。

它的目的是创建一个 map,其中 keys 将由给定的 keyExtractor functionvalues将用元素的Deque表示(选择这种数据类型是为了方便访问到最近添加的元素).

填充每个 deque基本上用作 Stack 数据结构)的过程将由提供的 谓词.

所以简而言之,总体思路是所有具有特定 name 的元素都应该放在一个单独的 堆栈 中,并控制正在处理的元素的值添加到 堆栈 .

为了使负责创建收集器的方法统一且可重用,它利用了泛型并需要上述两个参数:一个函数和一个谓词.

此收集器生成的辅助地图将具有以下结构Map<String,Deque<ExampleObject>>。创建中间映射后,为了获得最终结果,其值将组合在一起并存储在列表中。

如何创建收集器

要创建一个自定义收集器,您可以使用需要以下参数的静态方法Collector.of()

  • Supplier Supplier<A> 旨在提供一个 可变容器 来存储流的元素。在这种情况下,HashMap 将用作容器。
  • Accumulator BiConsumer<A,T> 定义如何将元素添加到供应商提供的可变容器。此功能被提取到一个单独的方法 tryAdd() 中,如果给定的键不在地图中,该方法将创建一个新的 ArrayDeque。根据 stack 的状态,它可能会丢弃提供的 element 或(and)删除堆栈顶部的元素.
  • Combiner BinaryOperator<A> combiner() 建立了如何合并并行执行流时获得的两个 容器 的规则。在这里,组合器依赖于为累加器描述的相同逻辑。
  • Finisher Function<A,R> 旨在通过转换 可变容器 [=] 来产生 最终结果 128=]。此收集器的 finisher 中间映射 的所有值转储到流中并创建结果 list.
  • 特征 允许 fine-tuning 收集器通过提供有关其应如何运行的附加信息。这里应用了一个特性 Collector.Characteristics.UNORDERED。这表明并行产生的归约的部分结果的顺序并不重要,这可以提高并行流收集器的性能。

实施

代码可能如下所示:

public static <T, U> Collector<T, ?, List<T>> toFilteredList(Function<T, U> keyExtractor,
                                                             Predicate<T> condition) {
    return Collector.of(
        HashMap::new,
        (Map<U, Deque<T>> map, T next) -> tryAdd(map, next, keyExtractor, condition),
        (left, right) -> merge(left, right, keyExtractor, condition),
        map -> map.values().stream().flatMap(Deque::stream).toList(),
        Collector.Characteristics.UNORDERED);
}

public static <T, U> void tryAdd(Map<U, Deque<T>> map, T next,
                                 Function<T, U> keyExtractor,
                                 Predicate<T> condition) {
    
    Deque<T> stack = map.computeIfAbsent(keyExtractor.apply(next), k -> new ArrayDeque<>());
    
    if      (!stack.isEmpty() && condition.test(stack.peek()))  stack.pop();      // stack is not empty and element on the top has a value of null
    else if (stack.isEmpty() || condition.negate().test(next))  stack.push(next); // stack is empty - then any element could be added, or new element has a non-null value
}

public static <T, U> Map<U, Deque<T>> merge(Map<U, Deque<T>> left, Map<U, Deque<T>> right,
                                            Function<T, U> keyExtractor,
                                            Predicate<T> condition) {
    
    right.forEach((k, v) -> v.forEach(next -> tryAdd(left, next, keyExtractor, condition)));
    
    return left;
}

main() - 演示(dummyExampleObjectclass的代码未显示 )

public static void main(String[] args) {
    List<ExampleObject> list =
        List.of(new ExampleObject(1, "Real Name", "Real Value"),
            new ExampleObject(2, "Duplicate Name", "Real Value"),
            new ExampleObject(3, "Duplicate Name", null),
            new ExampleObject(4, "Duplicate Name", null),
            new ExampleObject(5, "Real Name 2", "Real Value 2"),
            new ExampleObject(6, "Duplicate Name 2", "Real Value"),
            new ExampleObject(7, "Duplicate Name 2", null));

    List<ExampleObject> result = list.stream()
        .collect(toFilteredList(ExampleObject::getName, // the key of the auxiliary map
                                exampleObject -> exampleObject.getValue() == null)); // condition to determine an element to discard

    result.forEach(System.out::println);
}

输出ID为的元素34 7 已按要求丢弃)

ExampleObject{id=6, name='Duplicate Name 2', value='Real Value'}
ExampleObject{id=2, name='Duplicate Name', value='Real Value'}
ExampleObject{id=5, name='Real Name 2', value='Real Value 2'}
ExampleObject{id=1, name='Real Name', value='Real Value'}