Java ストリーム APIの要点
内容の確認日: 2026-10-04
この分野は Gold で最も出題数が多く、ストリームの処理順と、中間操作・終端操作・Collectors の各メソッドが何を返すかが細かく問われます。特に「いつ、どの順番で処理が実行されるか」(遅延評価と短絡評価) を理解していないと、出力を答える問題で間違えます。
出題項目は「Stream API の概要」「Stream の中間操作 (filter, map, peek, sorted および flatMap メソッドなど)」「Stream のリダクション処理 (reduce, collect メソッドおよび Collectors の使用)」「Stream の集計処理」「Stream の探索処理」「java.util.Optional クラスの使用」「ParallelStream を使用した並列処理」の 7 つです。
遅延評価と処理の順番
ストリームの中間操作 (filter・map・peek など) は、呼び出しただけでは実行されず、終端操作 (forEach・collect・count など) が呼ばれたときに初めて実行されます (遅延評価)。終端操作がなければ、何も実行されません。
また、ストリームは「すべての要素に filter → すべての要素に map」のように操作ごとにまとめて処理するのではなく、要素を 1 つずつ、パイプラインの最後まで流してから次の要素に進みます。findFirst のような短絡操作は、結果が決まった時点で残りの要素を処理しません。
import java.util.List;public class Main { public static void main(String[] args) { List.of("a", "bb", "ccc").stream() .filter(s -> { System.out.println("filter " + s); return s.length() > 1; }) .map(s -> { System.out.println("map " + s); return s.toUpperCase(); }) .findFirst() .ifPresent(s -> System.out.println("結果 " + s)); }}java Main filter a filter bb map bb 結果 BB
ccc は、bb で結果が決まったので処理されません。また、ストリームは一度しか使えません。終端操作を実行した後のストリームをもう一度使うと、IllegalStateException が発生します。
import java.util.stream.Stream;public class Main { public static void main(String[] args) { Stream<String> s = Stream.of("x", "y"); System.out.println(s.count()); s.forEach(System.out::println); }}java Main 2
実行時に例外が発生: IllegalStateException
中間操作
主な中間操作は次のとおりです。
・filter(条件): 条件に合う要素だけを残す
・map(関数): 要素を変換する。mapToInt などでプリミティブ型のストリームに、boxed() や mapToObj で Stream に戻せる
・flatMap(関数): 各要素をストリームに変換し、1 つのストリームにつなげる (平坦化)
・distinct(): 重複を除く
・sorted()・sorted(Comparator): 並べ替える
・skip(n)・limit(n): 先頭の n 個を飛ばす・先頭の n 個だけにする
・peek(処理): 要素を変えずに処理を挟む (デバッグ用)
IntStream.range(1, 4) は終わりの値を含まない 1, 2, 3、IntStream.rangeClosed(1, 4) は含む 1, 2, 3, 4 を作ります。
import java.util.Comparator;import java.util.List;import java.util.stream.Collectors;import java.util.stream.IntStream;public class Main { public static void main(String[] args) { List<List<Integer>> nested = List.of(List.of(2, 7), List.of(7, 9), List.of(4)); List<Integer> result = nested.stream() .flatMap(List::stream) .distinct() .sorted(Comparator.reverseOrder()) .skip(1) .limit(2) .collect(Collectors.toList()); System.out.println(result); System.out.println(IntStream.rangeClosed(1, 4).sum() + " " + IntStream.range(1, 4).sum()); }}java Main [7, 4] 10 6
2, 7, 7, 9, 4 → distinct で 2, 7, 9, 4 → 降順で 9, 7, 4, 2 → skip(1) で 7, 4, 2 → limit(2) で 7, 4 です。
リダクション: reduce と collect
reduce は要素を 1 つの値にまとめます。単位元 (初期値) を渡す reduce(単位元, 演算) は値そのものを返し、単位元を渡さない reduce(演算) は、要素がない場合に備えて Optional を返します。
collect には Collectors のメソッドで作ったコレクタを渡します。よく使うものは次のとおりです。
・toList()・toSet()・joining(区切り, 前, 後)
・groupingBy(分類関数): 分類関数の結果をキーにした Map<K, List<T>>。第 2 引数にマップの生成方法 (TreeMap::new など)、最後の引数に下流のコレクタ (counting() など) を指定できる
・partitioningBy(条件): true と false の 2 グループに分けた Map<Boolean, List<T>>。該当する要素がなくても両方のキーがある
・mapping(変換, 下流のコレクタ): 要素を変換してから下流のコレクタに渡す
・toMap(キーの関数, 値の関数): Map を作る
import java.util.List;import java.util.Map;import java.util.Optional;import java.util.TreeMap;import java.util.stream.Collectors;record Book(String title, String genre, int price) {}public class Main { public static void main(String[] args) { List<Book> books = List.of( new Book("A", "IT", 3000), new Book("B", "NOVEL", 800), new Book("C", "IT", 2500), new Book("D", "NOVEL", 1200)); int total = books.stream().map(Book::price).reduce(0, Integer::sum); Optional<Integer> max = books.stream().map(Book::price).reduce(Integer::max); System.out.println(total + " " + max.get()); Map<String, Long> countByGenre = books.stream() .collect(Collectors.groupingBy(Book::genre, TreeMap::new, Collectors.counting())); System.out.println(countByGenre); Map<Boolean, List<String>> expensive = books.stream() .collect(Collectors.partitioningBy(b -> b.price() >= 2000, Collectors.mapping(Book::title, Collectors.toList()))); System.out.println(expensive); System.out.println(books.stream().map(Book::title).collect(Collectors.joining(", ", "[", "]"))); }}java Main 7500 3000 {IT=2, NOVEL=2} {false=[B, D], true=[A, C]} [A, B, C, D]
toMap は、キーが重複すると IllegalStateException を投げます。重複がありうる場合は、第 3 引数に値を合成する関数 ((a, b) -> a + "," + b など) を渡します。
import java.util.Map;import java.util.stream.Collectors;import java.util.stream.Stream;public class Main { public static void main(String[] args) { Map<String, String> byInitial = Stream.of("apple", "avocado") .collect(Collectors.toMap(s -> s.substring(0, 1), s -> s)); System.out.println(byInitial); }}java Main
実行時に例外が発生: IllegalStateException
集計と探索の終端操作
count() は long を返します。IntStream などのプリミティブ型のストリームには sum()・average()・max()・min() があり、average() は OptionalDouble、max()・min() は OptionalInt を返します。空のストリームの sum() は 0 ですが、average() は値のない OptionalDouble です。summaryStatistics() を使うと、件数・合計・最小・最大・平均を 1 回でまとめて求められます。
Stream<T> の max・min には、比較方法を決める Comparator を渡す必要があります (引数なしでは呼べません)。
import java.util.Comparator;import java.util.IntSummaryStatistics;import java.util.stream.IntStream;import java.util.stream.Stream;public class Main { public static void main(String[] args) { IntSummaryStatistics st = IntStream.of(4, 8, 3).summaryStatistics(); System.out.println(st.getMin() + " " + st.getMax() + " " + st.getAverage()); System.out.println(IntStream.empty().sum() + " " + IntStream.empty().average().isPresent()); System.out.println(Stream.of("kiwi", "fig", "banana").max(Comparator.comparing(String::length)).get()); }}java Main 3 8 5.0 0 false banana
探索の終端操作 findFirst・findAny は Optional を返します。anyMatch・allMatch・noneMatch は boolean を返し、結果が決まった時点で残りの要素を処理しない短絡操作です。空のストリームでは、anyMatch は false、allMatch と noneMatch は true になります。
import java.util.stream.Stream;public class Main { public static void main(String[] args) { boolean all = Stream.of(2, 4, 5, 6) .peek(n -> System.out.print(n + " ")) .allMatch(n -> n % 2 == 0); System.out.println("-> " + all); System.out.println(Stream.empty().allMatch(x -> false) + " " + Stream.empty().anyMatch(x -> true)); }}java Main 2 4 5 -> false true false
Optional
Optional は、値があるかないかを表す入れ物です。作り方と取り出し方の主なメソッドは次のとおりです。
・Optional.of(値): null を渡すと、その場で NullPointerException
・Optional.ofNullable(値): null なら空の Optional
・Optional.empty(): 空の Optional
・get(): 値を返す。空なら NoSuchElementException
・orElse(値)・orElseGet(関数)・orElseThrow(): 空のときの代わりの値・代わりの値を作る関数・例外
・map・filter・ifPresent・isPresent・isEmpty
orElse の引数は普通のメソッドの引数なので、Optional に値があっても呼び出す前に評価されます。orElseGet に渡した関数は、空のときだけ実行されます。
import java.util.Optional;public class Main { static String fallback() { System.out.println("fallback 実行"); return "default"; } public static void main(String[] args) { Optional<String> name = Optional.of("gold"); System.out.println(name.orElse(fallback())); System.out.println(name.orElseGet(Main::fallback)); Optional<String> none = Optional.ofNullable(null); System.out.println(none.map(String::length).orElse(-1)); System.out.println(name.filter(s -> s.length() > 5).isPresent()); }}java Main fallback 実行 gold gold -1 false
並列ストリーム
parallelStream() や parallel() で得られる並列ストリームは、要素を複数のスレッドで分担して処理します。並列ストリームでも、順序を持つストリーム (List や range から作ったもの) を collect で集めると、結果の順序は保たれます。一方、forEach は処理したスレッドの都合で順番が変わります。元の順序で処理したいときは forEachOrdered を使います。
import java.util.List;import java.util.stream.Collectors;import java.util.stream.IntStream;public class Main { public static void main(String[] args) { List<Integer> list = IntStream.rangeClosed(1, 5).boxed().collect(Collectors.toList()); List<Integer> doubled = list.parallelStream().map(n -> n * 2).collect(Collectors.toList()); System.out.println(doubled); list.parallelStream().forEachOrdered(n -> System.out.print(n + " ")); System.out.println(); System.out.println(list.parallelStream().reduce(0, Integer::sum)); }}java Main [2, 4, 6, 8, 10] 1 2 3 4 5 15
並列ストリームの reduce では、要素をいくつかのグループに分けて合成するので、第 1 引数は本当の単位元 (足し算なら 0、掛け算なら 1) でなければなりません。reduce(10, Integer::sum) のように単位元でない値を渡すと、10 がグループの数だけ足されて、結果が逐次処理と変わることがあります。
試験でよくある引っかけ
終端操作のないストリーム
中間操作だけでは何も実行されません。
peekの出力を問う問題では、終端操作があるかを最初に確かめましょう。ストリームの再利用
終端操作を実行したストリームをもう一度使うと
IllegalStateExceptionです。reduceの戻り値の型単位元ありの
reduce(0, ...)は値、単位元なしのreduce(...)はOptionalを返します。toMapのキーの重複キーが重複すると
IllegalStateExceptionです。第 3 引数で値の合成方法を指定します。空のストリームの
allMatch空のストリームでは
allMatchとnoneMatchはtrue、anyMatchはfalseです。orElseとorElseGetorElse(fallback())は値があってもfallback()を実行します。空のときだけ実行したいならorElseGet。
問題で確認する
この分野の問題を、解説つきで読めます (登録不要)。
出典・参考
- Oracle University 日本「Java SE 17 Programmer II (1Z0-826-JPN) 試験」
- Java SE 17 API ドキュメント「java.util.stream」
- Java SE 17 API ドキュメント「Collectors」
- Java SE 17 API ドキュメント「Optional」
コード例は、試験の対象バージョンで実際にコンパイル・実行して結果を確認しています。