【问题标题】:Adding an element to the end of a stream for each element already in the stream为流中已有的每个元素添加一个元素到流的末尾
【发布时间】:2017-09-18 11:57:15
【问题描述】:

给定一个函数Function<T, T> f 和一个Stream<T> ts 什么是创建新Stream<T> 的好方法(可读性好,性能好),它首先包含原始元素,然后是f 转换的元素。

有人可能认为这会起作用:

Stream.concat(ts, ts.map(f));

但这不起作用并导致异常:

java.lang.IllegalStateException: stream has already been operated upon or closed

注意:顺序很重要:原始元素必须以正确的顺序出现在第一位,然后是转换后的元素以匹配的顺序出现。

【问题讨论】:

    标签: java java-8 java-stream concat


    【解决方案1】:

    你不能打开一瓶酒,然后把瓶子递给另一个人,让他再次打开它

    因此,我认为流的性质不可能满足您的要求。

    每个流都有一个“处理”链。你不能有两个

    所以你能得到的最接近的是从“它的起源”开始工作,比如

    Stream.concat(someList.stream(), someList.stream().map(f));
    

    例如。当然,当您没有该列表时,您可以选择:

    List<Whatever> someList = ts.collect(Collectors.asList());
    

    首先。

    【讨论】:

      【解决方案2】:

      您可以实现一个 Spliterator 来包装您的源流。在内部,您将为每个已处理的元素创建“重复”元素,然后在源为空时切换到这些重复元素:

      public class Duplicates<T> implements Spliterator<T> {
          private Spliterator<T> source;
      
          private Consumer<T>    addDuplicate;
      
          private Builder<T>     extrasStreamBuilder = Stream.builder();
          private Spliterator<T> extrasSpliterator;
      
          private Duplicates(Stream<T> source, UnaryOperator<T> f) {
              this.addDuplicate = t -> extrasStreamBuilder.add(f.apply(t));
              this.source = source.spliterator();
          }
      
          public static <T> Stream<T> of(Stream<T> source, UnaryOperator<T> f) {
              return StreamSupport.stream(new Duplicates<>(source, f), false);
          }
      
          @Override
          public boolean tryAdvance(Consumer<? super T> action) {
              boolean advanced = false;
      
              if (extrasSpliterator == null) {
                  advanced = source.tryAdvance(addDuplicate.andThen(action));
              }
      
              if (!advanced) {
                  if (extrasSpliterator == null) {
                      extrasSpliterator = extrasStreamBuilder.build().spliterator();
                  }
                  advanced = extrasSpliterator.tryAdvance(action);
              }
      
              return advanced;
          }
      
          @Override
          public void forEachRemaining(Consumer<? super T> action) {
              if (extrasSpliterator == null) {
                  source.forEachRemaining(addDuplicate.andThen(action));
                  extrasSpliterator = extrasStreamBuilder.build().spliterator();
              }
      
              extrasSpliterator.forEachRemaining(action);
          }
      
          // other spliterator methods worked with default (Eclipse) implementation for the example below, but should probably delegate to source
      }
      
      public static void main(String[] args) {
          List<String> input = Arrays.asList("1", "2", "3");
      
          Stream<String> wrapper = Duplicates.of(input.stream(), i -> i + "0");
      
          wrapper.forEach(System.out::println);
      }
      
      // Output:
      // 1
      // 2
      // 3
      // 10
      // 20
      // 30
      

      这可能取决于您的用例,因为您将 extras 保留在流构建器中,这在内存消耗方面是否足够有效。

      在实际流处理之前收集和映射的优势在于您只需遍历源一次。当检索元素需要很长时间或元素的顺序可能在流之间发生变化时,这可能会有所帮助。

      您还可以在复制之前先将一些流操作链接到源,而无需将中间结果收集到集合中。

      【讨论】:

      • 我没有仔细研究过细节,但是Spliterator 在这种情况下要走的路;这应该是公认的答案。
      • Spliterator 将极大地受益于专用的forEachRemaining 实现。另外,我会在轮询源拆分器之前添加一个extrasSpliterator==null 检查,以避免在其迭代完成后一遍又一遍地轮询。
      • @Holger 我已经在等你的意见了。我添加了空检查和forEachRemaining。这样的实现有用吗? forEachRemaining 有什么优势?它避免了tryAdvance 的哪些开销?如果我们调用 extrasBuilder.build().forEach(action) 而不是通过拆分器,情况会有所改善吗?
      • 在当前实现下,f.andThen(action) 每次都会构造一个新的Consumer 实例,因为它会捕获faction。所以forEachRemaining 只做一次。此外,它避免了使用tryAdvance 对每个元素进行评估的许多条件。它看起来很合理,但是您可以将第一条语句移到 if 块中,if(extrasSpliterator==null) { source.forEachRemaining( f.andThen(action)); extrasSpliterator = extrasStreamBuilder.build().spliterator(); }
      • 看起来真的很不错,如果需要更频繁地使用它可能是正确的做法,但就我而言,一次性使用它太费力了。
      【解决方案3】:

      当它只针对元素而不针对订单时(首先是原始项目,然后是修改后的项目),您可以使用 flatMap:

      Stream<T> s = ...;
      Stream<T> result = s.flatMap(x -> Stream.of(x, f.apply(x));
      result.forEach(System.out::println);
      

      如果顺序相关,有人可能会问你为什么要使用流,因为你不会从惰性求值中受益...

      【讨论】:

      • 顺序很重要,有问题会澄清的。
      • 关于流可能是无限的事实,你必须有一个无限的缓存来处理它。 #justsaying
      • 我不确定。但是当然,对于无限流,该操作将完全没有用,就像许多其他操作(concat,collect 的许多用途)一样
      • afaik 一个函数接受一个流并产生一个流应该尽可能地懒惰(因为只要第一个流没有完成,concat 就不会触及第二个流),但这可能是一个哲学讨论.
      • 但是实现这个懒惰并没有内在的问题。并且缓冲区大小仅限于您实际从结果流中检索的元素数。
      猜你喜欢
      • 1970-01-01
      • 2021-01-21
      • 2019-10-17
      • 2020-09-11
      • 1970-01-01
      • 1970-01-01
      • 2023-03-21
      • 2012-12-02
      • 2013-02-14
      相关资源
      最近更新 更多