【问题标题】:guava's Streams::findLast implementation番石榴的 Streams::findLast 实现
【发布时间】:2025-11-27 19:00:01
【问题描述】:

我正在研究来自 guava 的 Streams::findLast 的实现,在尝试理解它的同时,有几件事我根本无法理解。这是它的实现:

public static <T> java.util.Optional<T> findLast(Stream<T> stream) {
    class OptionalState {

        boolean set = false;
        T value = null;

        void set(@Nullable T value) {
            set = true;
            this.value = value;
        }

        T get() {
            checkState(set);
            return value;
        }
    }
    OptionalState state = new OptionalState();

    Deque<Spliterator<T>> splits = new ArrayDeque<>();
    splits.addLast(stream.spliterator());

    while (!splits.isEmpty()) {
        Spliterator<T> spliterator = splits.removeLast();

        if (spliterator.getExactSizeIfKnown() == 0) {
            continue; // drop this split
        }

        // Many spliterators will have trySplits that are SUBSIZED even if they are not themselves
        // SUBSIZED.
        if (spliterator.hasCharacteristics(Spliterator.SUBSIZED)) {
            // we can drill down to exactly the smallest nonempty spliterator
            while (true) {
                Spliterator<T> prefix = spliterator.trySplit();
                if (prefix == null || prefix.getExactSizeIfKnown() == 0) {
                    break;
                } else if (spliterator.getExactSizeIfKnown() == 0) {
                    spliterator = prefix;
                    break;
                }
            }

            // spliterator is known to be nonempty now
            spliterator.forEachRemaining(state::set);
            return java.util.Optional.of(state.get());
        }

        Spliterator<T> prefix = spliterator.trySplit();
        if (prefix == null || prefix.getExactSizeIfKnown() == 0) {
            // we can't split this any further
            spliterator.forEachRemaining(state::set);
            if (state.set) {
                return java.util.Optional.of(state.get());
            }
            // fall back to the last split
            continue;
        }
        splits.addLast(prefix);
        splits.addLast(spliterator);
    }
    return java.util.Optional.empty();
}

老实说,从本质上讲,实现并不是那么复杂,但这里有一些我觉得有点奇怪的事情(如果这个问题被关闭为“基于意见”,我会在这里承担责任,我理解可能发生)。


首先是创建OptionalState 类,这可以替换为单个元素的数组:

T[] state = (T[]) new Object[1];

使用起来很简单:

spliterator.forEachRemaining(x -> state[0] = x);

那么整个方法可以拆分成3个部分:

1) 当某个Spliterator已知为空时:

if (spliterator.getExactSizeIfKnown() == 0) 

在这种情况下,这很容易 - 只需放下它。

2) 如果知道 Spliterator 是 SUBSIZED。这是“快乐路径”场景;在这种情况下,我们可以拆分它,直到我们到达最后一个元素。基本上,实现说:拆分直到prefixnull 或者它为空(在这种情况下使用“正确”拆分器),或者如果拆分后已知“正确”拆分器为空,则使用@987654330 @ 一。这是通过:

// spliterator is known to be nonempty now
spliterator.forEachRemaining(state::set);
return java.util.Optional.of(state.get());

第二个问题其实是关于这条评论的:

// Many spliterators will have trySplits that are SUBSIZED 
// even if they are not themselves SUBSIZED.

这很有趣,但我找不到这样的例子,如果有人能给我介绍一个,我将不胜感激。事实上,因为这个注释的存在,接下来的代码(方法的第三部分不能像第二个那样用while(true)来完成),因为它假设在trySplit之后我们可以获得SpliteratorSUBSIZED,即使我们最初的不是,所以它必须到findLast的最开始。

3) 这部分方法是当一个Spliterator已知SUBSIZED并且在这种情况下它没有已知的大小;因此它依赖于源中的 Spliterator 是如何实现的,在这种情况下,实际上 findLast 没有什么意义......例如,来自 HashSetSpliterator 将返回最后一个存储桶中的最后一个条目。 ..

【问题讨论】:

    标签: java java-8 java-stream guava


    【解决方案1】:
    1. 当你迭代一个未知大小的Spliterator 时,你必须跟踪是否遇到了一个元素。这可以通过调用tryAdvance 并使用返回值或使用forEachRemainingConsumer 来完成,该Consumer 记录是否遇到了一个元素。当您走后一条路线时,专用类比数组更简单。一旦你有了一个专门的类,为什么不将它也用于SIZED 拆分器。

      让我感到奇怪的是,这个只作为Consumer 存在的本地类没有实现Consumer,而是需要通过state::set 进行绑定。

    2. 考虑

      Stream.concat(
          Stream.of("foo").filter(s -> !s.isEmpty()),
          Stream.of("bar", "baz"))
      

      代表整个流的Spliterator 不能有SIZED 特性。但是当拆分出第一个未知大小的子流时,剩余的流具有已知大小。

      测试代码:

      Spliterator<String> sp = Stream.concat(
          Stream.of("foo").filter(s -> !s.isEmpty()),
          Stream.of("bar", "baz"))
          .spliterator();
      do {
          System.out.println(
                "SIZED: "+sp.hasCharacteristics(Spliterator.SIZED)
              + ", SUBSIZED: "+sp.hasCharacteristics(Spliterator.SUBSIZED)
              + ", exact size if known: "+sp.getExactSizeIfKnown());
      } while(sp.trySplit() != null);
      

      结果:

      SIZED: false, SUBSIZED: false, exact size if known: -1
      SIZED: true, SUBSIZED: true, exact size if known: 2
      SIZED: true, SUBSIZED: true, exact size if known: 1
      

      但对我来说,当有人在评论中告诉知道拆分可以改变特征,然后用SUBSIZED 进行预测试,而不是仅仅进行拆分并检查结果是否已知时,这看起来很奇怪尺寸。毕竟,当特征不存在时,代码无论如何都会在替代分支中进行拆分。在my old answer 中,我进行了预测试以避免分配数据结构,但在这里,ArrayDeque 始终被创建和使用。但我认为,即使是我的旧答案也可以简化。

    3. 我不确定你的目标是什么。当Spliterator 具有ORDERED 特征时,遍历和拆分的顺序是明确定义的。由于HashSet 没有排序,“last”一词毫无意义。如果您是激进的,您可以优化操作以仅返回无序流的第一个元素;这是有效的,而且速度更快。

      奇怪的是,这个条件是:

      if (prefix == null || prefix.getExactSizeIfKnown() == 0) {
          // we can't split this any further
      

      (以及SUBSIZED 路径中的类似循环终止)

      仅仅因为一个前缀碰巧有一个已知的零大小,并不意味着后缀不能进一步拆分。规范中没有任何说明。

      由于这种情况,Stream.concat(Stream.of("foo"), Stream.of("bar","baz")) 可以得到最佳处理,而对于Stream.concat(Stream.of(), Stream.of("bar", "baz")),它将退回到遍历,因为第一个前缀的已知大小为零。

    【讨论】: