1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaGold Stream API について理解を深める

1
Last updated at Posted at 2024-09-17

はじめに

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の性質

  1. データを保持しない ── Streamはソース(List, 配列など)から要素を運ぶパイプライン
  2. 一度きり ── 終端操作を実行したStreamは再利用できない(IllegalStateException
  3. 遅延評価 ── 終端操作が呼ばれるまで中間操作は実行されない

公式ドキュメントにも次のように明記されています。

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() の戻り値は常に OptionalDoubleIntStream でも 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 / maxOptional を返します。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 のキーは必ず truefalse の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が安全に使える条件

  1. ステートレス: ラムダ内で外部変数を変更しない
  2. 非干渉: Stream処理中にソースを変更しない
  3. 結合的: reduce のaccumulatorが結合律 (a op b) op c == a op (b op c) を満たす
  4. スレッドセーフ: 共有データへのアクセスが安全

これらが崩れると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公式ドキュメント

推奨書籍

  • 徹底攻略 Java SE 11 Gold 問題集(通称: 黒本)
  • オラクル認定資格教科書 Javaプログラマ Gold SE11(通称: 紫本)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?