はじめに
Java Gold (SE 11 / SE 17) 試験では、Stream APIとラムダ式で全体の4〜5割 を占めると言われるほど出題比率が高い分野です。試験勉強を始めてみると分かりますが、Stream APIは「使える」だけでは足りず、以下のような細かい挙動まで正確に答えられること が求められます。
- 中間操作と終端操作の見分けがつくか
- 各メソッドの戻り値(
Stream<T>/Optional<T>/long/OptionalDoubleなど)を暗記しているか -
Stream<Integer>とIntStreamの違いを説明できるか - 消費済みStreamを再利用するとどうなるか
-
Collectors.groupingByのネストが読めるか
この記事では、1年前に自分が書いた入門的なまとめを、JavaGold試験対策の視点で全面的に書き直し ます。出題パターンごとに「ここが落とし穴」を明示しながら進めます。
対象: JavaGold SE 11 (1Z0-816) / SE 17 (1Z0-826) 受験者
前提: Java Silver 合格レベル(ラムダ式の基本は理解済み)
参考: Oracle Certified Java Programmer, Gold SE 11 公式ページ
1. Streamの基本構造 ── 試験で問われる3つの性質
JavaGoldでは「Streamとは何か」を言葉で問う問題は少なく、挙動の理解 が問われます。そのためには次の3点を押さえます。
試験で押さえるべきStreamの性質
- データを保持しない ── Streamはソース(List, 配列など)から要素を運ぶパイプライン
-
一度きり ── 終端操作を実行したStreamは再利用できない(
IllegalStateException) - 遅延評価 ── 終端操作が呼ばれるまで中間操作は実行されない
公式ドキュメントにも次のように明記されています。
A stream is not a data structure that stores elements; instead, it conveys elements from a source ... through a pipeline of computational operations.
─ java.util.stream パッケージ
【頻出】Streamの再利用で実行時エラー
Oracle公式のサンプル問題にも出ている典型パターンです。
String[] arr = { "Madrid", "Barcelona" };
Stream<String> strm = Arrays.stream(arr);
strm.map(s -> s.toUpperCase()).forEach(System.out::println); // ①
strm.filter(s -> s.length() >= 7).forEach(System.out::println); // ② ← ここで例外
正解: 実行時に IllegalStateException が発生
①の forEach でStreamが消費されている。②で同じStreamを使おうとするため例外になる。
再度使うには Arrays.stream(arr) で新しいStreamを作り直す必要がある。
2. 中間操作と終端操作 ── 戻り値で見分ける
試験では「中間操作か終端操作か」を判別させる問題がよく出ます。覚え方はシンプルで、戻り値が Stream<T> 系なら中間操作、それ以外なら終端操作 です。
| 区分 | メソッド | 戻り値 |
|---|---|---|
| 中間操作 | filter |
Stream<T> |
| 中間操作 | map |
Stream<R> |
| 中間操作 | mapToInt |
IntStream |
| 中間操作 | flatMap |
Stream<R> |
| 中間操作 | distinct |
Stream<T> |
| 中間操作 | sorted |
Stream<T> |
| 中間操作 | peek |
Stream<T> |
| 中間操作 |
limit / skip
|
Stream<T> |
| 終端操作 |
forEach / forEachOrdered
|
void |
| 終端操作 | count |
long |
| 終端操作 | reduce |
Optional<T> または T
|
| 終端操作 | collect |
R(Collector次第) |
| 終端操作 | toArray |
Object[] / T[]
|
| 終端操作 |
findFirst / findAny
|
Optional<T> |
| 終端操作 |
anyMatch / allMatch / noneMatch
|
boolean |
| 終端操作 |
min / max
|
Optional<T> |
試験で間違えやすいポイント
-
count()の戻り値はlong(int ではない) -
findFirst()はOptional<T>(T を直接返すわけではない) -
anyMatch()はboolean(Optional ではない)
3. Stream<Integer> と IntStream の違い
JavaGoldで頻出する「よく似ているのに戻り値が違う」パターンです。
// Stream<Integer> ── 参照型のStream
Stream<Integer> s1 = Stream.of(1, 2, 3);
Optional<Integer> sum1 = s1.reduce(Integer::sum); // reduce() を使う
// sum() メソッドは存在しない!
// IntStream ── プリミティブ特化Stream
IntStream s2 = IntStream.of(1, 2, 3);
int sum2 = s2.sum(); // sum() が使える
double avg = IntStream.of(1,2,3).average().getAsDouble();
long cnt = IntStream.of(1,2,3).count();
Stream<Integer> には sum() / average() / max() / min() がない
集計メソッドはプリミティブ特化Stream (IntStream / LongStream / DoubleStream) にしかない。Stream<Integer> で合計を出したければ reduce() か mapToInt(...).sum() を使う。
特化Streamの集計メソッド戻り値(暗記必須)
| メソッド | IntStream | LongStream | DoubleStream | Stream<T> |
|---|---|---|---|---|
sum() |
int |
long |
double |
なし |
average() |
OptionalDouble |
OptionalDouble |
OptionalDouble |
なし |
max() |
OptionalInt |
OptionalLong |
OptionalDouble |
Optional<T> |
min() |
OptionalInt |
OptionalLong |
OptionalDouble |
Optional<T> |
count() |
long |
long |
long |
long |
summaryStatistics() |
IntSummaryStatistics |
LongSummaryStatistics |
DoubleSummaryStatistics |
なし |
average() の戻り値は常に OptionalDouble(IntStream でも double を返す)。平均値は小数になりうるためで、試験でよく聞かれる。
Stream間の相互変換
Stream<String> s = Stream.of("apple", "banana");
// Stream<T> → IntStream
IntStream is = s.mapToInt(String::length);
// IntStream → Stream<Integer>
Stream<Integer> si = IntStream.of(1, 2, 3).boxed();
// IntStream → Stream<R>
Stream<String> ss = IntStream.of(1, 2, 3).mapToObj(i -> "num" + i);
4. 遅延評価と短絡評価 ── peek で挙動を追う
遅延評価はJavaGoldで頻出の概念です。実際にコードを読ませて出力を問う問題が出ます。
List<String> result = Stream.of("apple", "banana", "cherry", "date")
.peek(s -> System.out.println("filter前: " + s))
.filter(s -> s.length() > 5)
.peek(s -> System.out.println("filter後: " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("map後: " + s))
.collect(Collectors.toList());
出力:
filter前: apple
filter前: banana
filter後: banana
map後: BANANA
filter前: cherry
filter後: cherry
map後: CHERRY
filter前: date
ここがポイント
filter前: apple, banana, cherry, date と横に全要素が出力されるのではなく、
1要素ずつパイプラインを縦に通過している。これが遅延評価の正体。
短絡評価の例
// 無限Streamでも limit で停止できる
Stream.iterate(1, i -> i + 1)
.filter(i -> i % 7 == 0)
.limit(3)
.forEach(System.out::println);
// 出力: 7, 14, 21
もし遅延評価でなければ、Stream.iterate の時点で無限ループに落ちます。
【注意】sortedは無限Streamで止まらない
// ❌ 実行すると止まらない
Stream.iterate(1, i -> i + 1)
.sorted() // 全要素を見ないとソートできない
.limit(10)
.forEach(System.out::println);
sorted / distinct は ステートフル中間操作。全要素を一旦保持する必要があるため、無限Streamでは終わらない。
5. Optional ── Stream終端操作とセットで必ず出る
findFirst / findAny / reduce / min / max は Optional を返します。JavaGoldでは Optional のメソッド名と戻り値もセットで問われます。
Optional<String> opt = Stream.of("a", "b", "c").findFirst();
opt.get(); // String(空だと NoSuchElementException)
opt.orElse("default"); // String
opt.orElseGet(() -> "computed"); // String
opt.orElseThrow(); // Java 10+、空なら NoSuchElementException
opt.isPresent(); // boolean
opt.isEmpty(); // boolean(Java 11+)
opt.ifPresent(System.out::println);
opt.ifPresentOrElse( // Java 9+
System.out::println,
() -> System.out.println("empty"));
Optional<T> vs OptionalInt / OptionalLong / OptionalDouble
- プリミティブ特化版は
get()がなく、getAsInt()/getAsLong()/getAsDouble()を使う -
orElse/orElseGetは共通
6. reduce ── 3つの形を使い分ける
reduce は引数が1・2・3個のオーバーロードがあり、それぞれ戻り値と用途が違います。
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// ① 引数1: Optional<T> を返す(恒等値なし)
Optional<Integer> r1 = nums.stream().reduce((a, b) -> a + b);
// ② 引数2: T を返す(恒等値あり、空でも結果が出る)
int r2 = nums.stream().reduce(0, (a, b) -> a + b);
// ③ 引数3: U を返す(恒等値 + accumulator + combiner、並列用)
int r3 = nums.stream().reduce(
0, // identity
(acc, x) -> acc + x, // accumulator: U と T を U に
Integer::sum); // combiner: 部分結果をマージ(並列時に使用)
引数3形式の意味
- 並列Streamで要素を分割処理するとき、各スレッドの部分結果を最後に合成するのが
combiner - 逐次Streamでは
combinerは呼ばれないが、シグネチャ上は必須
7. Collectors ── 試験で最もよく読まされる
JavaGoldでは Collectors の組み合わせを読ませる問題が頻出します。ネストが深くなるほど読みにくくなるので、パターンを暗記しておきます。
基本形
import static java.util.stream.Collectors.*;
// toList / toSet / toMap
List<String> list = stream.collect(toList());
Set<String> set = stream.collect(toSet());
Map<String, Integer> map = people.stream()
.collect(toMap(Person::name, Person::age));
// joining(文字列連結)
String joined = stream.collect(joining(", ", "[", "]"));
// → [a, b, c]
グルーピング(最頻出)
record Employee(String name, String dept, int salary) {}
List<Employee> emps = List.of(
new Employee("Alice", "HR", 400),
new Employee("Bob", "Eng", 600),
new Employee("Charlie", "Eng", 550),
new Employee("Dave", "HR", 450)
);
// ① 部門ごとに従業員リスト
Map<String, List<Employee>> g1 = emps.stream()
.collect(groupingBy(Employee::dept));
// ② 部門ごとに人数(ダウンストリームCollector)
Map<String, Long> g2 = emps.stream()
.collect(groupingBy(Employee::dept, counting()));
// ③ 部門ごとに平均給与
Map<String, Double> g3 = emps.stream()
.collect(groupingBy(Employee::dept, averagingInt(Employee::salary)));
// ④ 部門ごとに給与合計
Map<String, Integer> g4 = emps.stream()
.collect(groupingBy(Employee::dept, summingInt(Employee::salary)));
// ⑤ 部門ごとに名前だけ集める(mapping)
Map<String, List<String>> g5 = emps.stream()
.collect(groupingBy(
Employee::dept,
mapping(Employee::name, toList())));
ダウンストリームCollector
groupingBy の第2引数に別のCollectorを渡すと、グループ内でさらに集計できる。
-
counting()→ 件数 -
summingInt/Long/Double→ 合計 -
averagingInt/Long/Double→ 平均 -
mapping(f, downstream)→ 変換してから集計 -
filtering(p, downstream)→ フィルタしてから集計(Java 9+)
partitioningBy ── true/falseで2分割
Map<Boolean, List<Employee>> part = emps.stream()
.collect(partitioningBy(e -> e.salary() >= 500));
// {false=[Alice, Dave], true=[Bob, Charlie]}
partitioningBy のキーは必ず true と false の2つ。該当0件でも空リストが入る(groupingBy だとキー自体が存在しない)。ここは試験でよく出る差分。
8. flatMap ── ネストを解消する
map と混同しやすいが、flatMap は「各要素をStreamに変換 → 結合して1本のStreamに」するメソッドです。
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6)
);
// ❌ map ではネストが解消されない
Stream<Stream<Integer>> bad = nested.stream().map(List::stream);
// ✅ flatMap で平坦化
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList();
// [1, 2, 3, 4, 5, 6]
9. 並列Stream ── 正しく使える条件
JavaGoldでは「並列Streamを使っていい条件」が問われます。
// 並列化の2通り
list.parallelStream()...
list.stream().parallel()...
並列Streamが安全に使える条件
- ステートレス: ラムダ内で外部変数を変更しない
- 非干渉: Stream処理中にソースを変更しない
-
結合的:
reduceのaccumulatorが結合律(a op b) op c == a op (b op c)を満たす - スレッドセーフ: 共有データへのアクセスが安全
これらが崩れるとConcurrentModificationExceptionやデータ欠損が発生する。
典型的なNG例
// ❌ 副作用で共有リストを書き換え
List<Integer> result = new ArrayList<>();
list.parallelStream()
.filter(n -> n > 0)
.forEach(result::add); // 競合でArrayListが壊れる
// ✅ collectで安全に収集
List<Integer> result = list.parallelStream()
.filter(n -> n > 0)
.collect(Collectors.toList());
forEach と forEachOrdered
// forEach: 並列時は順序保証なし
Stream.of(1,2,3,4,5).parallel().forEach(System.out::print);
// → 3 1 2 5 4 など(実行毎に異なる)
// forEachOrdered: 順序保証あり(並列でも)
Stream.of(1,2,3,4,5).parallel().forEachOrdered(System.out::print);
// → 12345 常に
参考リンク
試験公式
Java公式ドキュメント
- Stream (Java SE 21 JavaDoc)
- IntStream (Java SE 21 JavaDoc)
- Collectors (Java SE 21 JavaDoc)
- Optional (Java SE 21 JavaDoc)
- java.util.stream パッケージ概要(日本語)
推奨書籍
- 徹底攻略 Java SE 11 Gold 問題集(通称: 黒本)
- オラクル認定資格教科書 Javaプログラマ Gold SE11(通称: 紫本)