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

Java Java
スポンサーリンク

zip処理は「2つの列を“ペアの列”にする」技

zip(ジップ)処理は、
「2つのコレクションを、同じ位置同士でペアにしていく」操作です。

名前のリスト
点数のリスト

この2つを「名前+点数」のペアにしたい——これがまさに zip の出番です。
“2本の線を、同じ長さだけチャックで閉じていく”イメージを持ってください。


まずは素朴な for 文での zip 処理

同じ長さの2つの List をペアにする

一番基本の形からいきます。

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

public class ZipBasic {

    public static void main(String[] args) {
        List<String> names = List.of("山田", "佐藤", "鈴木");
        List<Integer> scores = List.of(80, 90, 75);

        List<String> result = new ArrayList<>();

        int size = Math.min(names.size(), scores.size());
        for (int i = 0; i < size; i++) {
            String name = names.get(i);
            Integer score = scores.get(i);
            result.add(name + " : " + score);
        }

        System.out.println(result); // [山田 : 80, 佐藤 : 90, 鈴木 : 75]
    }
}
Java

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

一つ目は、「Math.min(names.size(), scores.size()) で“短い方の長さ”に合わせている」ことです。
片方が長くても、はみ出した分は無視する——これが zip の基本的な考え方です。

二つ目は、「同じインデックス i で両方の List から要素を取り出し、ペアとして扱っている」ことです。
“位置で結びつける”という感覚をしっかり持ってください。


ペアを表す小さなクラスを用意する

Pair<L, R> という「2つ組」を作る

zip 処理を汎用的に使いたいなら、「ペア」を表すクラスを一つ持っておくと便利です。

public final class Pair<L, R> {
    private final L left;
    private final R right;

    public Pair(L left, R right) {
        this.left = left;
        this.right = right;
    }

    public L getLeft()  { return left; }
    public R getRight() { return right; }
}
Java

これを使って、zip ユーティリティを書きます。

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

public final class Zips {

    private Zips() {}

    public static <L, R> List<Pair<L, R>> zip(List<L> left, List<R> right) {
        if (left == null || right == null) {
            return List.of();
        }
        int size = Math.min(left.size(), right.size());
        if (size == 0) {
            return List.of();
        }
        List<Pair<L, R>> result = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            result.add(new Pair<>(left.get(i), right.get(i)));
        }
        return Collections.unmodifiableList(result);
    }
}
Java

使い方はこうなります。

List<String> names = List.of("山田", "佐藤", "鈴木");
List<Integer> scores = List.of(80, 90, 75);

List<Pair<String, Integer>> zipped = Zips.zip(names, scores);

for (Pair<String, Integer> p : zipped) {
    System.out.println(p.getLeft() + " : " + p.getRight());
}
Java

ここで深掘りしたいポイントは三つです。

一つ目は、「Pair<L, R> という“名前付きの2つ組”を使うことで、コードの意図が読みやすくなる」ことです。
Map.Entry などで代用することもできますが、Pair の方が意味がストレートです。

二つ目は、「null や空 List の扱いをユーティリティ側で決めている」ことです。
呼び出し側は「とりあえず zip したい」とだけ書けばよく、毎回 null チェックをしなくて済みます。

三つ目は、「Collections.unmodifiableList で“結果を不変にして返している”」ことです。
zip の結果は“その時点のスナップショット”として扱うことが多いので、勝手に書き換えられないようにしておくと安全です。


Stream を使った zip 処理

IntStream.range でインデックスを流す

Stream で書くと、こうなります。

import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public final class Zips {

    private Zips() {}

    public static <L, R> Stream<Pair<L, R>> stream(List<L> left, List<R> right) {
        if (left == null || right == null) {
            return Stream.empty();
        }
        int size = Math.min(left.size(), right.size());
        if (size == 0) {
            return Stream.empty();
        }
        return IntStream.range(0, size)
                        .mapToObj(i -> new Pair<>(left.get(i), right.get(i)));
    }
}
Java

使い方の例です。

