【问题标题】:Collect certain elements from stream从流中收集某些元素
【发布时间】:2015-09-09 14:10:20
【问题描述】:

如何按位置从Stream 中提取两个元素?例如,我试图从Stream<String> 中提取元素 0 和 1(这些数字是任意的!)。一种天真的方法是这样的:

List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
Consumer<String> c0 = s -> System.out.println("c0.accept(" + s + ")");
Consumer<String> c1 = s -> System.out.println("c1.accept(" + s + ")");
strings.stream().skip(0).peek(c0).skip(1).peek(c1).findAny();

这会产生以下输出:

c0.accept(s0)
c0.accept(s1)
c1.accept(s1)

我知道这是因为s0 将进入流,遇到skip(0),然后遇到peek(c0)(给出第一行),然后是skip(1),它将跳过这个元素,然后显然继续流开头的下一个元素。

我以为我可以使用这些消费者来提取字符串,但 c0 会被第二个元素覆盖:

String[] extracted = new String[2];
c0 = s -> extracted[0];
c1 = s -> extracted[1];

编辑:

这些是流的特征:

  • 只有流,没有列表或数组
  • 流可能是无限的
  • 可以制作流sequential

【问题讨论】:

  • 首先,skip(0) 没有任何作用,您可以将其删除。其次,您必须多解释一下您的用例,因为您最好使用原始列表按索引提取,而不是使用流。
  • 您不能真正通过索引从Stream 中提取,因为流不是List - 它是任意流 项。您需要告诉我们您要做什么,因为这听起来很像 XY Problem。
  • @steffen 以及流是否并行?一般情况下没有“第 5 项”。
  • 为什么不向流询问迭代器,然后根据需要跳过元素?流必须是连续的。
  • 我不会说它是重复的。在linked 问题中,允许随机访问流源,许多解决方案都使用此属性。这里 OP 明确表示,这是不允许的。另一方面,问题更具体,并产生不同的答案。例如,limit 无法解决该问题。

标签: java java-8 java-stream


【解决方案1】:

鉴于您的限制,您可以将 limit() 与自定义收集器结合起来,如下所示:

public static <T, A, R> Collector<T, ?, R> collectByIndex(Set<Integer> wantedIndices, 
                                                          Collector<T, A, R> downstream) {
    class Acc {
        int pos;
        A acc = downstream.supplier().get();
    }
    return Collector.of(Acc::new, (acc, t) -> {
        if(wantedIndices.contains(acc.pos++))
            downstream.accumulator().accept(acc.acc, t);
    }, (a, b) -> {throw new UnsupportedOperationException();}, // combining not supported
       acc -> downstream.finisher().apply(acc.acc));
}

这里Set&lt;Integer&gt; wantedIndices 是包含所需元素索引的集合(不限于2)。用法:

Set<Integer> wantedIndices = new HashSet<>(Arrays.asList(1, 3));
Stream<String> input = Stream.of("s0", "s1", "s2", "s3", "s4");
List<String> result = input.limit(Collections.max(wantedIndices)+1)
            .collect(collectByIndex(wantedIndices, Collectors.toList()));
// [s1, s3]

