小编典典

为什么stream.spliterator()的tryAdvance会将项目累积到缓冲区中?

java

SpliteratorStream管道获取可能会返回StreamSpliterators.WrappingSpliterator的实例。例如,获取以下内容Spliterator

Spliterator<String> source = new Random()
            .ints(11, 0, 7) // size, origin, bound
            .filter(nr -> nr % 2 != 0)
            .mapToObj(Integer::toString)
            .spliterator();

鉴于以上所述Spliterator<String> source,当我们通过的tryAdvance (Consumer<? super P_OUT> consumer)方法(Spliterator在本例中为StreamSpliterators.WrappingSpliterator的一个实例)单独遍历元素时,它将首先将项目累积到内部缓冲区中,然后再使用这些项目,如我们在StreamSpliterators.java中所看到的那样。
#298
。从简单的角度来看,doAdvance()先将项目插入buffer,然后再获取下一个项目并将其传递给consumer.accept (…)

public boolean tryAdvance(Consumer<? super P_OUT> consumer) {
    boolean hasNext = doAdvance();
    if (hasNext)
        consumer.accept(buffer.get(nextToConsume));
    return hasNext;
}

但是,我没有弄清楚这一点的必要性buffer

在这种情况下,为什么不将的consumer参数tryAdvance简单地用作Sink管道的终端?


阅读 348

收藏
2020-11-16

共1个答案

小编典典

我大都同意@Holger的出色回答,但我会不同地强调口音。我认为您很难理解对缓冲区的需求,因为您对Stream
API所允许的思维模型非常简单。如果一个人想着流中的序列mapfilter,也没有必要额外的缓冲,因为这些行动有2个重要的“好”的属性:

  1. 一次处理一个元素
  2. 结果产生0或1个元素

但是,在一般情况下,这些都不是正确的。正如@Holger所提到的flatMap,Java
8中已经打破了规则2,在Java9中,他们终于添加了takeWhile,它实际上是在整个Stream->
Stream而不是在每个元素的基础上进行转换的(并且这是AFAIK的第一个中间衬衫循环操作)。

我不太同意@Holger的另一点是,我认为最根本的原因与他在第二段中提到的原因(即a)有些不同,您可能会称其tryAdvanceStream多次结束,而b)“ 没有保证呼叫者将始终通过相同的消费者 ”)。我认为最重要的原因是,Spliterator
在功能上必须相同,Stream必须支持短路和惰性(即不处理全部内容Stream或不支持未绑定流的能力)。换句话说,即使Spliterator
API(相当奇怪)要求您必须使用同一Consumer对象的所有方法的所有调用一个给定的Spliterator,你仍然需要tryAdvancetryAdvance实现仍然必须使用一些缓冲区。如果您所拥有的只是全部,就无法停止处理数据,forEachRemaining(Consumer<? super T> )因此您将无法实现与之相似findFirsttakeWhile使用的任何数据。实际上,这是JDK实现内部使用Sink接口而不是接口Consumer(以及“换行”
wrapAndCopyInto代表的意思)的原因之一:Sink具有其他boolean cancellationRequested()方法。

所以 总结一下 :需要一个缓冲,因为我们希望Spliterator

  1. 使用简单方法Consumer,它不提供报告处理/取消后端的方法
  2. 提供一种手段,可以根据(逻辑)使用者的请求停止处理数据。

请注意,这两个要求实际上有点矛盾。

示例和一些代码

在这里,我想提供一些代码示例,我认为如果没有当前的API协定(接口),没有附加缓冲区就无法实现。本示例基于您的示例。

有一个简单的整数整数Collat​​z序列被猜想总是最终命中1。AFAIK该猜想尚未得到证明,但已针对许多整数(至少对于整个32位int范围)进行了验证。

因此,假设我们要解决的问题如下:从Collat​​z序列流中的1到1,000,000范围内的随机起始数字中查找第一个以十进制表示形式包含“ 123”的数字。

这是一个仅使用Stream(不是Spliterator)的解决方案:

static String findGoodNumber() {
    return new Random()
            .ints(1, 1_000_000) // unbound!
            .flatMap(nr -> collatzSequence(nr))
            .mapToObj(Integer::toString)
            .filter(s -> s.contains("123"))
            .findFirst().get();
}

这里collatzSequence是一个返回Stream包含Collat​​z序列直到第一个1
的函数(对于nitpickers,请让它在当前值大于时也停止,Integer.MAX_VALUE /3这样我们就不会溢出)。

每一个这样Stream的返回collatzSequence势必。标准Random也会最终生成所提供范围内的每个数字。这意味着我们可以保证流中最终会有一些“好”的数字(例如123),并且findFirst存在短路,因此整个操作实际上将终止。但是,没有合理的Stream
API实现可以预测这一点。

现在假设由于某些奇怪的原因,您想使用middle来执行相同的操作Spliterator。即使您只有一个逻辑并且不需要不同Consumer的,也不能使用forEachRemaining。因此,您必须执行以下操作:

static Spliterator<String> createCollatzRandomSpliterator() {
    return new Random()
            .ints(1, 1_000_000) // unbound!
            .flatMap(nr -> collatzSequence(nr))
            .mapToObj(Integer::toString)
            .spliterator();
}

static String findGoodNumberWithSpliterator() {
    Spliterator<String> source = createCollatzRandomSpliterator();

    String[] res = new String[1]; // work around for "final" closure restriction

    while (source.tryAdvance(s -> {
        if (s.contains("123")) {
            res[0] = s;
        }
    })) {
        if (res[0] != null)
            return res[0];
    }
    throw new IllegalStateException("Impossible");
}

同样重要的是,对于某些起始数字,Collat​​z序列将包含多个匹配数字。例如,41123123370(== 41123 * 3 + 1)都包含“
123”。这意味着我们真的不希望Consumer在第一个匹配命中后被调用。但是由于Consumer没有公开报告处理结束的任何方法,WrappingSpliterator所以不能仅仅将我们传递Consumer给内部Spliterator。唯一的解决方案是将内部的所有结果flatMap(以及所有后处理)累积到某个缓冲区中,然后一次在该缓冲区上迭代一个元素。

2020-11-16