Java - Stream - 每N个元素收集一次

21

我正在尝试学习Java - Stream。我能够进行简单的迭代/过滤/映射/集合等操作。

当我试图像这个例子中所示那样收集每3个元素并打印时,遇到了困难。

    List<String> list = Arrays.asList("a","b","c","d","e","f","g","h","i","j");

    int count=0;
    String append="";
    for(String l: list){
        if(count>2){
            System.out.println(append);
            System.out.println("-------------------");
            append="";
            count=0;
        }
        append = append + l;
        count++;
    }
    System.out.println(append);

输出:

abc
-------------------
def
-------------------
ghi
-------------------
j

我一点头绪都没有,该如何使用stream完成这个任务。是应该实现自己的collector来实现吗?


1
流是做这件事的错误方法。当使用流时,您应该对每个元素执行无状态操作,以便可以并行执行它们和/或按任何顺序执行它们。然而,元素的计数器一个状态。您最好使用常规的for循环。 - Timothy Truckle
1
此外,您还可以使用Guava中的Iterables.partition - ZhekaKozlov
3
https://dev59.com/u14c5IYBdhLWcg3wMnz1#28211518 - Holger
6个回答

27

你实际上可以使用IntStream来模拟列表的分页。

List<String> list = Arrays.asList("a","b","c","d","e","f","g","h","i","j");

int pageSize = 3;

IntStream.range(0, (list.size() + pageSize - 1) / pageSize)
        .mapToObj(i -> list.subList(i * pageSize, Math.min(pageSize * (i + 1), list.size())))
        .forEach(System.out::println);

输出结果为:

[a, b, c]
[d, e, f]
[g, h, i]
[j]

如果你想生成字符串,可以直接使用String.join,因为你正在处理一个List<String>

.mapToObj(i -> String.join("", list.subList(i * pageSize, Math.min(pageSize * (i + 1), list.size()))))

很酷...我应该知道子列表的存在。 - KitKarson
1
请注意:只有当输入是基于数组的 List(例如 ArrayListArrays.asList())时,这才是一个好答案。对于 LinkedList 和其他 Collection 对象,其中 get(int index) 不是 O(1) 的情况下,性能会受到影响。--- 如果输入是流(Stream),则也无法使用,这是问题“如何从流中收集每 N 个元素”的一种解释。--- 尽管如此,还是要给出有用的答案加上一分。 - Andreas
@Andreas 确实,如果源是一个流,则我在此处再次链接Holger提供的解决方案 - Alexis C.

12

如果你的项目中有Guava,你可以使用Iterables.partition方法:

import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
...

Stream<List<String>> stream = Streams.stream(Iterables.partition(list, 3));

我不使用它。 我可以。我会尝试。 - KitKarson
1
如果潜在的额外jar是一个选项并且输入是Iterable(最常见的情况),那么这是一个好的简单解决方案,但如果输入是流,则无法使用,这是对问题“如何从流中收集每N个元素”的一种解释。尽管如此,这仍然是一个有用的答案,+1。 - Andreas

11

你可以创建自己的 Collector。最简单的方法是调用Collector.of()

由于你的使用场景要求按顺序处理值,因此这里提供了一个实现,它不支持并行处理。

public static Collector<String, List<List<String>>, List<List<String>>> blockCollector(int blockSize) {
    return Collector.of(
            ArrayList<List<String>>::new,
            (list, value) -> {
                List<String> block = (list.isEmpty() ? null : list.get(list.size() - 1));
                if (block == null || block.size() == blockSize)
                    list.add(block = new ArrayList<>(blockSize));
                block.add(value);
            },
            (r1, r2) -> { throw new UnsupportedOperationException("Parallel processing not supported"); }
    );
}

测试

List<String> input = Arrays.asList("a","b","c","d","e","f","g","h","i","j");
List<List<String>> output = input.stream().collect(blockCollector(3));
output.forEach(System.out::println);

输出

[a, b, c]
[d, e, f]
[g, h, i]
[j]

谢谢。我也会尝试这种方法。 - KitKarson

7
我是这样解决的:
    List<String> list = Arrays.asList("a","b","c","d","e","f","g","h","i","j");
    int groupBy = 3;

    AtomicInteger index = new AtomicInteger(0);         
    Map<Integer, List<String>> groups = list.stream()
        .collect(Collectors.groupingBy(cdm -> index.getAndIncrement()/groupBy));

    System.out.println(groups);

它准备了一张地图,其中行号是键,行上的字符串是键内的内容。

非常有趣 - KitKarson
聪明,但它有副作用。 - Dexter Legaspi

3

我认为最好的方法是使用StreamEx库,这是一个由Tagir Valeev创建的惊人库。解决方案只需要一行代码))

StreamEx.ofSubLists(list, 3).toList();

1
最明显的解决方案:

IntStream.range(0, list.size() / N)
         .map(i -> i * charactersAmount)
         .mapToObj(i -> list.subList(i, i + charactersAmount)
         .collect(Collectors.toWhateverYouWant());

第一行 - 你将获得一个从0到结果行数的整数流。从您的示例中,list.size() / N 等于4,因此流将是0-1-2-3。
第二行 - 此流将映射到按 charactersAmount 缩放的流中,对于您的情况,它是3 - 0-3-6-9。
第三行将从初始列表中剪切子列表。
最后一行只将生成的流视为集合。

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