【问题标题】:Java - Intersection of multiple collections using stream + lambdasJava - 使用流 + lambdas 的多个集合的交集
【发布时间】:2017-07-02 01:20:55
【问题描述】:

我有以下功能统一多个集合(包括重复元素):

public static <T> List<T> unify(Collection<T>... collections) {
        return Arrays.stream(collections)
               .flatMap(Collection::stream)
               .collect(Collectors.toList()); 
}

如果集合的交集有一个类似签名的函数(使用类型相等),那就太好了。例如:

public static <T> List<T> intersect(Collection<T>... collections) {
     //Here is where the magic happens
}

我找到了 intersect 函数的实现,但它不使用流:

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) {
    Set<T> common = new LinkedHashSet<T>();
    if (!collections.isEmpty()) {
       Iterator<? extends Collection<T>> iterator = collections.iterator();
       common.addAll(iterator.next());
       while (iterator.hasNext()) {
          common.retainAll(iterator.next());
       }
    }
    return common;
}

有没有办法实现类似于使用流的统一功能?我在 java8/stream api 方面不是很有经验,因此一些建议会很有帮助。

【问题讨论】:

  • 你认为为什么需要流?
  • 纯属好奇!我忘了提到我对 java 8/stream API 真的很陌生,所以我目前正在尝试通过使用 api 来了解更多信息 :)
  • 对。就个人而言,我觉得学习这些 API 的最好方法是尝试自己解决这样的问题。试一试,如果遇到困难,请返回一个具体问题,概述您的问题。
  • 当前 Java 中 Streams 的实现受到严重限制,因为一个方法只能返回一个值,因此一次处理多个流源非常麻烦(与例如 Haskell 相比,它是微不足道的处理多个无限列表)。

标签: java collections lambda java-8 java-stream


【解决方案1】:

您可以在某个实用程序类中编写自己的收集器并使用它:

public static <T, S extends Collection<T>> Collector<S, ?, Set<T>> intersecting() {
    class Acc {
        Set<T> result;

        void accept(S s) {
            if(result == null) result = new HashSet<>(s);
            else result.retainAll(s);
        }

        Acc combine(Acc other) {
            if(result == null) return other;
            if(other.result != null) result.retainAll(other.result);
            return this;
        }
    }
    return Collector.of(Acc::new, Acc::accept, Acc::combine, 
                        acc -> acc.result == null ? Collections.emptySet() : acc.result, 
                        Collector.Characteristics.UNORDERED);
}

用法很简单:

Set<T> result = Arrays.stream(collections).collect(MyCollectors.intersecting());

但请注意,收集器不能短路:即使中间结果是空集合,它仍会处理流的其余部分。

这样的收集器在我的免费StreamEx 库中很容易获得(请参阅MoreCollectors.intersecting())。它适用于上面的普通流,但如果你将它与 StreamEx(它扩展了普通流)一起使用,它会变得短路:处理实际上可能会提前停止。

【讨论】:

  • 看起来很有趣。我稍后会仔细看看。你的图书馆看起来真的很酷,就像我看到的一样!它提供了一些我在 C# 中错过的 Linq 的一些功能。遗憾的是,对于我将使用 intersect/unify 函数的项目,我只允许使用 java.util 和其他一些基本库。
  • @Jota.Toledo,那么您可以将收集器从答案复制到某个实用程序类并使用它。
【解决方案2】:

虽然很容易将retainAll 视为一个黑盒批量操作,它必须是实现交集操作的最有效方式,但它只是意味着迭代整个集合并测试每个元素是否包含在集合作为参数传递。您在 Set 上调用它这一事实并不意味着任何优势,因为它是 other 集合,其 contains 方法将决定整体性能。

这意味着线性扫描一个集合并测试所有其他集合中的每个元素的包含将与为每个集合执行retainAll 相当。首先迭代最小集合的奖励积分:

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) {
    if(collections.isEmpty()) return Collections.emptySet();
    Collection<T> smallest
        = Collections.min(collections, Comparator.comparingInt(Collection::size));
    return smallest.stream().distinct()
        .filter(t -> collections.stream().allMatch(c -> c==smallest || c.contains(t)))
        .collect(Collectors.toSet());
}

