Java Tips | コレクション:分割処理

Java Java
スポンサーリンク

分割処理は「大きな塊を、ちょうどいいサイズのかたまりに刻む」技

分割処理は、ざっくり言うと
「大きな List を、一定サイズごとの小さな List に分ける」テクニックです。

一度に 1 万件のレコードを外部 API に投げるとタイムアウトするから、100 件ずつに分けたい。
DB にバルク登録するとき、1000 件ずつに区切って insert したい。
重い処理を並列化するとき、元の一覧を複数のチャンク(かたまり)に分けたい。

こういう「大きすぎる一覧を、そのまま扱うと危ない・重い」場面で、
分割処理ユーティリティがあると、業務コードが一気に安定します。


基本形:List を「固定サイズのチャンク」に分割する

シンプルな実装イメージをつかむ

まずは、List<Integer> を「3 件ずつ」に分割する例から見てみます。

import java.util.ArrayList;
import java.util.List;

public final class Lists {

    private Lists() {}

    public static <T> List<List<T>> partition(List<T> source, int size) {
        if (source == null || source.isEmpty()) {
            return List.of();
        }
        if (size <= 0) {
            throw new IllegalArgumentException("size must be > 0");
        }

        List<List<T>> result = new ArrayList<>();
        int total = source.size();
        for (int i = 0; i < total; i += size) {
            int toIndex = Math.min(i + size, total);
            result.add(source.subList(i, toIndex));
        }
        return result;
    }
}
Java

使い方はこうです。

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<List<Integer>> chunks = Lists.partition(numbers, 3);

// chunks は [[1,2,3], [4,5,6], [7]] というイメージ
Java

ここでの重要ポイントは三つです。

一つ目は、「size が 3 なら、0〜2、3〜5、6〜…というようにインデックスを刻んでいる」ことです。
for (int i = 0; i < total; i += size) というループが、その“刻み”を表しています。

二つ目は、「最後のチャンクは size 未満でもよい」としていることです。
7 件を 3 件ずつに分けると、3・3・1 になりますが、それで正しい、というルールです。

三つ目は、「size <= 0 を明示的に弾いている」ことです。
分割サイズが 0 やマイナスだと無限ループや例外の原因になるので、
ユーティリティ側で早めにチェックしておくのが実務的です。


分割処理ユーティリティを業務でどう使うか

例:外部 API に 100 件ずつ送る

よくあるパターンが、「外部 API の制限で、一度に送れる件数が決まっている」ケースです。

public class SendSample {

    public static void main(String[] args) {
        List<String> ids = /* 1 万件くらいの ID 一覧 */ List.of();

        for (List<String> chunk : Lists.partition(ids, 100)) {
            sendToExternalApi(chunk);
        }
    }

    private static void sendToExternalApi(List<String> idsChunk) {
        // ここで外部 API 呼び出し
    }
}
Java

ここでの重要ポイントは、
「呼び出し側のコードが“100 件ずつ送る”という意図だけを素直に書けている」ことです。

Lists.partition(ids, 100) という一行が、
「大きな一覧を 100 件単位に刻む」という業務ルールを表現しています。
for 文の中では「チャンク単位で処理する」ことだけに集中できます。


分割処理の“副作用”に注意するポイント

subList の「ビュー」であることを理解する

先ほどの実装では source.subList(i, toIndex) を使いました。
これは「元の List の一部を切り出した“ビュー”」であり、
元の List と要素を共有しています。

つまり、元の List をあとから変更すると、
subList 側にも影響が出る可能性があります(実装による)。

安全側に振るなら、「コピーを作る」実装にしてもよいです。

public static <T> List<List<T>> partitionCopy(List<T> source, int size) {
    if (source == null || source.isEmpty()) {
        return List.of();
    }
    if (size <= 0) {
        throw new IllegalArgumentException("size must be > 0");
    }

    List<List<T>> result = new ArrayList<>();
    int total = source.size();
    for (int i = 0; i < total; i += size) {
        int toIndex = Math.min(i + size, total);
        result.add(new ArrayList<>(source.subList(i, toIndex)));
    }
    return result;
}
Java

ここでの重要ポイントは、
「ビューでよい場面(読み取り専用)と、コピーが必要な場面(後で変更する可能性がある)を意識して使い分ける」ことです。

実務では、「基本は読み取り専用にしておく」「変更したいならコピー版を使う」という方針を決めておくと安全です。


条件で「二分割」する partitioningBy との違い

「サイズで分ける」と「条件で分ける」は別物

Java の Stream には Collectors.partitioningBy というメソッドもあります。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

List<Integer> values = List.of(1, 2, 3, 4, 5);

Map<Boolean, List<Integer>> partitioned =
        values.stream()
              .collect(Collectors.partitioningBy(v -> v % 2 == 0));

// true  → 偶数の List
// false → 奇数の List
Java

これは「条件に合うもの/合わないもの」の二分割です。
一方、今回の「分割処理ユーティリティ」は「件数で刻む」分割です。

どちらも「partition」という言葉を使いますが、
意味が違うので、頭の中で整理しておくと混乱しません。

条件で二分割したいときは partitioningBy
大きな List を「N 件ずつ」に刻みたいときは、自前の Lists.partition

この住み分けを意識しておくと、
「今やりたいのはどっちの partition だ?」と迷わずに済みます。


並列処理・バッチ処理との組み合わせ

例:チャンクごとに並列で処理する

分割処理は、「並列処理」と組み合わせるとさらに威力を発揮します。

import java.util.List;

public class ParallelChunkSample {

    public static void main(String[] args) {
        List<String> ids = /* 大量の ID */ List.of();

        Lists.partition(ids, 100)
             .parallelStream()
             .forEach(ParallelChunkSample::processChunk);
    }

    private static void processChunk(List<String> chunk) {
        // チャンク単位の重い処理
    }
}
Java

ここでの重要ポイントは、
「分割処理が“並列化の単位”を決める役割を担っている」ことです。

1 件ずつ並列にするのではなく、
「100 件単位で並列にする」ことで、
外部 API や DB の負荷をコントロールしやすくなります。

分割サイズは、そのまま「負荷の粒度」を意味します。
ここをユーティリティ+定数で管理しておくと、
チューニングもしやすくなります。


まとめ:分割処理ユーティリティで身につけてほしい感覚

分割処理は、
単に「List を小分けにするテクニック」ではなく、
「大きな処理を、安全に・制御可能な単位に刻むための設計」です。

Lists.partition(list, size) のようなユーティリティで、「N 件ずつ」というルールを一箇所に閉じ込める。
最後のチャンクは size 未満でもよい、という仕様を明確にしておく。
subList がビューであることを理解し、「読み取り専用か、コピーが必要か」を意識して使い分ける。
条件で二分割する partitioningBy と、「件数で刻む partition」を頭の中で区別する。
バッチ処理・外部 API 呼び出し・並列処理の「単位」を決める道具として、分割処理を位置づける。

あなたのコードのどこかに、
「for 文でインデックスをいじりながら、手作業で 100 件ずつに分けている」箇所があれば、
それを一度「分割処理ユーティリティ」に置き換えられないか眺めてみてください。

その小さな一歩が、
「大きな処理を、無理なく“ちょうどいいサイズ”に刻めるエンジニア」への、
確かなステップになります。

タイトルとURLをコピーしました