中級 60分 Lesson 8

Stream API・Optional

Stream の3性質・中間/終端操作・遅延評価・map/flatMap・collect詳解・IntStream・Optional の全メソッドと罠

Java Java Silver SE21 Stream API Optional IntStream

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, sortedStream<T>○(チェーン可)
終端操作collect, forEach, countStream 以外✗(1回だけ)

8-2. Stream の 3 つの重要な性質

性質 1: 遅延評価(Lazy Evaluation)

中間操作は終端操作が呼ばれるまで一切実行されない。パイプラインを組み立てても、collectforEach が来るまでは「設計図を書いただけ」の状態。

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 の allMatchtrue になるのを知らなかった」という声が多い。

なぜ 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()truefalse
isEmpty()falsetrue(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]

  1. filter(n -> n % 3 == 0): 3 の倍数 → 3, 6, 9
  2. map(n -> n * n): 二乗 → 9, 36, 81
  3. collect(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 つも条件を満たす要素なし」→ false
  • noneMatch: 空 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 を含まない)→ 合計 10
  • rangeClosed(1, 5): 1, 2, 3, 4, 5(5 を含む)→ 合計 15

Chapter 08 チェックリスト

  • Stream の 3 性質: 遅延評価・使い捨て・元データ不変
  • 中間操作は終端操作が呼ばれるまで実行されない(遅延評価)
  • 終端操作後の Stream を再利用すると IllegalStateException
  • filterPredicatemapFunctionforEachConsumer を引数に取る
  • 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 の allMatchtrue(vacuous truth)
  • 空 Stream の anyMatchfalse
  • IntStream.range(a, b) は b を含まない、rangeClosed(a, b) は b を含む
  • mapToIntStream<Integer>IntStreamboxed()IntStreamStream<Integer>
  • IntStreamsum(), average(), min(), max() を直接持つ
  • Optional.of(null) は即 NPE、Optional.ofNullable(null) は empty
  • orElse(T) は値があっても引数を常に評価する
  • orElseGet(Supplier) は empty のときだけ Supplier を呼ぶ(遅延評価)
  • opt.get() を empty の Optional に使うと NoSuchElementException
  • Optional はメソッドの戻り値型として使うのが本来の用途