为什么Java Streams没有中间组合操作?

时间:2017-11-30 13:01:12

标签: java-8 java-stream

注意:我不一定在寻找下面描述的具体示例问题的解决方案。我真的很感兴趣为什么在Java 8中不能开箱即用。

Java流是懒惰的。最后他们有一个终端操作 我的解释是这个终端操作将通过流提取所有值。没有任何中间操作可以做到这一点。为什么没有中间操作通过流引入任意数量的元素?像这样:

stream
    .mapMultiple(this::consumeMultipleElements) // or groupAndMap or combine or intermediateCollect or reverseFlatMap
    .collect(Collectors.toList());

当下游操作尝试将流推进一次时,中间操作可能会尝试多次推进上游(或根本不推进)。

我会看到几个用例:
(这些只是示例。所以你可以看到它确实可以处理这些用例,但它不是“流式传输方式”,而且这些解决方案缺乏Streams所具有的理想的懒惰属性。)

  • 将多个元素组合到一个新元素中,以传递到流的其余部分。 (例如,成对(1,2,3,4,5,6) ➔ ((1,2),(3,4),(5,6))

    // Something like this,
    // without needing to consume the entire stream upfront,
    // and also more generic. (The combiner should decide for itself how many elements to consume/combine per resulting element. Maybe the combiner is a Consumer<Iterator<E>> or a Consumer<Supplier<E>>)
    public <E, R> Stream<R> combine(Stream<E> stream, BiFunction<E, E, R> combiner) {
        List<E> completeList = stream.collect(toList());
        return IntStream.range(0, completeList.size() / 2)
            .mapToObj(i -> combiner.apply(
                    completeList.get(2 * i),
                    completeList.get(2 * i + 1)));
    }
    
  • 确定Stream是否为空(将Stream映射到可选的非空流)

    // Something like this, without needing to consume the entire stream
    public <E> Optional<Stream<E>> toNonEmptyStream(Stream<E> stream) {
        List<E> elements = stream.collect(toList());
        return elements.isEmpty()
            ? Optional.empty()
            : Optional.of(elements.stream());
    }
    
  • 拥有一个不会终止流的惰性Iterator(允许基于更复杂的逻辑跳过元素,然后只是skip(long n))。

    Iterator<E> iterator = stream.iterator();
    // Allow this without throwing a "java.lang.IllegalStateException: stream has already been operated upon or closed"
    stream.collect(toList());
    

当他们设计Streams及其周围的一切时,他们是否忘记了这些用例,还是明确地将其删除了? 我知道在处理并行流时这些可能会产生意想不到的结果,但在我看来,这是一个可以记录的风险。

2 个答案:

答案 0 :(得分:5)

您想要的所有操作实际上都可以在Stream API中实现,但不是开箱即用。

将多个元素组合成元素对 - 您需要自定义Spliterator。这是Tagir Valeev这样做的。他有一个名为StreamEx的绝对野兽,可以做很多其他有用的东西,不支持开箱即用。

我不明白你的第二个例子,但我敢打赌它也是可行的。

skip更复杂的操作是java-9通过dropWhiletakeWhilePredicate作为输入。

请注意,如果您说没有任何中间操作可以做到这一点并不准确 - 那么sorteddistinct就是这样做的。否则他们无法工作。还有flatMap这样的行为,但这更像是一个错误。

另一件事是并行流的中间操作没有定义的顺序,因此这样的有状态中间操作将具有并行流的未知条目。另一方面,您总是可以选择滥用以下内容:

List<Something> list = Collections.synchronizedList()
.map(x -> {
     list.add(x);
     // your mapping
 })

如果我是你并且真的认为我可能需要那个,我不会这样做,但为了以防万一......

答案 1 :(得分:3)

并非每个终端操作都会“通过流提取所有值”。终端操作iterator()spliterator()不会立即获取所有值并允许进行延迟处理,包括再次构建新的Stream。对于后者,强烈建议使用spliterator(),因为这样可以将更多元信息传递给新流,并且意味着更少的对象包装。

E.g。你的第二个例子可以实现为

public static <T> Stream<T> replaceWhenEmpty(Stream<T> s, Supplier<Stream<T>> fallBack) {
    boolean parallel = s.isParallel();
    Spliterator<T> sp = s.spliterator();
    Stream.Builder<T> firstElement;
    if(sp.getExactSizeIfKnown()==0 || !sp.tryAdvance(firstElement=Stream.builder())) {
        s.close();
        return fallBack.get();
    }
    return Stream.concat(firstElement.build(), StreamSupport.stream(sp, parallel))
                 .onClose(s::close);
}

对于您的一般问题,我没有看到这些示例的一般抽象应该是什么样子,除了已经存在的spliterator()方法。正如the documentation所说的那样

  

但是,如果提供的流操作不提供所需的功能,则可以使用BaseStream.iterator()和BaseStream.spliterator()操作来执行受控遍历。