如何使用 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 为 3
、4
和 7
的记录。
我可以通过做一些流和一些 for
循环来解决这个问题,但我不太喜欢这个实现,我知道只有一个流应该是可能的。
我该如何解决这个问题?
做我们想做的事情的一种可能性是
- 收集一些
Collection<ExampleData>
、 中的所有结果
- 构造一个
TreeSet<ExampleData>
with a suiting Comparator<? super ExampleData>
使得重复 wrt。我们的规范是“相等的”(它们会被集合自动过滤掉),并且
- 将
Collection
中的所有元素添加到 TreeSet
。
stream-based 实现可能如下所示:
final TreeSet<ExampleObject> deduped = objects.stream()
.collect(Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparing(ExampleObject::getName)
.thenComparing(ExampleObject::getValue))));
如果我们不喜欢 stream-based 方法,我们也可以使用“传统的”命令式方法解决此问题:
final TreeSet<ExampleObject> deduped = new TreeSet<>(
Comparator.comparing(ExampleObject::getName)
.thenComparing(ExampleObject::getValue));
deduped.addAll(objects);
关于性能的一句话:
重复数据删除不是“免费”的。在提供的解决方案中,我们用执行时间来支付费用。 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
如果我对您的目标的理解正确,您只想删除 value
为 null
的元素,前提是至少还有一个元素具有相同的 name
。
同时,不应丢弃所有名称为 non-null value
的元素(因此元素为 id
的 2
和 6
在你的例子中应该保留; 如果这个假设不正确,下面实现的这个行为可以很容易地改变).
此外,如果列表中只有一个元素具有特定 name
和值 null
。这个假设是基于纯粹的逻辑:这样的元素不能被认为是重复的,因为它的 name
属性是唯一的。
收藏家描述
为了实现这一点,我编写了一个可以与Collectors.groupingBy
模糊比较的自定义收集器。
它的目的是创建一个 map,其中 keys 将由给定的 keyExtractor
function和values将用元素的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()
- 演示(dummyExampleObject
class的代码未显示 )
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为的元素3
、4
、和 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'}
我正在尝试弄清楚如何在 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 为 3
、4
和 7
的记录。
我可以通过做一些流和一些 for
循环来解决这个问题,但我不太喜欢这个实现,我知道只有一个流应该是可能的。
我该如何解决这个问题?
做我们想做的事情的一种可能性是
- 收集一些
Collection<ExampleData>
、 中的所有结果
- 构造一个
TreeSet<ExampleData>
with a suitingComparator<? super ExampleData>
使得重复 wrt。我们的规范是“相等的”(它们会被集合自动过滤掉),并且 - 将
Collection
中的所有元素添加到TreeSet
。
stream-based 实现可能如下所示:
final TreeSet<ExampleObject> deduped = objects.stream()
.collect(Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparing(ExampleObject::getName)
.thenComparing(ExampleObject::getValue))));
如果我们不喜欢 stream-based 方法,我们也可以使用“传统的”命令式方法解决此问题:
final TreeSet<ExampleObject> deduped = new TreeSet<>(
Comparator.comparing(ExampleObject::getName)
.thenComparing(ExampleObject::getValue));
deduped.addAll(objects);
关于性能的一句话:
重复数据删除不是“免费”的。在提供的解决方案中,我们用执行时间来支付费用。 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 ID3
,4
, and7
如果我对您的目标的理解正确,您只想删除 value
为 null
的元素,前提是至少还有一个元素具有相同的 name
。
同时,不应丢弃所有名称为 non-null value
的元素(因此元素为 id
的 2
和 6
在你的例子中应该保留; 如果这个假设不正确,下面实现的这个行为可以很容易地改变).
此外,如果列表中只有一个元素具有特定 name
和值 null
。这个假设是基于纯粹的逻辑:这样的元素不能被认为是重复的,因为它的 name
属性是唯一的。
收藏家描述
为了实现这一点,我编写了一个可以与Collectors.groupingBy
模糊比较的自定义收集器。
它的目的是创建一个 map,其中 keys 将由给定的 keyExtractor
function和values将用元素的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()
- 演示(dummyExampleObject
class的代码未显示 )
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为的元素3
、4
、和 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'}