アイテムのリストを含む大きなファイルがあります。
アイテムのバッチを作成し、このバッチでHTTPリクエストを作成します(すべてのアイテムは、HTTPリクエストのパラメーターとして必要です)。 for
ループを使用すると非常に簡単に実行できますが、Java 8の恋人として、Java 8のStreamフレームワークでこれを書いてみてください(そして、遅延処理)。
例:
List<String> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < data.size(); i++) {
batch.add(data.get(i));
if (batch.size() == BATCH_SIZE) process(batch);
}
if (batch.size() > 0) process(batch);
lazyFileStream.group(500).map(processBatch).collect(toList())
の長い行で何かをしたい
これを行う最良の方法は何でしょうか?
注意! このソリューションは、forEachを実行する前にファイル全体を読み取ります。
jOOλ でこれを行うことができます。これは、シングルスレッドの順次ストリームのユースケース用にJava 8ストリームを拡張するライブラリです。
Seq.seq(lazyFileStream) // Seq<String>
.zipWithIndex() // Seq<Tuple2<String, Long>>
.groupBy(Tuple -> Tuple.v2 / 500) // Map<Long, List<String>>
.forEach((index, batch) -> {
process(batch);
});
舞台裏では、zipWithIndex()
は次のとおりです。
static <T> Seq<Tuple2<T, Long>> zipWithIndex(Stream<T> stream) {
final Iterator<T> it = stream.iterator();
class ZipWithIndex implements Iterator<Tuple2<T, Long>> {
long index;
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public Tuple2<T, Long> next() {
return Tuple(it.next(), index++);
}
}
return seq(new ZipWithIndex());
}
... groupBy()
はAPIの利便性です:
default <K> Map<K, List<T>> groupBy(Function<? super T, ? extends K> classifier) {
return collect(Collectors.groupingBy(classifier));
}
(免責事項:私はjOOλの背後にある会社で働いています)
完全を期すために、ここに Guava ソリューションがあります。
Iterators.partition(stream.iterator(), batchSize).forEachRemaining(this::process);
質問ではコレクションが利用可能であるため、ストリームは必要なく、次のように記述できます。
Iterables.partition(data, batchSize).forEach(this::process);
Pure Java-8の実装も可能です。
int BATCH = 500;
IntStream.range(0, (data.size()+BATCH-1)/BATCH)
.mapToObj(i -> data.subList(i*BATCH, Math.min(data.size(), (i+1)*BATCH)))
.forEach(batch -> process(batch));
JOOlとは異なり、並行して適切に動作することに注意してください(data
がランダムアクセスリストである場合)。
Pure Java 8ソリューション:
これをエレガントに行うカスタムコレクターを作成し、batch size
とConsumer
を取り込んで各バッチを処理できます。
import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.List;
import Java.util.Set;
import Java.util.function.*;
import Java.util.stream.Collector;
import static Java.util.Objects.requireNonNull;
/**
* Collects elements in the stream and calls the supplied batch processor
* after the configured batch size is reached.
*
* In case of a parallel stream, the batch processor may be called with
* elements less than the batch size.
*
* The elements are not kept in memory, and the final result will be an
* empty list.
*
* @param <T> Type of the elements being collected
*/
class BatchCollector<T> implements Collector<T, List<T>, List<T>> {
private final int batchSize;
private final Consumer<List<T>> batchProcessor;
/**
* Constructs the batch collector
*
* @param batchSize the batch size after which the batchProcessor should be called
* @param batchProcessor the batch processor which accepts batches of records to process
*/
BatchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
batchProcessor = requireNonNull(batchProcessor);
this.batchSize = batchSize;
this.batchProcessor = batchProcessor;
}
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
public BiConsumer<List<T>, T> accumulator() {
return (ts, t) -> {
ts.add(t);
if (ts.size() >= batchSize) {
batchProcessor.accept(ts);
ts.clear();
}
};
}
public BinaryOperator<List<T>> combiner() {
return (ts, ots) -> {
// process each parallel list without checking for batch size
// avoids adding all elements of one to another
// can be modified if a strict batching mode is required
batchProcessor.accept(ts);
batchProcessor.accept(ots);
return Collections.emptyList();
};
}
public Function<List<T>, List<T>> finisher() {
return ts -> {
batchProcessor.accept(ts);
return Collections.emptyList();
};
}
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
オプションで、ヘルパーユーティリティクラスを作成します。
import Java.util.List;
import Java.util.function.Consumer;
import Java.util.stream.Collector;
public class StreamUtils {
/**
* Creates a new batch collector
* @param batchSize the batch size after which the batchProcessor should be called
* @param batchProcessor the batch processor which accepts batches of records to process
* @param <T> the type of elements being processed
* @return a batch collector instance
*/
public static <T> Collector<T, List<T>, List<T>> batchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
return new BatchCollector<T>(batchSize, batchProcessor);
}
}
使用例:
List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> output = new ArrayList<>();
int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> output.addAll(xs);
input.stream()
.collect(StreamUtils.batchCollector(batchSize, batchProcessor));
誰かが見てみたい場合は、GitHubにもコードを投稿しました:
このようなシナリオ用のカスタムスプリッターを作成しました。入力ストリームから指定されたサイズのリストを埋めます。このアプローチの利点は、遅延処理を実行し、他のストリーム関数と連携することです。
public static <T> Stream<List<T>> batches(Stream<T> stream, int batchSize) {
return batchSize <= 0
? Stream.of(stream.collect(Collectors.toList()))
: StreamSupport.stream(new BatchSpliterator<>(stream.spliterator(), batchSize), stream.isParallel());
}
private static class BatchSpliterator<E> implements Spliterator<List<E>> {
private final Spliterator<E> base;
private final int batchSize;
public BatchSpliterator(Spliterator<E> base, int batchSize) {
this.base = base;
this.batchSize = batchSize;
}
@Override
public boolean tryAdvance(Consumer<? super List<E>> action) {
final List<E> batch = new ArrayList<>(batchSize);
for (int i=0; i < batchSize && base.tryAdvance(batch::add); i++)
;
if (batch.isEmpty())
return false;
action.accept(batch);
return true;
}
@Override
public Spliterator<List<E>> trySplit() {
if (base.estimateSize() <= batchSize)
return null;
final Spliterator<E> splitBase = this.base.trySplit();
return splitBase == null ? null
: new BatchSpliterator<>(splitBase, batchSize);
}
@Override
public long estimateSize() {
final double baseSize = base.estimateSize();
return baseSize == 0 ? 0
: (long) Math.ceil(baseSize / (double) batchSize);
}
@Override
public int characteristics() {
return base.characteristics();
}
}
解決すべき同様の問題がありました。システムメモリよりも大きいストリーム(データベース内のすべてのオブジェクトを反復処理)を取得し、可能な限り順序をランダム化することを望んでいました。10,000個のアイテムをバッファリングしてランダム化することは問題ないと考えました。
ターゲットは、ストリームを取り込む関数でした。
ここで提案されているソリューションには、さまざまなオプションがあるようです。
私たちの本能はもともとカスタムコレクターを使用することでしたが、これはストリーミングを中断することを意味していました。上記のカスタムコレクターソリューションは非常に優れており、ほぼ使用しています。
Stream
sがIterator
を与えることができるという事実を使用することでごまかす解決策を示します。これはエスケープハッチとして使用でき、ストリームがサポートしない追加の操作を実行できます。 Iterator
は、Java 8 StreamSupport
sorceryの別のビットを使用してストリームに変換されます。
/**
* An iterator which returns batches of items taken from another iterator
*/
public class BatchingIterator<T> implements Iterator<List<T>> {
/**
* Given a stream, convert it to a stream of batches no greater than the
* batchSize.
* @param originalStream to convert
* @param batchSize maximum size of a batch
* @param <T> type of items in the stream
* @return a stream of batches taken sequentially from the original stream
*/
public static <T> Stream<List<T>> batchedStreamOf(Stream<T> originalStream, int batchSize) {
return asStream(new BatchingIterator<>(originalStream.iterator(), batchSize));
}
private static <T> Stream<T> asStream(Iterator<T> iterator) {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator,ORDERED),
false);
}
private int batchSize;
private List<T> currentBatch;
private Iterator<T> sourceIterator;
public BatchingIterator(Iterator<T> sourceIterator, int batchSize) {
this.batchSize = batchSize;
this.sourceIterator = sourceIterator;
}
@Override
public boolean hasNext() {
prepareNextBatch();
return currentBatch!=null && !currentBatch.isEmpty();
}
@Override
public List<T> next() {
return currentBatch;
}
private void prepareNextBatch() {
currentBatch = new ArrayList<>(batchSize);
while (sourceIterator.hasNext() && currentBatch.size() < batchSize) {
currentBatch.add(sourceIterator.next());
}
}
}
これを使用する簡単な例は次のようになります。
@Test
public void getsBatches() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
.forEach(System.out::println);
}
上記のプリント
[A, B, C]
[D, E, F]
ユースケースでは、バッチをシャッフルし、ストリームとして保持することを望んでいました。次のようになりました。
@Test
public void howScramblingCouldBeDone() {
BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
// the lambda in the map expression sucks a bit because Collections.shuffle acts on the list, rather than returning a shuffled one
.map(list -> {
Collections.shuffle(list); return list; })
.flatMap(List::stream)
.forEach(System.out::println);
}
これは次のようなものを出力します(ランダム化されているため、毎回異なります)
A
C
B
E
D
F
ここでの秘密のソースは、常にストリームがあるため、バッチのストリームを操作するか、各バッチに対して何かをしてからflatMap
をストリームに戻すことができるということです。さらに良いことに、上記のすべては、最後のforEach
またはcollect
またはその他の終了式PULLとしてのみ実行されます。
iterator
は、ストリームに対する特別なタイプの終了操作であり、ストリーム全体が実行されてメモリに入ることはありません! Java 8人の素晴らしいデザインに感謝します!
RxJava を使用することもできます。
Observable.from(data).buffer(BATCH_SIZE).forEach((batch) -> process(batch));
または
Observable.from(lazyFileStream).buffer(500).map((batch) -> process(batch)).toList();
または
Observable.from(lazyFileStream).buffer(500).map(MyClass::process).toList();
cyclops-react もご覧ください。私はこのライブラリの作成者です。 jOOλインターフェース(および拡張JDK 8ストリーム)を実装しますが、JDK 8パラレルストリームとは異なり、非同期操作(非同期I/O呼び出しのブロックなど)に重点を置いています。 JDK Parallel Streamsは、対照的に、CPUバウンド操作のデータ並列性に焦点を当てています。 Futureに基づいたタスクの集合体を内部で管理することで機能しますが、エンドユーザーに標準の拡張Stream APIを提供します。
このサンプルコードは、開始に役立つ場合があります
LazyFutureStream.parallelCommonBuilder()
.react(data)
.grouped(BATCH_SIZE)
.map(this::process)
.run();
ここでのバッチ処理に関するチュートリアル があります
独自のスレッドプール(おそらくI/Oのブロックに適している)を使用するには、次のコマンドで処理を開始できます。
LazyReact reactor = new LazyReact(40);
reactor.react(data)
.grouped(BATCH_SIZE)
.map(this::process)
.run();
並列ストリームでも機能するPure Java 8の例。
使い方:
Stream<Integer> integerStream = IntStream.range(0, 45).parallel().boxed();
CsStreamUtil.processInBatch(integerStream, 10, batch -> System.out.println("Batch: " + batch));
メソッドの宣言と実装:
public static <ElementType> void processInBatch(Stream<ElementType> stream, int batchSize, Consumer<Collection<ElementType>> batchProcessor)
{
List<ElementType> newBatch = new ArrayList<>(batchSize);
stream.forEach(element -> {
List<ElementType> fullBatch;
synchronized (newBatch)
{
if (newBatch.size() < batchSize)
{
newBatch.add(element);
return;
}
else
{
fullBatch = new ArrayList<>(newBatch);
newBatch.clear();
newBatch.add(element);
}
}
batchProcessor.accept(fullBatch);
});
if (newBatch.size() > 0)
batchProcessor.accept(new ArrayList<>(newBatch));
}
Java 8
とcom.google.common.collect.Lists
を使用すると、次のようなことができます。
public class BatchProcessingUtil {
public static <T,U> List<U> process(List<T> data, int batchSize, Function<List<T>, List<U>> processFunction) {
List<List<T>> batches = Lists.partition(data, batchSize);
return batches.stream()
.map(processFunction) // Send each batch to the process function
.flatMap(Collection::stream) // flat results to gather them in 1 stream
.collect(Collectors.toList());
}
}
ここでT
は入力リストのアイテムのタイプであり、U
は出力リストのアイテムのタイプです
そして、あなたはこれを次のように使うことができます:
List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
userKeys,
10, // Batch Size
partialKeys -> service.getUsers(partialKeys)
);
Spliteratorを使用した簡単な例
// read file into stream, try-with-resources
try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
//skip header
Spliterator<String> split = stream.skip(1).spliterator();
Chunker<String> chunker = new Chunker<String>();
while(true) {
boolean more = split.tryAdvance(chunker::doSomething);
if (!more) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class Chunker<T> {
int ct = 0;
public void doSomething(T line) {
System.out.println(ct++ + " " + line.toString());
if (ct % 100 == 0) {
System.out.println("====================chunk=====================");
}
}
}
Bruceの答えはより包括的なものですが、大量のファイルを処理するために迅速で汚れたものを探していました。
Apache.commonsを使用できます:
ListUtils.partition(ListOfLines, 500).stream()
.map(partition -> processBatch(partition)
.collect(Collectors.toList());
これは遅延評価された純粋なJavaソリューションです。
public static <T> Stream<List<T>> partition(Stream<T> stream, int batchSize){
List<List<T>> currentBatch = new ArrayList<List<T>>(); //just to make it mutable
currentBatch.add(new ArrayList<T>(batchSize));
return Stream.concat(stream
.sequential()
.map(new Function<T, List<T>>(){
public List<T> apply(T t){
currentBatch.get(0).add(t);
return currentBatch.get(0).size() == batchSize ? currentBatch.set(0,new ArrayList<>(batchSize)): null;
}
}), Stream.generate(()->currentBatch.get(0).isEmpty()?null:currentBatch.get(0))
.limit(1)
).filter(Objects::nonNull);
}