web-dev-qa-db-ja.com

チャンクでストリームを処理するエレガントな方法はありますか?

私の正確なシナリオは、バッチでデータベースにデータを挿入することです。そのため、DOMオブジェクトを1000個ごとに蓄積し、フラッシュします。

私はアキュムレータにコードを入れて満杯を検出してフラッシュすることで実装しましたが、それは間違っているようです-フラッシュコントロールは呼び出し元から来る必要があります。

ストリームをリストに変換してからsubListを反復的に使用することもできますが、それも不格好です。

N要素ごとにアクションを実行し、ストリームを1回だけ処理しながらストリームを続行するためのきちんとした方法はありますか?

43
Bohemian

エレガンスは見る人の目にあります。 groupingByでステートフル関数を使用してもかまわない場合は、次のようにできます。

AtomicInteger counter = new AtomicInteger();

stream.collect(groupingBy(x->counter.getAndIncrement()/chunkSize))
    .values()
    .forEach(database::flushChunk);

これは、何もする前にストリーム全体をマテリアライズするため、元のソリューションよりもパフォーマンスまたはメモリ使用量の点を獲得しません。

リストの具体化を避けたい場合、ストリームAPIは役に立ちません。ストリームのイテレーターまたはスプリッターを取得し、次のようなことを行う必要があります。

Spliterator<Integer> split = stream.spliterator();
int chunkSize = 1000;

while(true) {
    List<Integer> chunk = new ArrayList<>(size);
    for (int i = 0; i < chunkSize && split.tryAdvance(chunk::add); i++){};
    if (chunk.isEmpty()) break;
    database.flushChunk(chunk);
}
14
Misha

プロジェクトにguavaの依存関係がある場合、これを行うことができます。

StreamSupport.stream(Iterables.partition(simpleList, 1000).spliterator(), false).forEach(...);

https://google.github.io/guava/releases/23.0/api/docs/com/google/common/collect/Lists.html#partition-Java.util.List-int- を参照してください

8
user2814648

ライブラリの使用 StreamEx ソリューションは次のようになります

Stream<Integer> stream = IntStream.iterate(0, i -> i + 1).boxed().limit(15);
AtomicInteger counter = new AtomicInteger(0);
int chunkSize = 4;

StreamEx.of(stream)
        .groupRuns((prev, next) -> counter.incrementAndGet() % chunkSize != 0)
        .forEach(chunk -> System.out.println(chunk));

出力:

[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
[12, 13, 14]

groupRuns 2つの要素を同じグループに含めるかどうかを決定する述語を受け入れます。

グループに属していない最初の要素が見つかるとすぐにグループを作成します。

6
Nazarii Bardiuk

チャンクのストリームList<T>)のアイテムのストリームおよび指定されたチャンクサイズ

  • チャンクインデックス(要素インデックス/チャンクサイズ)によるアイテムのグループ化
  • インデックスによるチャンクの順序付け
  • マップを順序付けられた要素のみに削減する

コード:

public static <T> Stream<List<T>> chunked(Stream<T> stream, int chunkSize) {
    AtomicInteger index = new AtomicInteger(0);

    return stream.collect(Collectors.groupingBy(x -> index.getAndIncrement() / chunkSize))
            .entrySet().stream()
            .sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue);
}

使用例:

Stream<Integer> stream = IntStream.range(0, 100).mapToObj(Integer::valueOf);
Stream<List<Integer>> chunked = chunked(stream, 8);
chunked.forEach(chunk -> System.out.println("Chunk: " + chunk));

出力:

Chunk: [0, 1, 2, 3, 4, 5, 6, 7]
Chunk: [8, 9, 10, 11, 12, 13, 14, 15]
Chunk: [16, 17, 18, 19, 20, 21, 22, 23]
Chunk: [24, 25, 26, 27, 28, 29, 30, 31]
Chunk: [32, 33, 34, 35, 36, 37, 38, 39]
Chunk: [40, 41, 42, 43, 44, 45, 46, 47]
Chunk: [48, 49, 50, 51, 52, 53, 54, 55]
Chunk: [56, 57, 58, 59, 60, 61, 62, 63]
Chunk: [64, 65, 66, 67, 68, 69, 70, 71]
Chunk: [72, 73, 74, 75, 76, 77, 78, 79]
Chunk: [80, 81, 82, 83, 84, 85, 86, 87]
Chunk: [88, 89, 90, 91, 92, 93, 94, 95]
Chunk: [96, 97, 98, 99]
5
Peter Walser

