从 Java 8 流中获取每个第 n 个元素元素、Java

2023-09-07 01:01:09 作者:素衣白纱,负了蒹葭

假设我有一个这样的列表:

Suppose I have a list like this :

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

是否可以使用 Java 8 流从该列表中获取每个第二个元素以获得以下内容?

Is it possible to use a Java 8 stream to take every second element from this list to obtain the following?

[1, 3, 5, 7, 9]

或者甚至可能是每三个元素?

Or maybe even every third element?

[1, 4, 7, 10]

基本上,我正在寻找一个函数来获取流的每个第 n 个元素:

Basically, I'm looking for a function to take every nth element of a stream:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> list2 = list.stream().takenth(3).collect(Collectors.toList());
System.out.println(list2);
// => [1, 4, 7, 10]

推荐答案

引入 Java 流的主要动机之一是允许并行操作.这导致了对 Java 流(如 mapfilter 等操作)的要求独立于项目在流中的位置或其周围的项目.这样做的好处是可以轻松拆分流以进行并行处理.它的缺点是使某些操作更加复杂.

One of the prime motivations for the introduction of Java streams was to allow parallel operations. This led to a requirement that operations on Java streams such as map and filter be independent of the position of the item in the stream or the items around it. This has the advantage of making it easy to split streams for parallel processing. It has the disadvantage of making certain operations more complex.

因此,简单的答案是,没有简单的方法来执行诸如获取每第 n 个项目或将每个项目映射到所有先前项目的总和的事情.

So the simple answer is that there is no easy way to do things such as take every nth item or map each item to the sum of all previous items.

实现您的要求最直接的方法是使用您从中流式传输的列表的索引:

The most straightforward way to implement your requirement is to use the index of the list you are streaming from:

List<String> list = ...;
return IntStream.range(0, list.size())
    .filter(n -> n % 3 == 0)
    .mapToObj(list::get)
    .toList();

更复杂的解决方案是创建一个自定义收集器,将每第 n 个项目收集到一个列表中.

A more complicated solution would be to create a custom collector that collects every nth item into a list.

class EveryNth<C> {
    private final int nth;
    private final List<List<C>> lists = new ArrayList<>();
    private int next = 0;

    private EveryNth(int nth) {
        this.nth = nth;
        IntStream.range(0, nth).forEach(i -> lists.add(new ArrayList<>()));
    }

    private void accept(C item) {
        lists.get(next++ % nth).add(item);
    }

    private EveryNth<C> combine(EveryNth<C> other) {
        other.lists.forEach(l -> lists.get(next++ % nth).addAll(l));
        next += other.next;
        return this;
    }

    private List<C> getResult() {
        return lists.get(0);
    }

    public static Collector<Integer, ?, List<Integer>> collector(int nth) {
        return Collector.of(() -> new EveryNth(nth), 
            EveryNth::accept, EveryNth::combine, EveryNth::getResult));
}

可以这样使用:

Stream.of("Anne", "Bill", "Chris", "Dean", "Eve", "Fred", "George")
    .parallel().collect(EveryNth.collector(3)).toList();

返回结果 [Anne", Dean", George"] 如您所愿.

Which returns the result ["Anne", "Dean", "George"] as you would expect.

即使使用并行处理,这也是一种非常低效的算法.它将它接受的所有项目拆分为 n 个列表,然后只返回第一个.不幸的是,它必须通过累积过程保留所有项目,因为直到它们组合起来它才知道哪个列表是第 n 个.

This is a very inefficient algorithm even with parallel processing. It splits all items it accepts into n lists and then just returns the first. Unfortunately it has to keep all items through the accumulation process because it's not until they are combined that it knows which list is the nth one.

鉴于收集器解决方案的复杂性和低效率,如果可以的话,我绝对建议您优先使用上述基于索引的解决方案.如果您没有使用支持 get 的集合(例如,您传递的是 Stream 而不是 List),那么您要么需要使用 Collectors.toList 收集流或使用上面的 EveryNth 解决方案.

Given the complexity and inefficiency of the collector solution I would definitely recommend sticking with the indices based solution above in preference to this if you can. If you aren't using a collection that supports get (e.g. you are passed a Stream rather than a List) then you will either need to collect the stream using Collectors.toList or use the EveryNth solution above.

 
精彩推荐