Java Streams - 有效地对排序流上的项目进行分组

时间:2015-04-12 09:58:35

标签: java java-8 java-stream

我正在寻找一种实现非终端分组操作的方法,这样内存开销就会很小。

例如,考虑distinct()。在一般情况下,它别无选择,只能收集所有不同的项目,然后才向前流式传输。但是,如果我们知道输入流已经排序,则可以使用最少的内存“即时”完成操作。

我知道我可以使用迭代器包装器并自己实现分组逻辑来实现迭代器。有没有更简单的方法来实现使用流API?

- 的修改 -

我发现了一种滥用Stream.flatMap(..)来实现此目的的方法:

  private static class DedupSeq implements IntFunction<IntStream> {
    private Integer prev;

    @Override
    public IntStream apply(int value) {
      IntStream res = (prev != null && value == prev)? IntStream.empty() : IntStream.of(value);
      prev = value;
      return res;
    }    
  }

然后:

IntStream.of(1,1,3,3,3,4,4,5).flatMap(new DedupSeq()).forEach(System.out::println);

打印哪些:

1
3
4
5

通过一些更改,相同的技术可用于任何类型的内存有效的流序列分组。无论如何,我不太喜欢这个解决方案,而且我正在寻找更自然的东西(比如映射或过滤工作的方式)。此外,我在这里违约,因为提供给flatMap(..)的函数是有状态的。

2 个答案:

答案 0 :(得分:4)

如果你想要一个不会向一个不应该拥有它的函数添加可变状态的解决方案,你可以诉诸collect

static void distinctForSorted(IntStream s, IntConsumer action) {
    s.collect(()->new long[]{Long.MIN_VALUE},
              (a, i)->{ if(a[0]!=i) { action.accept(i); assert i>a[0]; a[0]=i; }},
              (a, b)->{ throw new UnsupportedOperationException(); });
}

这是因为它是使用可变容器的预期方式,但是它不能并行工作,因为在任意流位置拆分意味着可能在两个(甚至更多)线程中遇到一个值。

如果您需要通用IntStream而不是forEach操作,则首选Spliterator低级解决方案,尽管增加了复杂性。

static IntStream distinctForSorted(IntStream s) {
    Spliterator.OfInt sp=s.spliterator();
    return StreamSupport.intStream(
      new Spliterators.AbstractIntSpliterator(sp.estimateSize(),
      Spliterator.DISTINCT|Spliterator.SORTED|Spliterator.NONNULL|Spliterator.ORDERED) {
        long last=Long.MIN_VALUE;
        @Override
        public boolean tryAdvance(IntConsumer action) {
            long prev=last;
            do if(!sp.tryAdvance(distinct(action))) return false; while(prev==last);
            return true;
        }
        @Override
        public void forEachRemaining(IntConsumer action) {
            sp.forEachRemaining(distinct(action));
        }
        @Override
        public Comparator<? super Integer> getComparator() {
            return null;
        }
        private IntConsumer distinct(IntConsumer c) {
            return i-> {
                if(i==last) return;
                assert i>last;
                last=i;
                c.accept(i);
            };
        }
    }, false);
}

它甚至继承了并行支持,虽然它通过在另一个线程中处理它们之前预取一些值来工作,因此它不会加速 distinct 操作,但是如果有计算则可能是后续操作激烈的。


为了完成,这里是一个独特的操作,即不依赖于“拳击加IntStream”的任意,即未分类的HashMap,因此可能有更好的内存占用:

static IntStream distinct(IntStream s) {
    boolean parallel=s.isParallel();
    s=s.collect(BitSet::new, BitSet::set, BitSet::or).stream();
    if(parallel) s=s.parallel();
    return s;
}

仅适用于正int个值;将其扩展到完整的32位范围需要两个BitSet s因此看起来不简洁,但通常用例允许将存储限制在31位范围甚至更低......

答案 1 :(得分:1)

正确执行此操作的方法是将流转换为分裂器,然后根据返回的分裂器的属性将其包装

  • 如果源既没有排序也没有区别,则使用并发集执行简单的重复数据删除
  • 执行优化的优化dedpulication如果源分裂器已经分类。支持trySplit操作将是棘手的,因为它可能必须推进子分裂器几步,直到它可以确定它没有看到一系列非独特元素的尾巴。
  • 如果源已经是不同的,则只返回分裂器

一旦你有了分裂器,你就可以将它变回具有相同属性的流并继续对其进行流操作

由于我们无法修改现有的jdk-interfaces,因此helper API必须看起来更像这样:dedup(IntStream.of(...).map(...)).collect(...)


如果您检查java.util.stream.DistinctOps.makeRef(AbstractPipeline<?, T, ?>)的来源,您会注意到JDK或多或少地为基于参考的流做了。

只是IntStream实现(java.util.stream.IntPipeline.distinct())采用了一种不利用DISTINCTSORTED的低效方法。

它只是盲目地将IntStream转换为盒装Integer流并使用基于引用的重复数据删除,而不传递使其具有内存效率的适当标志。

如果在jdk9中尚未解决这个问题,那么它可能值得一个bug,因为如果它们不必要地丢弃流标志,那么它本质上就是不必要的内存消耗和流操作的浪费优化潜力。