如何根据自己的 Equal class 消除流中的重复条目
How to eliminate duplicate entries within a stream based on a own Equal class
我确实遇到了描述的类似问题 here。但是有两个区别,首先我确实使用了流 api,其次我确实已经有了 equals()
和 hashCode()
方法。但是在流中,博客的平等性在此上下文中与 Blog
class.
中定义的不相同
Collection<Blog> elements = x.stream()
... // a lot of filter and map stuff
.peek(p -> sysout(p)) // a stream of Blog
.? // how to remove duplicates - .distinct() doesn't work
我确实有一个 class 具有相同的方法让我们用方法
调用它 ContextBlogEqual
public boolean equal(Blog a, Blog b);
有什么方法可以使用我当前基于 ContextBlogEqual#equal
方法的流方法删除所有重复条目吗?
我已经想到了分组,但这也行不通,因为blogA
和blogB
相等的原因不仅仅是一个参数。我也不知道如何使用 .reduce(..),因为实际上剩下的元素不止一个。
本质上,您要么必须定义 hashCode
以使您的数据与哈希表一起使用,要么必须定义总顺序以使其与二叉搜索树一起使用。
对于哈希表,您需要声明一个包装器 class,它将覆盖 equals
和 hashCode
.
对于二叉树,您可以定义一个 Comparator<Blog>
,它尊重您的等式定义并添加一个任意但一致的排序标准。然后就可以收藏成new TreeSet<Blog>(yourComparator)
.
首先,请注意 equal(Blog, Blog)
方法在大多数情况下是不够的,因为您需要成对比较所有条目,效率不高。最好定义从博客条目中提取新密钥的函数。例如,让我们考虑以下 Blog
class:
static class Blog {
final String name;
final int id;
final long time;
public Blog(String name, int id, long time) {
this.name = name;
this.id = id;
this.time = time;
}
@Override
public int hashCode() {
return Objects.hash(name, id, time);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Blog other = (Blog) obj;
return id == other.id && time == other.time && Objects.equals(name, other.name);
}
public String toString() {
return name+":"+id+":"+time;
}
}
让我们来一些测试数据:
List<Blog> blogs = Arrays.asList(new Blog("foo", 1, 1234),
new Blog("bar", 2, 1345), new Blog("foo", 1, 1345),
new Blog("bar", 2, 1345));
List<Blog> distinctBlogs = blogs.stream().distinct().collect(Collectors.toList());
System.out.println(distinctBlogs);
此处 distinctBlogs
包含三个条目:[foo:1:1234, bar:2:1345, foo:1:1345]
。假设它是不需要的,因为我们不想比较 time
字段。创建新密钥的最简单方法是使用 Arrays.asList
:
Function<Blog, Object> keyExtractor = b -> Arrays.asList(b.name, b.id);
生成的键已经有正确的 equals
和 hashCode
实现。
现在,如果您对终端操作没问题,您可以像这样创建一个自定义收集器:
List<Blog> distinctByNameId = blogs.stream().collect(
Collectors.collectingAndThen(Collectors.toMap(
keyExtractor, Function.identity(),
(a, b) -> a, LinkedHashMap::new),
map -> new ArrayList<>(map.values())));
System.out.println(distinctByNameId);
这里我们使用keyExtractor
生成key,合并函数是(a, b) -> a
,意思是select出现重复key时,之前添加的entry。我们使用 LinkedHashMap
来保留顺序(如果您不关心顺序,请省略此参数)。最后,我们将映射值转储到新的 ArrayList
中。您可以将此类收集器创建移动到单独的方法中并对其进行概括:
public static <T> Collector<T, ?, List<T>> distinctBy(
Function<? super T, ?> keyExtractor) {
return Collectors.collectingAndThen(
Collectors.toMap(keyExtractor, Function.identity(), (a, b) -> a, LinkedHashMap::new),
map -> new ArrayList<>(map.values()));
}
这样使用会更简单:
List<Blog> distinctByNameId = blogs.stream()
.collect(distinctBy(b -> Arrays.asList(b.name, b.id)));
基本上,您需要一个像这样的辅助方法:
static <T, U> Stream<T> distinct(
Stream<T> stream,
Function<? super T, ? extends U> keyExtractor
) {
final Map<U, String> seen = new ConcurrentHashMap<>();
return stream.filter(t -> seen.put(keyExtractor.apply(t), "") == null);
}
它需要一个 Stream
,并且 returns 一个新的 Stream
,它只包含给定 keyExtractor
的不同值。一个例子:
class O {
final int i;
O(int i) {
this.i = i;
}
@Override
public String toString() {
return "O(" + i + ")";
}
}
distinct(Stream.of(new O(1), new O(1), new O(2)), o -> o.i)
.forEach(System.out::println);
这会产生
O(1)
O(2)
免责声明
正如 and in this similar answer by Stuart Marks 评论的那样,这种方法有缺陷。此处实施的操作...
- 对于有序并行流来说不稳定
- 不是顺序流的最佳选择
- 违反了
Stream.filter()
上的无状态谓词约束
将以上内容包装在您自己的库中
您当然可以使用自己的功能扩展 Stream
并在其中实现这个新的 distinct()
功能,例如喜欢 jOOλ
or Javaslang 做:
Seq.of(new O(1), new O(1), new O(2))
.distinct(o -> o.i)
.forEach(System.out::println);
我确实遇到了描述的类似问题 here。但是有两个区别,首先我确实使用了流 api,其次我确实已经有了 equals()
和 hashCode()
方法。但是在流中,博客的平等性在此上下文中与 Blog
class.
Collection<Blog> elements = x.stream()
... // a lot of filter and map stuff
.peek(p -> sysout(p)) // a stream of Blog
.? // how to remove duplicates - .distinct() doesn't work
我确实有一个 class 具有相同的方法让我们用方法
调用它ContextBlogEqual
public boolean equal(Blog a, Blog b);
有什么方法可以使用我当前基于 ContextBlogEqual#equal
方法的流方法删除所有重复条目吗?
我已经想到了分组,但这也行不通,因为blogA
和blogB
相等的原因不仅仅是一个参数。我也不知道如何使用 .reduce(..),因为实际上剩下的元素不止一个。
本质上,您要么必须定义 hashCode
以使您的数据与哈希表一起使用,要么必须定义总顺序以使其与二叉搜索树一起使用。
对于哈希表,您需要声明一个包装器 class,它将覆盖 equals
和 hashCode
.
对于二叉树,您可以定义一个 Comparator<Blog>
,它尊重您的等式定义并添加一个任意但一致的排序标准。然后就可以收藏成new TreeSet<Blog>(yourComparator)
.
首先,请注意 equal(Blog, Blog)
方法在大多数情况下是不够的,因为您需要成对比较所有条目,效率不高。最好定义从博客条目中提取新密钥的函数。例如,让我们考虑以下 Blog
class:
static class Blog {
final String name;
final int id;
final long time;
public Blog(String name, int id, long time) {
this.name = name;
this.id = id;
this.time = time;
}
@Override
public int hashCode() {
return Objects.hash(name, id, time);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Blog other = (Blog) obj;
return id == other.id && time == other.time && Objects.equals(name, other.name);
}
public String toString() {
return name+":"+id+":"+time;
}
}
让我们来一些测试数据:
List<Blog> blogs = Arrays.asList(new Blog("foo", 1, 1234),
new Blog("bar", 2, 1345), new Blog("foo", 1, 1345),
new Blog("bar", 2, 1345));
List<Blog> distinctBlogs = blogs.stream().distinct().collect(Collectors.toList());
System.out.println(distinctBlogs);
此处 distinctBlogs
包含三个条目:[foo:1:1234, bar:2:1345, foo:1:1345]
。假设它是不需要的,因为我们不想比较 time
字段。创建新密钥的最简单方法是使用 Arrays.asList
:
Function<Blog, Object> keyExtractor = b -> Arrays.asList(b.name, b.id);
生成的键已经有正确的 equals
和 hashCode
实现。
现在,如果您对终端操作没问题,您可以像这样创建一个自定义收集器:
List<Blog> distinctByNameId = blogs.stream().collect(
Collectors.collectingAndThen(Collectors.toMap(
keyExtractor, Function.identity(),
(a, b) -> a, LinkedHashMap::new),
map -> new ArrayList<>(map.values())));
System.out.println(distinctByNameId);
这里我们使用keyExtractor
生成key,合并函数是(a, b) -> a
,意思是select出现重复key时,之前添加的entry。我们使用 LinkedHashMap
来保留顺序(如果您不关心顺序,请省略此参数)。最后,我们将映射值转储到新的 ArrayList
中。您可以将此类收集器创建移动到单独的方法中并对其进行概括:
public static <T> Collector<T, ?, List<T>> distinctBy(
Function<? super T, ?> keyExtractor) {
return Collectors.collectingAndThen(
Collectors.toMap(keyExtractor, Function.identity(), (a, b) -> a, LinkedHashMap::new),
map -> new ArrayList<>(map.values()));
}
这样使用会更简单:
List<Blog> distinctByNameId = blogs.stream()
.collect(distinctBy(b -> Arrays.asList(b.name, b.id)));
基本上,您需要一个像这样的辅助方法:
static <T, U> Stream<T> distinct(
Stream<T> stream,
Function<? super T, ? extends U> keyExtractor
) {
final Map<U, String> seen = new ConcurrentHashMap<>();
return stream.filter(t -> seen.put(keyExtractor.apply(t), "") == null);
}
它需要一个 Stream
,并且 returns 一个新的 Stream
,它只包含给定 keyExtractor
的不同值。一个例子:
class O {
final int i;
O(int i) {
this.i = i;
}
@Override
public String toString() {
return "O(" + i + ")";
}
}
distinct(Stream.of(new O(1), new O(1), new O(2)), o -> o.i)
.forEach(System.out::println);
这会产生
O(1)
O(2)
免责声明
正如
- 对于有序并行流来说不稳定
- 不是顺序流的最佳选择
- 违反了
Stream.filter()
上的无状态谓词约束
将以上内容包装在您自己的库中
您当然可以使用自己的功能扩展 Stream
并在其中实现这个新的 distinct()
功能,例如喜欢 jOOλ
or Javaslang 做:
Seq.of(new O(1), new O(1), new O(2))
.distinct(o -> o.i)
.forEach(System.out::println);