Zips.stream(names, scores)
    .map(p -> p.getLeft() + " : " + p.getRight())
    .forEach(System.out::println);
Java

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

一つ目は、「IntStream.range(0, size) で“インデックスの Stream”を作り、それを Pair に変換している」ことです。
インデックスを軸にして、2つの List を同時に参照しています。

二つ目は、「Stream.empty() を返すことで、“zip できない状況”を自然に表現している」ことです。
呼び出し側は、空 Stream ならそのまま何も処理されないので、余計な if 文が不要になります。


長さが違うとき、どう振る舞うかを決める

「短い方に合わせる」が基本だが、他の選択肢もある

今までの例では、「短い方の長さに合わせる」実装にしてきました。
これは多くの言語・ライブラリで採用されている、標準的な振る舞いです。

ただし、業務によっては別のルールが欲しくなることもあります。

短い方に合わせる(余りは捨てる)
長い方に合わせて、足りない側は null やデフォルト値で埋める
長さが違ったら例外にする

例えば、「必ず同じ件数であるべき」なら、こう書けます。

public static <L, R> List<Pair<L, R>> zipStrict(List<L> left, List<R> right) {
    if (left == null || right == null) {
        throw new IllegalArgumentException("null は許可しない");
    }
    if (left.size() != right.size()) {
        throw new IllegalArgumentException("サイズが違います");
    }
    return zip(left, right); // さきほどの zip を再利用
}
Java

ここでの重要ポイントは、
「“長さが違うときどうするか”を、ユーティリティのレベルで決めておく」ことです。

毎回呼び出し側で if 文を書くのではなく、
zip(短い方に合わせる)、zipStrict(サイズ違いは例外)など、
“ポリシーの違い”をメソッド名で表現しておくと、コードの意図がとても読みやすくなります。


zip処理が役立つ具体例

例1:ヘッダ行+データ行を結びつける

CSV の1行目がヘッダ、2行目以降がデータ、というよくあるパターン。

List<String> header = List.of("id", "name", "score");
List<String> row    = List.of("u001", "山田", "80");

Zips.stream(header, row)
    .forEach(p -> System.out.println(p.getLeft() + " = " + p.getRight()));
// id = u001
// name = 山田
// score = 80
Java

ここから Map<String, String> を作ることもできます。

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

Map<String, String> record =
        Zips.stream(header, row)
            .collect(Collectors.toMap(
                    Pair::getLeft,
                    Pair::getRight
            ));
Java

「ヘッダ名 → 値」という辞書が一発で作れるので、CSV処理などで非常によく使うパターンです。

例2:2つのバージョンを比較する

「旧データと新データを、同じ位置同士で比較したい」場合。

List<Integer> oldScores = List.of(70, 80, 90);
List<Integer> newScores = List.of(75, 82, 88);

Zips.stream(oldScores, newScores)
    .forEach(p -> {
        int oldScore = p.getLeft();
        int newScore = p.getRight();
        System.out.println(oldScore + " -> " + newScore);
    });
Java

インデックスを意識せずに、「ペア」として扱えるのが zip の気持ちよさです。


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

zip処理は、
単に「2つの List を for 文で同時に回す」話ではなく、
「“位置で結びつく2つの列”を、きれいに扱う設計」です。

同じインデックスの要素同士をペアにしていく、という基本イメージを持つ。
Pair<L, R>Zips.zip のようなユーティリティで、「zip している」ことをコード上で明示する。
長さが違うときの振る舞い(短い方に合わせる/例外にする/埋める)を、メソッドとして切り分けておく。
CSVヘッダ+行、旧値+新値など、「2つの列をセットで扱いたい場面」で積極的に使う。

あなたのコードのどこかに、
2つの List を同じ for 文の中でゴリゴリ回している箇所があれば、
それを一度「zipユーティリティ+Pair」に置き換えられないか眺めてみてください。

その小さな整理が、
「複数の列を“関係のあるペア”として、気持ちよく扱えるエンジニア」への、
確かな一歩になります。

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