如何根据自己的 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 方法的流方法删除所有重复条目吗?

我已经想到了分组,但这也行不通,因为blogAblogB相等的原因不仅仅是一个参数。我也不知道如何使用 .reduce(..),因为实际上剩下的元素不止一个。

本质上,您要么必须定义 hashCode 以使您的数据与哈希表一起使用,要么必须定义总顺序以使其与二叉搜索树一起使用。

对于哈希表,您需要声明一个包装器 class,它将覆盖 equalshashCode.

对于二叉树,您可以定义一个 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);

生成的键已经有正确的 equalshashCode 实现。

现在,如果您对终端操作没问题,您可以像这样创建一个自定义收集器:

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