收集器将流分割为给定大小的块

时间:2018-06-15 04:57:14

标签: java java-stream collectors

我手头有一个问题,我试图用一些东西来解决,我很确定我不应该这样做但是没有看到替代方案。我给出了一个字符串列表,并应将其拆分为给定大小的块。然后必须将结果传递给某些方法以进行进一步处理。由于列表可能很大,因此处理应该异步完成。

我的方法是创建一个自定义收集器,它接受字符串流并将其转换为流< List< Long>>:

final Stream<List<Long>> chunks = list
                        .stream()
                        .parallel()
                        .collect(MyCollector.toChunks(CHUNK_SIZE)) 
                        .flatMap(p -> doStuff(p))
                        .collect(MyCollector.toChunks(CHUNK_SIZE))
                        .map(...)
                        ...

收集者的代码:

public final class MyCollector<T, A extends List<List<T>>, R extends Stream<List<T>>> implements Collector<T, A, R> {
private final AtomicInteger index = new AtomicInteger(0);
private final AtomicInteger current = new AtomicInteger(-1);
private final int chunkSize;

private MyCollector(final int chunkSize){
    this.chunkSize = chunkSize;
}

@Override
public Supplier<A> supplier() {
    return () -> (A)new ArrayList<List<T>>();
}

@Override
public BiConsumer<A, T> accumulator() {
    return (A candidate, T acc) -> {
        if (index.getAndIncrement() % chunkSize == 0){
            candidate.add(new ArrayList<>(chunkSize));
            current.incrementAndGet();
        }
        candidate.get(current.get()).add(acc);
    };
}

@Override
public BinaryOperator<A> combiner() {
    return (a1, a2) -> {
        a1.addAll(a2);
        return a1;
    };
}
@Override
public Function<A, R> finisher() {
    return (a) -> (R)a.stream();
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.UNORDERED));
}

public static <T> MyCollector<T, List<List<T>>, Stream<List<T>>> toChunks(final int chunkSize){
    return new MyCollector<>(chunkSize);
}

}

这似乎在大多数情况下有效但我有时会得到一个NPE。我确定累加器中的线程不是安全的,因为在向主List添加新列表时可能会有两个线程干扰。我不介意一个有太多或太少元素的块。

我已尝试过此而不是当前的供应商功能:

 return () -> (A)new ArrayList<List<T>>(){{add(new ArrayList<T>());}};

确保始终存在列表。这根本不起作用,导致空列表。

的问题:

  • 我非常确定自定义Spliterator是一个很好的解决方案。但是,它不适用于同步方案。另外,我确定Spliterator被调用了吗?
    • 我知道我根本不应该有状态,但不知道如何改变它。

问题:

  • 这种做法是完全错误还是以某种方式解决?
  • 如果我使用Spliterator - 我能确定它被调用还是由底层实现决定?
  • 我很确定供应商和终结者的(A)和(R)的演员表没有必要,但IntelliJ抱怨。我有什么遗失的东西吗?

编辑

  • 我已经在客户端代码中添加了更多内容,因为IntStream.range的建议在链接时无法正常工作。
  • 我意识到我可以按照评论中的建议做不同的事情,但它也有点关于风格并知道它是否可能。
  • 我有CONCURRENT特性,因为我认为Stream API会回退到同步处理。如前所述,该解决方案不是线程安全的。

非常感谢任何帮助。

最佳, d

3 个答案:

答案 0 :(得分:1)

这是一种方法,本着在一个表达式中完成所有操作的精神,这非常令人满意:首先将每个字符串与其列表中的索引相关联,然后在收集器中使用它来选择字符串列表以放置每个字符串成。然后将这些列表并行转换为转换器方法。

  final Stream<List<Long>> longListStream = IntStream.range(0, strings.size())
    .parallel()
    .mapToObj(i -> new AbstractMap.SimpleEntry<>(i, strings.get(i)))
    .collect(
        () -> IntStream.range(0, strings.size() / CHUNK_SIZE + 1)
            .mapToObj(i -> new LinkedList<String>())
            .collect(Collectors.toList()),
        (stringListList, entry) -> {
            stringListList.get(entry.getKey() % CHUNK_SIZE).add(entry.getValue());
        },
        (stringListList1, stringListList2) -> { })
    .parallelStream()
    .map(this::doStuffWithStringsAndGetLongsBack);

答案 1 :(得分:1)

我认为您不需要编写自定义Collector,而是可以使用stream API中提供的现有功能来完成此操作。这是一种做法。

final int pageSize = 3;
List<Long> chunks  = IntStream.range(0, (numbers.size() + pageSize - 1) / pageSize)
        .peek(System.out::println)
        .mapToObj(i -> numbers.subList(i * pageSize, Math.min(pageSize * (i + 1), numbers.size())))
        .flatMap(l -> doStuff(l).stream())
        .collect(Collectors.toList());

此外,我没有看到将Stream<List<Long>> chunks作为最终结果的任何意义,而是List<Long>

答案 2 :(得分:1)

我还不能发表评论,但我想将以下链接发布到一个非常相似的问题上(尽管不是重复的,据我所知):Java 8 Stream with batch processing

您可能也对GitHub上的以下问题感兴趣:https://github.com/jOOQ/jOOL/issues/296

现在,您对CONCURRENT特征的使用是错误的 - 该文档说明以下关于Collector.Characteristics.CONCURRENT

  

表示此收集器是并发,这意味着结果容器可以支持从多个线程同时使用相同结果容器调用累加器函数。

这意味着supplier只会被调用一次,combiner实际上永远不会被调用(参见ReferencePipeline.collect()方法的来源)。这就是你有时会得到NPE的原因。

因此,我建议您提供简化版本:

public static <T> Collector<T, List<List<T>>, Stream<List<T>>> chunked(int chunkSize) {
  return Collector.of(
          ArrayList::new,
          (outerList, item) -> {
            if (outerList.isEmpty() || last(outerList).size() >= chunkSize) {
              outerList.add(new ArrayList<>(chunkSize));
            }
            last(outerList).add(item);
          },
          (a, b) -> {
            a.addAll(b);
            return a;
          },
          List::stream,
          Collector.Characteristics.UNORDERED
  );
}

private static <T> T last(List<T> list) {
  return list.get(list.size() - 1);
}

或者,您可以使用正确的同步编写真正的并发Collector,但如果您不介意有多个列表的大小小于chunkSize(这是您可以获得的效果与我上面提到的非并发Collector一样,我不会打扰。