【讨论】:

    【解决方案2】:

    这是我在其他答案中没有看到的一种方法。它使用了无处不在的Pair 类的变体:

    class Pair<T> {
        final T first;
        final T last;
        Pair(T t1, T t2) { first = t1; last = t2; }
        Pair(T t) { first = last = t; }
        Pair<T> merge(Pair<T> other) { return new Pair<>(this.first, other.last); }
    }
    

    一旦你有了这个,你就可以轻松地获取流的第一个和最后一个元素。给定无限流和所需的索引,您可以使用 skip() 和 limit() 修剪流以仅包含所需的元素:

    static <T> Pair<T> firstAndLast(Stream<T> stream, int firstIndex, int lastIndex) {
        // ensure indexes >= 0 and firstIndex <= lastIndex
        return stream.skip(firstIndex)
                     .limit(lastIndex - firstIndex + 1)
                     .map(Pair::new)
                     .reduce(Pair::merge)
                     .orElseThrow(() -> new IllegalArgumentException("nonexistent"));
    }
    

    其他变体包括将构造或合并逻辑内联到流操作中,而不是将其放在Pair 类中。重构以适应口味。

    你会这样使用它:

        Stream<String> str = Stream.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
        Pair<String> pair = firstAndLast(str, 4, 5);
        System.out.println(pair.first + " " + pair.last);
    
        e f
    

    【讨论】:

      【解决方案3】:

      这个解决方案来自 Federico Peralta Schaffner 的评论:

      public String[] collect(Stream<String> stream, int... positions) {
          String[] collect = new String[positions.length];
          Iterator<String> iterator = stream.iterator();
          int skipped = 0;
          for (int pos = 0; pos < positions.length; pos++) {
              while (skipped++ < positions[pos]) {
                  iterator.next();
              }
              collect[pos] = iterator.next();
          }
          return collect;
      }
      

      这是最直接、最直接的想法,效果很好。

      【讨论】:

        【解决方案4】:

        主要障碍是Streams 的一次性使用性质,这是可以绕过的:

        static <T> List<T> get(Stream<? extends T> s, int... positions) {
            Spliterator<? extends T> sp=s.spliterator();
            ArrayList<T> list=new ArrayList<>(positions.length);
            int current=0;
            for(int i: positions) {
                if(i<current) throw new IllegalArgumentException("positions not ascending");
                Optional<? extends T> o
                    =StreamSupport.stream(sp, false).skip(i-current).findFirst();
                if(!o.isPresent()) break;
                current=i+1;
                list.add(o.get());
            }
            return list;
        }
        

        虽然不知道喜不喜欢……

        【讨论】:

        • 看起来比@steffen 解决方案更麻烦,而且您依赖于findFirst() 不会将拆分器移动得超出必要范围这一未指明的事实......
        • 你参考他的哪个解决方案?为什么顺序流的findFirst() 会超过必要地推进拆分器?无论如何,我根本没有看到这种任务的任何用例,所以没关系。这更像是一个娱乐问题……
        • 没有注意到,@steffen 发布了两个答案(为什么允许这样做?)。我说的是this one。绝对不应该,只是没有指定。
        • @TagirValeev 这是允许的,因为有很多不同的方法可以解决相同的问题,您可以选择您认为最好的方法:)
        【解决方案5】:

        你可以这样写:

        public static void main(String[] args) throws Exception {
            List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
            System.out.println(getNthElement(strings.stream(), 0)); // prints "s0"
            System.out.println(getNthElement(strings.stream(), 1)); // prints "s1"
        }
        
        private static <T> T getNthElement(Stream<T> stream, int n) {
            return stream.skip(n).findFirst().get();
        }
        

        请注意,如果流中的元素少于n,这将引发异常。此外,仅当 Stream 不并行时才有意义。

        【讨论】:

        • 它还会多次读取Stream,如果Stream 是一次,这可能是个问题。
        • @BoristheSpider 是的,它会的。不过,我看不到任何其他方法。
        • 我会用索引压缩流,然后编写一个自定义收集器。但我确定 OP 到底想要什么,所以我认为不值得回答 XY 问题。
        • 实际上我无法多次读取流,因为我得到的是流实例而不是列表或数组。其次,我不能“压缩它”,因为它可能是无限的。
        • @steffen 你是什么意思我不能“压缩它”,因为它可能是无限的??这正是您需要压缩它的原因。
        【解决方案6】:

        这是一个(不是很漂亮,但简单且有效)的解决方案:

        List<String> strings = Arrays.asList("s0", "s1", "s2", "s3", "s4");
        String[] extracted = new String[2];
        Consumer<String> c0 = s -> extracted[0] = extracted[0] == null ? s : extracted[0];
        Consumer<String> c1 = s -> extracted[1] = extracted[1] == null ? s : extracted[1];
        strings.stream().skip(0).peek(c0).skip(1 - 0).peek(c1).findAny();
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-11-09
          • 1970-01-01
          • 1970-01-01
          • 2016-05-13
          • 2023-03-25
          相关资源
          最近更新 更多