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

60

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

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

能否使用Java 8流来从此列表中获取每个第二个元素以获得以下结果?

[1, 3, 5, 7, 9]

或者甚至是每三个元素?

[1, 4, 7, 10]

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

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]

1
如果这是一个简化的情况,那么也许了解实际情况对于得出解决方案会有帮助。但如果不是这种情况的话,你可以通过取模2或3来进行过滤。 - the8472
为什么要使用流?源是流吗?结果总是以列表结束吗?然后将流转换为迭代器,并使用外部 int 来跟踪项目编号。如果源不是流,请使用 for 循环。在这里使用流的唯一原因是结果应该是流或源是流... - Erk
9个回答

56

Java流被引入的主要动机之一是允许并行操作。这导致了对Java流上的操作(如mapfilter)的要求,即这些操作应独立于流中项目的位置或其周围的项目。这具有将流轻松拆分以进行并行处理的优点。它的缺点是使某些操作更加复杂。

因此,简单的答案是没有简单的方法来执行诸如获取每个第n项或将每个项映射到所有前面项的总和等操作。

实现您的需求最直接的方法是使用您正在流式传输的列表的索引:

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

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

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();

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

考虑到收集器解决方案的复杂性和低效性,如果可以的话,我肯定建议优先选择上面基于索引的解决方案。如果您没有使用支持get的集合(例如传递给您的是Stream而不是List),那么您将需要使用Collectors.toList来收集流或者使用上面的EveryNth解决方案。


4
创建顺序流的这种收集器并不是很难,但是正确的并行实现会非常低效。因此,在我看来,最好放弃基于收集器的解决方案,使用索引。 - Tagir Valeev
2
我认为一个高效的并行实现应该依赖于ORDEREDSIZEDSUBSIZED分裂特性。 - the8472
2
@TagirValeev 我会为了读者可能感兴趣而添加一个收集器。我同意这种方法非常低效 - 使用索引更加直接。 - sprinter
指标解决方案解决了我的问题。谢谢!我会接受这个答案,因为你还付出了额外的努力来创建基于收集器的解决方案。 - Michel Krämer
3
for (int i = 0; i < list.size(); i += 3) ... - Erk

14

编辑 - 2017年11月28日

正如用户@Emiel在评论中建议的那样,最好的方法是使用 Stream.itearate 通过一系列索引驱动列表:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);

List<Integer> result = Stream.iterate(0, i -> i + skip)
    .limit(limit)
    .map(list::get)
    .collect(Collectors.toList());

System.out.println(result); // [1, 4, 7, 10]

这种方法没有我之前回答中的缺点(下面会陈述,出于历史原因,我决定将其保留)。


另一种方法是使用Stream.iterate(),如下所示:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);

List<Integer> result = Stream.iterate(list, l -> l.subList(skip, l.size()))
    .limit(limit)
    .map(l -> l.get(0))
    .collect(Collectors.toList());

System.out.println(result); // [1, 4, 7, 10]

这个想法是创建一个子列表的流,每一个子列表跳过前一个子列表的前N个元素(在例子中N=3)。

我们必须限制迭代次数,以便不尝试获取边界超出范围的子列表。

然后,我们将子列表映射到它们的第一个元素并收集结果。保留每个子列表的第一个元素的作用如预期一样,因为每个子列表的开始索引相对于源列表向右移动了N个元素。

这也是有效的,因为List.sublist()方法返回原始列表的视图,这意味着它不会为每次迭代创建一个新的List


编辑:过了一段时间,我了解到更好的方法是采用@sprinter的其中一种方法,因为subList()会创建一个原始列表的包装器。这意味着流的第二个列表将是第一个列表的包装器,第三个列表将是第二个列表的包装器(已经是一个包装器!),依此类推...

虽然这对于小型到中型列表可能有效,但应注意,对于非常大的源列表,可能会创建许多包装器。这可能会变得昂贵,甚至会生成StackOverflowError


非常有趣的解决方案。谢谢! - Michel Krämer
3
Stream.iterate(0, i -> i + skip).limit(limit).map(list::get).collect(Collectors.toList()); 这个方法不就可以解决你的包装问题了吗?这也是对 @sprinter 答案的优化,因为它不需要初始化和过滤所有跳过的值。 - Emiel
value iterate is not a member of scala.collection.immutable.Stream[String] - techkuz
1
@techkuz 这个回答是关于Java的,我不知道Scala。我正在使用java.util.stream.Stream.iterate,这是一个静态方法。 - fps

11
如果您愿意使用第三方库,那么jOOλ提供了有用的功能,如zipWithIndex()

每隔一个元素

System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
   .zipWithIndex()             // This produces a Tuple2(yourvalue, index)
   .filter(t -> t.v2 % 2 == 0) // Filter by the index
   .map(t -> t.v1)             // Remove the index again
   .toList()
);

[1, 3, 5, 7, 9]

每三个元素
System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
   .zipWithIndex()
   .filter(t -> t.v2 % 3 == 0)
   .map(t -> t.v1)
   .toList()
);

[1, 4, 7, 10]

免责声明:我为 jOOλ 背后的公司工作。

4

使用Guava:

Streams
    .mapWithIndex(stream, SimpleImmutableEntry::new)
    .filter(entry -> entry.getValue() % 3 == 0)
    .map(Entry::getKey)
    .collect(Collectors.toList());

2
您还可以使用带有自定义函数的flatMap来跳过项目:
private <T> Function<T, Stream<T>> everyNth(int n) {
  return new Function<T, Stream<T>>() {
    int i = 0;

    @Override
    public Stream<T> apply(T t) {
      if (i++ % n == 0) {
        return Stream.of(t);
      }
      return Stream.empty();
    }
  };
}

@Test
public void everyNth() {
  assertEquals(
    Arrays.asList(1, 4, 7, 10),
    IntStream.rangeClosed(1, 10).boxed()
      .flatMap(everyNth(3))
      .collect(Collectors.toList())
  );
}

它有与非索引流一起使用的优点。但是在并行流中使用它不是一个好主意(也许应该切换到原子整数 i)。


2

试一下这个。

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    int[] n = {0};
    List<Integer> result = list.stream()
        .filter(x -> n[0]++ % 3 == 0)
        .collect(Collectors.toList());
    System.out.println(result);
    // -> [1, 4, 7, 10]

2
这看起来会在并行实现中出问题。其他人通过使用AtomicIntegergetAndIncrement()方法解决了这个问题。 - MikaelF

1
这是 abacus-common 的代码。
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .filter(MutableInt.of(0), (e, idx) -> idx.getAndDecrement() % 2 == 0)
        .println();
// output: 1, 3, 5, 7, 9

如果需要索引:

或者如果需要索引:

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
      .indexed().filter(i -> i.index() % 2 == 0).println();
// output: [0]=1, [2]=3, [4]=5, [6]=7, [8]=9

声明:我是abacus-common的开发者。

0

你能试试这个吗?

employees.stream()
.filter(e -> e.getName().charAt(0) == 's')
.skip(n-1)
.findFirst()

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接