或者,或者

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) {
    if(collections.isEmpty()) return Collections.emptySet();
    Collection<T> smallest
        = Collections.min(collections, Comparator.comparingInt(Collection::size));
    HashSet<T> result=new HashSet<>(smallest);
    result.removeIf(t -> collections.stream().anyMatch(c -> c!=smallest&& !c.contains(t)));
    return result;
}

【讨论】:

  • 这是一个有趣的方法!我想过找到最小的集合作为起点,但我没有进一步发展我的想法。
【解决方案3】:

我认为使用 Set 而不是 List 可能更有意义(也许这是您问题中的一个错字):

public static <T> Set<T> intersect(Collection<T>... collections) {
     //Here is where the magic happens
     return (Set<T>) Arrays.stream(collections).reduce(
             (a,b) -> {
                 Set<T> c = new HashSet<>(a);
                 c.retainAll(b);
                 return c;
             }).orElseGet(HashSet::new);
}

【讨论】:

  • 请注意,您的解决方案可能会产生很多垃圾:每个输入集合都会创建一个额外的集合。当我们有很多短集合时,这会显着减慢处理速度。
  • 感谢您的评论@Tagir。您是对的,有更有效的解决方案可能,例如您出色的 StreamEx 库。我将把这个答案留在这里用于教育目的,因为它很短,它很好地演示了 reduce 的使用,并且不需要任何外部库。
  • @TagirValeev 我也注意到了这一点。我想知道这种方法的复杂性是什么。
【解决方案4】:

这是一个 Set 实现。 retainAll() 是一个 Collection 方法,因此它适用于所有这些方法。

public static <T> Set<T> intersect(Collection<T>... collections)
{
    return new HashSet<T>(Arrays.stream(collections).reduce(
            ((a, b) -> {
                a.retainAll(b);
                return a;
            })
    ).orElse(new HashSet<T>());
}

如果顺序很重要,则使用 List。

public static <T> List<T> intersect2(Collection<T>... collections)
{
    return new ArrayList<T>(Arrays.stream(collections).reduce(
            ((a, b) -> {
                a.retainAll(b);
                return a;
            })
    ).orElse(new ArrayList<T>()));
}

Java 集合让它们看起来几乎相同。如果需要,您可以将列表过滤为不同的,因为它可能包含重复项。

public static <T> List<T> intersect2(Collection<T>... collections)
{
    return new ArrayList<T>(Arrays.stream(collections).reduce(
            ((a, b) -> {
                a.retainAll(b);
                return a;
            })
    ).orElse(new ArrayList<T>())).stream().distinct());
}

【讨论】:

  • 我看不出这对我的答案有什么好处,除了你现在正在改变一些输入集合作为副作用,这似乎相当不可取。
【解决方案5】:

你可以用流来写它,如下所示:

return collections.stream()
        .findFirst()        // find the first collection
        .map(HashSet::new)  // make a set out of it
        .map(first -> collections.stream()
                .skip(1)    // don't need to process the first one
                .collect(() -> first, Set::retainAll, Set::retainAll) 
        )
        .orElseGet(HashSet::new); // if the input collection was empty, return empty set

3 参数 collect 复制您的 retainAll 逻辑

流实现让您可以灵活地更轻松地调整逻辑。例如,如果您的所有集合都是集合,您可能希望从最小集合而不是第一个集合开始(为了性能)。为此,您可以将findFirst() 替换为min(comparing(Collection::size)) 并去掉skip(1)。或者您可以通过并行运行第二个流来查看您使用的数据类型是否获得了更好的性能,您只需将stream 更改为parallelStream。

【讨论】:

  • 这假定迭代顺序是可重现的,因此skip(1) 将始终跳过第一次迭代的第一个遇到的元素。但这不能保证,如果输入是非特定的Collection。即使,如果集合的 Iterator 每次都以相同的顺序报告元素,则 skip(1) 不需要遵守该顺序,如果源没有报告 ORDERED 特征(这可以用 @987654334 显示@ 和一个并行流)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-06-02
  • 1970-01-01
  • 2022-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多