Stream API・Optional
Stream の3性質・中間/終端操作・遅延評価・map/flatMap・collect詳解・IntStream・Optional の全メソッドと罠
Chapter 08 ─ Stream API・Optional
Stream と Lambda は試験の 18%。「遅延評価・使い捨て・元データ不変」の 3 性質と、
filter/map/collectの組み合わせが試験の核心。Optional.of(null)で NPE、orElseは常に評価される点も頻出の引っかけ。
8-1. Stream とは ─「工場の組み立てライン」
現実の比喩: 車の組み立てラインを思い浮かべてほしい。
材料(データソース)
→ 塗装工程(中間操作 1: filter で不良品を除く)
→ 組み立て工程(中間操作 2: map で変換する)
→ 検査工程(中間操作 3: sorted で並べる)
→ 完成品の出荷(終端操作: collect で収集する)
従来の for ループが「どうやって処理するか(手順)」を書くのに対し、Stream は「何をしたいか(宣言)」を書く。
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
// ─ 命令型(for ループ、手順を逐一指定) ─
List<String> result1 = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
result1.add(name.toUpperCase());
}
}
// ─ 宣言型(Stream、意図を宣言) ─
List<String> result2 = names.stream()
.filter(name -> name.length() > 3) // 「3文字超のものを残す」
.map(String::toUpperCase) // 「大文字に変換する」
.collect(Collectors.toList()); // 「リストに集める」
// → [ALICE, CHARLIE, DAVID](Bob=3文字は除外)
パイプラインの構造
データソース → 中間操作(0回以上)→ 終端操作(必ず1回)
| 種別 | メソッド例 | 戻り型 | 複数使用 |
|---|---|---|---|
| 中間操作 | filter, map, sorted | Stream<T> | ○(チェーン可) |
| 終端操作 | collect, forEach, count | Stream 以外 | ✗(1回だけ) |
8-2. Stream の 3 つの重要な性質
性質 1: 遅延評価(Lazy Evaluation)
中間操作は終端操作が呼ばれるまで一切実行されない。パイプラインを組み立てても、collect や forEach が来るまでは「設計図を書いただけ」の状態。
Stream<String> pipeline = names.stream()
.filter(name -> {
System.out.println("チェック中: " + name); // いつ実行される?
return name.length() > 3;
});
System.out.println("── 終端操作の前 ──"); // ← ここで "チェック中" は出力されない!
List<String> result = pipeline.collect(Collectors.toList()); // ← ここで初めて filter が動く
出力順:
── 終端操作の前 ──
チェック中: Alice
チェック中: Bob
チェック中: Charlie
...
遅延評価の恩恵: 短絡操作(findFirst, limit 等)と組み合わせると、必要な分しか処理しない。
// "5を超える最初の偶数" を探す場合
OptionalInt first = IntStream.rangeClosed(1, 1000000)
.filter(n -> {
System.out.println("filter: " + n);
return n % 2 == 0 && n > 5;
})
.findFirst();
// filter: 1, filter: 2, filter: 3, filter: 4, filter: 5, filter: 6(見つかったら即停止)
// 100 万件全部は処理しない!
性質 2: 使い捨て(Single Use)
一度終端操作を呼んだ Stream は再利用できない。
Stream<Integer> s = Stream.of(1, 2, 3);
s.forEach(System.out::println); // OK: 1 2 3
s.forEach(System.out::println); // IllegalStateException: stream has already been operated upon or closed
Stream を変数に保存して再利用しようとするのは NG。毎回 stream() を呼び直す。
試験口コミ: 「Stream を変数に入れて 2 回 forEach したらエラーになった」という経験談が多い。
性質 3: 元のデータを変更しない
Stream 操作は元のコレクション・配列を変更しない。新しいデータ(または結果)を生成する。
List<String> original = new ArrayList<>(List.of("b", "a", "c"));
List<String> sorted = original.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(original); // [b, a, c](変わっていない)
System.out.println(sorted); // [a, b, c](新しいリスト)
8-3. Stream の生成
// ── コレクションから ──
List<String> list = List.of("A", "B", "C");
Stream<String> s1 = list.stream();
Stream<String> s2 = list.parallelStream(); // 並列 Stream
// ── 配列から ──
String[] arr = {"X", "Y", "Z"};
Stream<String> s3 = Arrays.stream(arr);
Stream<String> s4 = Arrays.stream(arr, 1, 3); // arr[1]〜arr[2] のみ
// ── 値を直接指定 ──
Stream<Integer> s5 = Stream.of(1, 2, 3, 4, 5);
Stream<Object> s6 = Stream.empty(); // 要素ゼロの空 Stream
// ── 無限 Stream(必ず limit などで止める) ──
// iterate: 初期値 → 次の値を計算する関数
Stream<Integer> s7 = Stream.iterate(0, n -> n + 2).limit(5);
// 0, 2, 4, 6, 8
// Java 9+ の iterate(終了条件を指定できる形)
Stream<Integer> s8 = Stream.iterate(0, n -> n < 10, n -> n + 3);
// 0, 3, 6, 9(n < 10 を満たす間)
// generate: Supplier で都度生成
Stream<Double> s9 = Stream.generate(Math::random).limit(3);
// ランダムな Double が 3 つ
// ── 2つの Stream を結合 ──
Stream<String> s10 = Stream.concat(Stream.of("A", "B"), Stream.of("C", "D"));
// A, B, C, D
8-4. 中間操作 ─ Stream を変形・絞り込みする
中間操作は Stream<T> を返すため、チェーンで繋げられる。終端操作が来るまで実行されない。
filter ─ 条件を満たす要素だけを残す
Predicate<T> を引数に取る。
List<String> list = List.of("apple", "banana", "cherry", "avocado");
list.stream()
.filter(s -> s.startsWith("a")) // "a" で始まるもの
.filter(s -> s.length() > 5) // かつ 5 文字超
.forEach(System.out::println); // "avocado"
map ─ 各要素を別の値に変換する
Function<T, R> を引数に取る。型を変えることもできる。
List<String> names = List.of("alice", "bob", "charlie");
// String → String(大文字化)
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // ALICE, BOB, CHARLIE
// String → Integer(文字数に変換)
names.stream()
.map(String::length)
.forEach(System.out::println); // 5, 3, 7
// プリミティブ専用 map(ボクシングを避けて効率化)
names.stream()
.mapToInt(String::length) // Stream<Integer> ではなく IntStream を返す
.sum(); // 5 + 3 + 7 = 15
flatMap ─ ネストを展開して1本の Stream にする
Function<T, Stream<R>> を引数に取る。「各要素を Stream に変換し、その Stream たちを1つに平坦化」。
// map の場合: Stream<Stream<Integer>> になってしまう(ネストされたまま)
// flatMap の場合: Stream<Integer> に展開される(フラット化)
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
nested.stream()
.flatMap(List::stream) // 各 List を Stream に変換して展開
.forEach(System.out::print); // 123456789
// 文字列を単語に分解する典型例
List<String> sentences = List.of("hello world", "java stream");
sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" "))) // 文→単語の Stream に変換→展開
.forEach(System.out::println);
// hello / world / java / stream
map vs flatMap:
map: 1 要素 → 1 要素(変換)flatMap: 1 要素 → 複数要素(展開して平坦化)
// Stream<String[]>(配列が要素) ← map の場合
Stream<String[]> mapped = sentences.stream().map(s -> s.split(" "));
// Stream<String>(String が要素)← flatMap の場合
Stream<String> flattened = sentences.stream().flatMap(s -> Arrays.stream(s.split(" ")));
sorted ─ ソート
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);
nums.stream().sorted().forEach(System.out::print); // 11234569(昇順)
nums.stream().sorted(Comparator.reverseOrder()).forEach(System.out::print); // 96543211(降順)
// カスタム Comparator
List<String> words = List.of("banana", "apple", "cherry", "fig");
words.stream()
.sorted(Comparator.comparingInt(String::length)) // 文字数昇順
.forEach(System.out::println); // fig, apple, banana, cherry
words.stream()
.sorted(Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())) // 文字数昇順 + 辞書順で同じ長さをさらにソート
.forEach(System.out::println);
distinct / limit / skip / peek
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3);
nums.stream()
.distinct() // 重複除去: 3,1,4,5,9,2,6
.sorted() // 昇順: 1,2,3,4,5,6,9
.skip(2) // 先頭2件スキップ: 3,4,5,6,9
.limit(3) // 先頭3件のみ: 3,4,5
.forEach(System.out::print); // 345
// peek: デバッグ用(Consumer を実行しつつ Stream はそのまま通す)
nums.stream()
.filter(n -> n > 4)
.peek(n -> System.out.println("通過: " + n)) // 副作用(ログ等)
.map(n -> n * 10)
.collect(Collectors.toList());
8-5. 終端操作 ─ Stream を消費して結果を得る
終端操作を呼ぶと Stream が「消費」される(再利用不可)。
forEach / forEachOrdered
List.of(3, 1, 2).stream()
.forEach(n -> System.out.print(n + " ")); // 3 1 2(順列では順序通りだが並列は不定)
List.of(3, 1, 2).parallelStream()
.forEachOrdered(n -> System.out.print(n + " ")); // 常に 3 1 2(並列でも順序保証)
collect ─ Stream をコレクションや文字列に集める
Collectors の static メソッドを渡す。試験頻出なので全種類を押さえる。
import java.util.stream.Collectors;
List<String> names = List.of("Alice", "Bob", "Charlie", "Alice", "David");
// ── List/Set に収集 ──
List<String> toList = names.stream().collect(Collectors.toList());
Set<String> toSet = names.stream().collect(Collectors.toSet()); // 重複除去
// Java 16+: toList() が使える(不変リストを返す)
List<String> immutableList = names.stream().toList();
// ── Map に収集(キーが重複するとデフォルトで IllegalStateException) ──
Map<String, Integer> toMap = names.stream()
.distinct() // 先に重複排除してからでないとキー衝突エラー
.collect(Collectors.toMap(
s -> s, // キーマッパー
String::length // 値マッパー
));
// {Alice=5, Bob=3, Charlie=7, David=5}
// キー衝突を解決する第3引数(merge 関数)
Map<String, Integer> toMapSafe = names.stream()
.collect(Collectors.toMap(
s -> s,
String::length,
(existing, newVal) -> existing // 既存の値を優先
));
// ── 文字列結合 ──
String joined1 = names.stream()
.distinct()
.collect(Collectors.joining()); // "AliceBobCharlieDavid"(区切りなし)
String joined2 = names.stream()
.distinct()
.collect(Collectors.joining(", ")); // "Alice, Bob, Charlie, David"
String joined3 = names.stream()
.distinct()
.collect(Collectors.joining(", ", "[", "]")); // "[Alice, Bob, Charlie, David]"
// ── グループ化(Map<K, List<V>>) ──
List<String> words = List.of("apple", "ant", "banana", "cat", "cherry");
Map<Character, List<String>> byFirstChar = words.stream()
.collect(Collectors.groupingBy(s -> s.charAt(0)));
// {a=[apple, ant], b=[banana], c=[cat, cherry]}
// グループ化 + カウント
Map<Character, Long> countByFirst = words.stream()
.collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.counting()));
// {a=2, b=1, c=2}
// グループ化 + mapping(各グループを変換)
Map<Character, List<Integer>> lengthsByFirst = words.stream()
.collect(Collectors.groupingBy(
s -> s.charAt(0),
Collectors.mapping(String::length, Collectors.toList())
));
// {a=[5, 3], b=[6], c=[3, 6]}
// ── 二分割(boolean キーの Map<Boolean, List<V>>) ──
Map<Boolean, List<Integer>> partitioned = List.of(1, 2, 3, 4, 5, 6).stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5], true=[2, 4, 6]}
count / reduce / min / max
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);
// count: 要素数
long count = nums.stream().count(); // 8
// reduce: 畳み込み
// 初期値あり → T を返す(空 Stream でも初期値が返る)
int sum = nums.stream().reduce(0, (acc, n) -> acc + n); // 31
int prod = nums.stream().reduce(1, (acc, n) -> acc * n); // 6480
// メソッド参照で書くことも多い
int sum2 = nums.stream().reduce(0, Integer::sum);
// 初期値なし → Optional<T> を返す(空 Stream では empty を返す)
Optional<Integer> max = nums.stream().reduce(Integer::max); // Optional[9]
Optional<Integer> min = nums.stream().reduce(Integer::min); // Optional[1]
// min/max は Comparator を受け取る専用メソッドもある
Optional<String> shortest = List.of("abc", "de", "fghi")
.stream()
.min(Comparator.comparingInt(String::length)); // Optional["de"]
findFirst / findAny ─ 短絡操作
条件を満たす要素が見つかった時点で処理を止める。
List<String> list = List.of("apple", "banana", "avocado", "cherry");
Optional<String> first = list.stream()
.filter(s -> s.startsWith("a"))
.findFirst(); // Optional[apple](最初に見つかったもの)
Optional<String> any = list.stream()
.filter(s -> s.startsWith("a"))
.findAny(); // Optional[apple] または Optional[avocado](どちらでもよい)
// 並列 Stream では findAny の方が高速なことがある
anyMatch / allMatch / noneMatch ─ 短絡マッチ
List<String> list = List.of("apple", "banana", "cherry");
// anyMatch: 1つでも条件を満たす要素があれば true(そこで停止)
boolean any = list.stream().anyMatch(s -> s.length() > 5); // true(banana=6, cherry=6)
// allMatch: 全要素が条件を満たせば true(満たさないものが見つかったら即 false)
boolean all = list.stream().allMatch(s -> s.length() >= 5); // true(全部 5 文字以上)
// noneMatch: 1つも条件を満たす要素がなければ true
boolean none = list.stream().noneMatch(s -> s.contains("z")); // true(z を含むものなし)
// ── 空の Stream の場合(試験頻出) ──
boolean allEmpty = Stream.empty().allMatch(x -> false); // true(vacuous truth: 反例なし)
boolean anyEmpty = Stream.empty().anyMatch(x -> true); // false(満たす要素が 1 つもない)
boolean noneEmpty = Stream.empty().noneMatch(x -> true); // true(反例なし)
試験口コミ: 「空 Stream の
allMatchがtrueになるのを知らなかった」という声が多い。なぜ
trueなのか。「クラス全員が宿題を出した」という命題を考えてほしい。生徒が 0 人のクラスなら、宿題を出さなかった反例が 1 人も存在しないので「全員が出した」は嘘とも言えない →true。論理学では「空集合に対して全称命題は真」とされる(vacuous truth)。一方anyMatchは「宿題を出した人が 1 人でもいたか?」→ 0 人なので誰も出していない →false。
8-6. プリミティブ Stream ─ ボクシングを避けて効率化
Stream<Integer> はオブジェクトのため、int をボクシング/アンボクシングするコストがかかる。大量のデータを数値で扱う場合は IntStream, LongStream, DoubleStream を使う。
import java.util.stream.IntStream;
// ── 範囲生成 ──
IntStream.range(0, 5) // 0, 1, 2, 3, 4(終端 5 を含まない)
.forEach(System.out::println);
IntStream.rangeClosed(1, 5) // 1, 2, 3, 4, 5(終端 5 を含む)
.forEach(System.out::println);
range vs rangeClosed の違い(試験頻出):
| メソッド | 範囲 | 例 |
|---|---|---|
IntStream.range(a, b) | a <= n < b(終端を含まない) | range(1, 5) → 1,2,3,4 |
IntStream.rangeClosed(a, b) | a <= n <= b(終端を含む) | rangeClosed(1, 5) → 1,2,3,4,5 |
// ── IntStream の集計メソッド ──
int sum = IntStream.rangeClosed(1, 100).sum(); // 5050
double avg = IntStream.of(10, 20, 30).average().getAsDouble(); // 20.0
int max = IntStream.of(3, 1, 4, 1, 5).max().getAsInt(); // 5
int min = IntStream.of(3, 1, 4, 1, 5).min().getAsInt(); // 1
long count = IntStream.range(0, 10).count(); // 10
// average/max/min は OptionalDouble/OptionalInt を返す
// 空 Stream の場合は .getAsDouble() で NoSuchElementException → isPresent() チェックが安全
// ── Stream<Integer> ↔ IntStream の変換 ──
// Stream<Integer> → IntStream(ボクシングコストを除去)
int total = Stream.of(1, 2, 3, 4, 5)
.mapToInt(Integer::intValue) // または .mapToInt(n -> n)
.sum(); // 15
// IntStream → Stream<Integer>(boxed でラップ)
List<Integer> list = IntStream.range(1, 6)
.boxed()
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
// IntStream → Stream<String>(mapToObj)
Stream<String> strs = IntStream.range(1, 4)
.mapToObj(n -> "item" + n); // "item1", "item2", "item3"
8-7. Optional ─ null を型で表現する
なぜ Optional が必要か
null を返すメソッドは「値がないかもしれない」という情報を型に表現できない。呼び出し側が null チェックを忘れると NPE になる。
// null を返すメソッド(呼び出し側が null チェックを忘れやすい)
User findUser(int id) {
if (found) return user;
return null; // ← 呼び出し側が .getName() を呼んだら即 NPE
}
// Optional を返すメソッド(「値がない可能性」が型で表現される)
Optional<User> findUser(int id) {
if (found) return Optional.of(user);
return Optional.empty(); // ← 呼び出し側は「empty かも」と意識せざるを得ない
}
Optional の生成
Optional<String> opt1 = Optional.of("hello"); // 値あり(null を渡すと即 NPE!)
Optional<String> opt2 = Optional.ofNullable("hi"); // 値あり
Optional<String> opt3 = Optional.ofNullable(null); // empty になる(NPE にならない)
Optional<String> opt4 = Optional.empty(); // 空(値なし)
Optional.of(null) は即 NPE。「null かもしれない値」には Optional.ofNullable() を使う。
値の取り出しメソッド ─ 全種類
Optional<String> opt = Optional.ofNullable(null); // empty の例
// ── 安全な取り出し ──
String v1 = opt.orElse("デフォルト"); // empty → "デフォルト"
String v2 = opt.orElseGet(() -> "生成した値"); // empty → Supplier の結果
String v3 = opt.orElseThrow(); // empty → NoSuchElementException
String v4 = opt.orElseThrow(
() -> new IllegalStateException("値がない")); // empty → 指定例外
// ── 値があるかどうか確認してから取り出し ──
if (opt.isPresent()) {
System.out.println(opt.get()); // 値あり → 取り出し(empty で get() → NoSuchElementException)
}
opt.ifPresent(s -> System.out.println("あり: " + s)); // 値あり時のみ実行
// ifPresentOrElse(Java 9+)
opt.ifPresentOrElse(
s -> System.out.println("あり: " + s),
() -> System.out.println("なし")
);
// isEmpty(Java 11+)
if (opt.isEmpty()) {
System.out.println("値がない");
}
| メソッド | 値あり | empty のとき |
|---|---|---|
get() | 値を返す | NoSuchElementException |
orElse(T) | 値を返す | 引数の値を返す(引数は常に評価) |
orElseGet(Supplier) | 値を返す | Supplier を呼んで返す(lazyに評価) |
orElseThrow() | 値を返す | NoSuchElementException |
orElseThrow(Supplier) | 値を返す | Supplier の例外を投げる |
isPresent() | true | false |
isEmpty() | false | true(Java 11+) |
orElse vs orElseGet ─ 評価タイミングの違い(試験頻出)
// orElse: 引数の式は値があっても必ず評価される
Optional<String> present = Optional.of("value");
String result1 = present.orElse(createDefault()); // createDefault() が呼ばれる!(値があっても)
// orElseGet: 引数の Supplier は empty のときだけ呼ばれる(遅延評価)
String result2 = present.orElseGet(() -> createDefault()); // createDefault() は呼ばれない
String createDefault() {
System.out.println("デフォルト生成"); // コストのかかる処理
return "default";
}
コストのかかるデフォルト値の生成には orElseGet を使う。orElse は常に評価されるので、「値があるときもデフォルト生成のコストを払う」ことになる。
Optional のパイプライン
map, filter, flatMap が使える。Stream と同じイメージ。
Optional<String> opt = Optional.of(" hello world ");
String result = opt
.map(String::trim) // Optional["hello world"]
.filter(s -> s.length() > 5) // Optional["hello world"](11文字 > 5)
.map(String::toUpperCase) // Optional["HELLO WORLD"]
.orElse("なし"); // "HELLO WORLD"
System.out.println(result); // "HELLO WORLD"
// flatMap: Optional<Optional<T>> のネストを防ぐ
Optional<String> opt2 = Optional.of("123");
Optional<Integer> num = opt2.flatMap(s -> {
try { return Optional.of(Integer.parseInt(s)); }
catch (NumberFormatException e) { return Optional.empty(); }
});
Optional の設計上の注意点
// NG: フィールドに Optional を使わない(シリアライゼーション問題など)
class User {
private Optional<String> nickname; // 非推奨
}
// NG: メソッドの引数に Optional を使わない(呼び出し元が扱いにくい)
void process(Optional<String> value) { ... } // 非推奨
// OK: メソッドの戻り値として使う
Optional<User> findById(int id) { ... } // これが本来の用途
✏️ 練習問題
問題 1: Stream の出力
List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.stream()
.filter(n -> n % 3 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
答え
[9, 36, 81]
filter(n -> n % 3 == 0): 3 の倍数 → 3, 6, 9map(n -> n * n): 二乗 → 9, 36, 81collect(Collectors.toList()): リストに収集
問題 2: 空 Stream の match 系
System.out.println(Stream.empty().allMatch(x -> true));
System.out.println(Stream.empty().anyMatch(x -> true));
System.out.println(Stream.empty().noneMatch(x -> false));
答え
true
false
true
allMatch: 空 Stream は「全員 OK」→true(vacuous truth)anyMatch: 空 Stream は「1 つも条件を満たす要素なし」→falsenoneMatch: 空 Stream は「条件を満たす要素が 1 つもない」→true
問題 3: Optional の動作
Optional<String> opt = Optional.empty();
System.out.println(opt.orElse("A"));
System.out.println(opt.orElseGet(() -> "B"));
System.out.println(opt.isPresent());
System.out.println(opt.isEmpty()); // Java 11+
答え
A
B
false
true
問題 4: IntStream の range
System.out.println(IntStream.range(1, 5).sum());
System.out.println(IntStream.rangeClosed(1, 5).sum());
答え
10
15
range(1, 5): 1, 2, 3, 4(5 を含まない)→ 合計 10rangeClosed(1, 5): 1, 2, 3, 4, 5(5 を含む)→ 合計 15
Chapter 08 チェックリスト
- Stream の 3 性質: 遅延評価・使い捨て・元データ不変
- 中間操作は終端操作が呼ばれるまで実行されない(遅延評価)
- 終端操作後の Stream を再利用すると
IllegalStateException -
filterはPredicate、mapはFunction、forEachはConsumerを引数に取る -
map: 1 要素 → 1 要素の変換、flatMap: 1 要素 → 複数要素(展開して平坦化) -
sorted()は自然順序、sorted(Comparator)はカスタム順序 -
distinct,limit,skipの動作 -
collect(Collectors.toList())/toSet()/toMap()/joining()/groupingBy()/partitioningBy()が使える -
toMapでキーが重複するとIllegalStateException(第 3 引数の merge 関数で解決) -
joining(delimiter, prefix, suffix)の引数順 -
reduceの初期値あり(Tを返す)vs 初期値なし(Optional<T>を返す) -
findFirst/findAny/anyMatch/allMatch/noneMatchは短絡操作 - 空 Stream の
allMatchはtrue(vacuous truth) - 空 Stream の
anyMatchはfalse -
IntStream.range(a, b)は b を含まない、rangeClosed(a, b)は b を含む -
mapToIntでStream<Integer>→IntStream、boxed()でIntStream→Stream<Integer> -
IntStreamはsum(),average(),min(),max()を直接持つ -
Optional.of(null)は即 NPE、Optional.ofNullable(null)は empty -
orElse(T)は値があっても引数を常に評価する -
orElseGet(Supplier)は empty のときだけ Supplier を呼ぶ(遅延評価) -
opt.get()を empty の Optional に使うとNoSuchElementException -
Optionalはメソッドの戻り値型として使うのが本来の用途