1. zip の紹介
プログラミングにおける zip(「ジップ化」「縫い合わせ」)とは、2 つ(またはそれ以上)のリストを取り、それぞれのリストから 1 要素ずつ取り出してペアのストリームに結合する操作を指します。Python にも zip という関数があり、まさにこれを行います。
例:
- 名前のリスト: ["アーニャ", "ボリス", "ヴィカ"]
- 年齢のリスト: [20, 25, 19]
- 「zip」した結果: [("アーニャ", 20), ("ボリス", 25), ("ヴィカ", 19)]
これは、2 つのコレクションを同期的に処理したい場合に便利です。たとえば、名前と年齢を組み合わせてオブジェクトを作る、といったケースです。
なぜ Stream API に zip がないのか?
標準の Stream API(Java 22 以前)には zip メソッドがありません。理由は、ストリームは無限になり得ること、コレクションの長さが異なることがあること、そして片方が長い場合にどう振る舞うべきかが常に自明ではないためです。とはいえ、実務では zip が必要になる場面はしばしばあります。
2. Java での zip 実装: 組み込みメソッドなしでどうするか
最も簡単なのはインデックスを使う方法
2 つのリストがあり、それが「ふつう」のコレクション(たとえば List<A>、List<B>)であることが分かっているなら、インデックスを使えます:
import java.util.*;
import java.util.stream.*;
public class ZipExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("アーニャ", "ボリス", "ヴィカ");
List<Integer> ages = Arrays.asList(20, 25, 19);
int size = Math.min(names.size(), ages.size());
List<Person> people = IntStream.range(0, size)
.mapToObj(i -> new Person(names.get(i), ages.get(i)))
.collect(Collectors.toList());
people.forEach(System.out::println);
}
static class Person {
String name;
int age;
Person(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
}
出力:
アーニャ (20)
ボリス (25)
ヴィカ (19)
何が起きているか?
- 2 つのリストの最小サイズを取得し、範囲外アクセスを防ぎます。
- IntStream.range(0, size) を使い、インデックスのストリームを作ります。
- 各インデックスについて、それぞれのリストから 1 要素ずつ取り出して「縫い合わせ」ます。
- 結果を Collectors.toList() でリストに収集します。
Stream<T> に対して zip を作れるか?
技術的には可能ですが、便利に使えるのは両方のストリームが有限で、かつ背後にインデックスで高速アクセスできる構造(実質 List)がある場合に限られます。「本当の」ストリーム(たとえば無限ストリーム)の正しい zip 実装はより複雑で追加のロジックが必要です。
代替案: サードパーティ製ライブラリ
「出来合いの」zip が欲しければ、次のライブラリを使えます:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
本講座では標準ライブラリを基本とするため、「手作り」の方法を扱います。
3. zip の実用例
例 1. 2 つのコレクションを同期走査(要素の合計)
List<Integer> a = Arrays.asList(1, 2, 3, 4);
List<Integer> b = Arrays.asList(10, 20, 30, 40);
List<Integer> sums = IntStream.range(0, Math.min(a.size(), b.size()))
.mapToObj(i -> a.get(i) + b.get(i))
.collect(Collectors.toList());
System.out.println(sums); // [11, 22, 33, 44]
例 2. 文字列と記号の zip
String[] words = {"cat", "dog", "fox"};
char[] marks = {'!', '?', '.'};
List<String> zipped = IntStream.range(0, Math.min(words.length, marks.length))
.mapToObj(i -> words[i] + marks[i])
.collect(Collectors.toList());
System.out.println(zipped); // [cat!, dog?, fox.]
可視化(図)
names: [アーニャ] [ボリス] [ヴィカ]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (アーニャ,20) (ボリス,25) (ヴィカ,19)
4. Stream.iterate と Stream.generate — 新しいストリームの生成
既存のコレクションを処理するだけでなく、「その場で」新しいシーケンスを作りたいことがあります。Stream API には次の 2 つの便利なメソッドがあります。
- Stream.iterate — 規則に従ってシーケンスを生成(例: 等差数列)。
- Stream.generate — 各要素を Supplier(例: 乱数、現在時刻など)で計算して生成。
Stream.iterate
シンタックス:
Stream.iterate(seed, unaryOperator)
- seed — 開始値。
- unaryOperator — 次の要素を計算する関数。
例 1: 等差数列
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// 出力: 0 2 4 6 8
例 2: 日付の生成
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// 例: 2024-06-09, 2024-06-10, 2024-06-11
例 3: 無限ストリーム — limit を忘れない!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
Java 9+ では、条件付きのオーバーロードが追加されました:
Stream.iterate(0, n -> n < 10, n -> n + 2)
.forEach(System.out::println); // 0 2 4 6 8
Stream.generate
シンタックス:
Stream.generate(Supplier<T>)
各要素は Supplier.get() の呼び出しで計算されます。
例 1: 乱数
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
例 2: 同一値の生成
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
例 3: ユニーク ID
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
可視化(図)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. 利用例: アプリ向けデータ生成
次のような Student クラスがあるとします:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
例 1. テスト用の学生データ生成
List<String> names = Arrays.asList("アーニャ", "ボリス", "ヴィカ", "グレブ", "ダーシャ");
Stream<Student> students = IntStream.range(0, names.size())
.mapToObj(i -> new Student(names.get(i), 18 + i));
students.forEach(System.out::println);
// アーニャ (18), ボリス (19), ヴィカ (20), グレブ (21), ダーシャ (22)
例 2. ランダムな学生を生成
Random random = new Random();
List<String> pool = Arrays.asList("イラ", "オレグ", "マクシム", "ターニャ", "セルゲイ");
Stream<Student> randomStudents = Stream.generate(() ->
new Student(
pool.get(random.nextInt(pool.size())),
18 + random.nextInt(5)
)
);
randomStudents.limit(3).forEach(System.out::println);
// 例: ターニャ (19), オレグ (21), イラ (20)
例 3. レポート用に日付シーケンスを生成
import java.time.LocalDate;
Stream<LocalDate> dates = Stream.iterate(LocalDate.of(2024, 6, 1), d -> d.plusDays(1));
dates.limit(5).forEach(System.out::println);
// 2024-06-01, 2024-06-02, ..., 2024-06-05
比較: zip・iterate・generate をいつ使うか
- zip — インデックスで要素を結びつけながら 2 つ(以上)のリスト/ストリームを同期的に処理したいとき。
- iterate — 規則に基づいたシーケンスが必要なとき(数列、日付、ステップなど)。
- generate — 各要素を独立に計算して生成するとき(乱数、ユニーク ID など)。
6. zip とストリーム生成でよくあるミス
エラー1: limit のない無制限ストリーム。 Stream.iterate や Stream.generate を limit で制限せずに使うと、プログラムがハングしたりメモリを使い切ったりします。
Stream.generate(() -> 1).forEach(System.out::println); // 終わらない!
エラー2: zip 時に長さの違いを正しく処理しない。 一方のリストが他方より長い場合は、最小長に合わせて走査しないと IndexOutOfBoundsException が発生します。
エラー3: 通常の Stream<T> を zip しようとする。 通常のストリームにはインデックスアクセスがありません。実用的な zip はたいてい List に対して行います。
エラー4: ストリーム生成中にコレクションを変更する。 ストリームを流しながらコレクションを変更すると、ConcurrentModificationException を招くことがあります。新しいデータは生成し、既存のデータを「その場で」変更しないでください。
エラー5: 順序の喪失。 順序が重要な場合(たとえば zip)は、List を使い、Set は避けましょう。そうしないと要素の順序が予測不能になります。
GO TO FULL VERSION