Vor einiger Zeit musste ich größere Listen von Daten abarbeiten und die Ergebnisse zwischenspeichern. An mehreren Stellen im Code hatte ich dann Batch-Schleifen, die eine Liste in Batches á N Stück aufteilten und diese verarbeiteten. Die Gelegenheit, das etwas generischer zu Formulieren.

Die Grundstruktur einer batchweisen Verarbeitung einer Liste von Elementen sieht wie folgt aus:

List data = ...
final int batchSize = 50;
int fromOffset = 0;
do {
    int toOffset = Math.min(data.size(), fromOffset + batchSize);
    List batch = data.subList(ftomOffset, toOffset);
 
    // verarbeite Batch
 
    fromOffset = toOffset;
} while (fromOffset < data.size());

Formuliert man das ganze mit einem Context-Objekt, in dem Informationen zum aktuellen Batch stehen, sowie ein Interface für Konsumenten, erhält man eine generische Batch-Abarbeitung:

@Log4j
public class Batch {
    public interface Consumer<T> {
        void consume(List<T> batch, Context context);
    }
 
    @Getter
    @AllArgsConstructor
    @Builder
    @EqualsAndHashCode
    public static class Context {
        private int currentBlockSize;
        private int blockSize;
        private int currentOffset;
    }
 
    public static <T> void process(List<T> data, int blockSize, Consumer<T> consumer) {
        final int maxOffset = data.size();
        int fromOffset = 0;
        do {
            int currentBlockSize = Math.min(blockSize, maxOffset - fromOffset);
            int toOffset = fromOffset + currentBlockSize;
            log.info("Verarbeite Block {} bis {} von {}", fromOffset, toOffset, maxOffset);
            consumer.consume(data.subList(fromOffset, toOffset),
                    Context.builder()
                            .currentOffset(fromOffset)
                            .currentBlockSize(currentBlockSize)
                            .blockSize(blockSize)
                            .build());
            fromOffset = toOffset;
        } while (fromOffset < maxOffset);
    }
}

Die Nutzung ist relativ einfach:

@Test
public void process() {
    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    List<Integer> receivedBatchEntries = new ArrayList<>();
    List<Batch.Context> contexts = new ArrayList<>();
    Batch.process(ints, 3, (batch, context) -> {
        receivedBatchEntries.addAll(batch);
        contexts.add(context);
    });
 
    assertThat(receivedBatchEntries, contains(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    assertThat(contexts, contains(
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(0).build(),
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(3).build(),
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(6).build(),
            // Letzter lauf hat nur ein Element
            Batch.Context.builder().blockSize(3).currentBlockSize(1).currentOffset(9).build()
    ));
}

Das Ganze kann man dann auch auf asynchrone und parallele Abarbeitungen erweitern.