我手头有一个问题,我试图用一些东西来解决,我很确定我不应该这样做但是没有看到替代方案。我给出了一个字符串列表,并应将其拆分为给定大小的块。然后必须将结果传递给某些方法以进行进一步处理。由于列表可能很大,因此处理应该异步完成。
我的方法是创建一个自定义收集器,它接受字符串流并将其转换为流< 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>());}};
确保始终存在列表。这根本不起作用,导致空列表。
的问题:
问题:
编辑:
非常感谢任何帮助。
最佳, d
答案 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
一样,我不会打扰。