ミーシャが正しく言ったように、エレガンスは見る人の目にあります。個人的には、データベースに挿入するクラスにこのタスクを実行させるのがエレガントな解決策だと思います。 BufferedWriterに似ています。このように、元のデータ構造に依存せず、次々に複数のストリームで使用することもできます。これがあなたが間違っていると思ったアキュムレータにコードがあることによってあなたが何を意味するのか正確にはわかりません。 BufferedWriterのような既存のクラスはこのように機能するので、私はそれが間違っているとは思いません。このようにして、ライターのflush()をいつでも呼び出すことにより、呼び出し元からいくつかのフラッシュコントロールを取得できます。

次のコードのようなもの。

class BufferedDatabaseWriter implements Flushable {
    List<DomObject> buffer = new LinkedList<DomObject>();
    public void write(DomObject o) {
        buffer.add(o);
        if(buffer.length > 1000)
            flush();
    }
    public void flush() {
        //write buffer to database and clear it
    }
}

これで、ストリームは次のように処理されます。

BufferedDatabaseWriter writer = new BufferedDatabaseWriter();
stream.forEach(o -> writer.write(o));
//if you have more streams stream2.forEach(o -> writer.write(o));
writer.flush();

マルチスレッドで作業したい場合は、フラッシュを非同期で実行できます。ストリームからの取得は並行して行うことはできませんが、いずれにしても、ストリームから1000の要素を並行してカウントする方法はないと思います。

また、ライターを拡張してコンストラクターでバッファーサイズを設定できるようにするか、AutoCloseableを実装してリソースなどを使用して試して実行することもできます。あなたがBufferedWriterから持っている素晴らしいもの。

2
findusl

見た目は違います。チャンクを作成するとストリームが減少し、減少すると終了します。すべてのデータを収集せずにストリームの性質を維持してチャンクを処理する必要がある場合は、ここに私のコードがあります(並列ストリームでは機能しません)。

private static <T> BinaryOperator<List<T>> processChunks(Consumer<List<T>> consumer, int chunkSize) {
    return (data, element) -> {
        if (data.size() < chunkSize) {
            data.addAll(element);
            return data;
        } else {
            consumer.accept(data);
            return element; // in fact it's new data list
        }
    };
}

private static <T> Function<T, List<T>> createList(int chunkSize) {
    AtomicInteger limiter = new AtomicInteger(0);
    return element -> {
        limiter.incrementAndGet();
        if (limiter.get() == 1) {
            ArrayList<T> list = new ArrayList<>(chunkSize);
            list.add(element);
            return list;
        } else if (limiter.get() == chunkSize) {
            limiter.set(0);
        }
        return Collections.singletonList(element);
    };
}

と使い方

Consumer<List<Integer>> chunkProcessor = (list) -> list.forEach(System.out::println);

    int chunkSize = 3;

    Stream.generate(StrTokenizer::getInt).limit(13)
            .map(createList(chunkSize))
            .reduce(processChunks(chunkProcessor, chunkSize))
            .ifPresent(chunkProcessor);

static Integer i = 0;

static Integer getInt()
{
    System.out.println("next");
    return i++;
}

印刷されます

次次次次0 1 2次次次3 4 5次次次6 7 8次次次9 10 11 12

背後にあるアイデアは、「パターン」を使用してマップ操作でリストを作成することです

[1 ,]、[2]、[3]、[4 ,] ...

そしてreduceでそれをマージ(+プロセス)します。

[1,2,3]、[4,5,6]、...

最後の「トリミングされた」チャンクを処理することを忘れないでください

.ifPresent(chunkProcessor);
1
Yura

上記の回答のほとんどは、メモリの節約などのストリームの利点を使用していません。イテレータを使用して問題を解決することができます

Stream<List<T>> chunk(Stream<T> stream, int size) {
  Iterator<T> iterator = stream.iterator();
  Iterator<List<T>> listIterator = new Iterator<>() {

    public boolean hasNext() {
      return iterator.hasNext();
    }

    public List<T> next() {
      List<T> result = new ArrayList<>(size);
      for (int i = 0; i < size && iterator.hasNext(); i++) {
        result.add(iterator.next());
      }
      return result;
    }
  };
  return StreamSupport.stream(((Iterable<List<T>>) () -> listIterator).spliterator(), false);
}
0
dmitryvim