9-3. コレクション・ストリーム・ラムダ
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”- List・Map・Set の特徴と使い分けを理解する
- ジェネリクスによる型安全なコレクションの扱い方を学ぶ
- ラムダ式と関数型インターフェースで処理を簡潔に書けるようになる
- Stream API を使った宣言的なデータ処理パターンを習得する
- Optional による null 安全な値の扱い方を理解する
コレクションフレームワーク
Section titled “コレクションフレームワーク”配列は要素数が固定だが、実務ではデータ数が動的に変わることが多い。JavaはそのためのコレクションAPIを標準で提供している。
主要なコレクションの構造 Iterable │ Collection ┌──────────┼──────────┐ List Set Queue │ │ ArrayList HashSet LinkedList LinkedHashSet TreeSet(ソート済み)
Map(Collectionの外) ├── HashMap ├── LinkedHashMap(挿入順を保持) └── TreeMap(キーがソート済み)順序を保持し、重複を許す。
import java.util.ArrayList;import java.util.List;
List<String> fruits = new ArrayList<>();fruits.add("apple");fruits.add("banana");fruits.add("cherry");
fruits.get(0) // "apple"fruits.size() // 3fruits.contains("banana") // truefruits.remove("banana") // 削除fruits.indexOf("cherry") // 1(削除後のインデックス)
// 初期化(Java 9以降)List<String> immutable = List.of("a", "b", "c"); // 変更不可List<String> mutable = new ArrayList<>(List.of("a", "b", "c")); // 変更可
// ループfor (String fruit : fruits) { System.out.println(fruit);}キーと値のペアを管理する。キーは重複不可。
import java.util.HashMap;import java.util.Map;
Map<String, Integer> scores = new HashMap<>();scores.put("Alice", 90);scores.put("Bob", 85);scores.put("Alice", 95); // 上書きされる
scores.get("Alice") // 95scores.getOrDefault("Carol", 0) // 0(存在しないキーのデフォルト値)scores.containsKey("Bob") // truescores.size() // 2
// 全エントリのループfor (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue());}
// 初期化(Java 9以降)Map<String, Integer> map = Map.of("a", 1, "b", 2); // 変更不可重複を許さないコレクション。
import java.util.HashSet;import java.util.Set;
Set<String> tags = new HashSet<>();tags.add("java");tags.add("spring");tags.add("java"); // 重複は無視されるtags.size() // 2
tags.contains("java") // truetags.remove("java");
// 2つのSetの集合演算Set<Integer> a = new HashSet<>(Set.of(1, 2, 3, 4));Set<Integer> b = new HashSet<>(Set.of(3, 4, 5, 6));a.retainAll(b); // aをa∩b(積集合)に変更 → {3, 4}ジェネリクス
Section titled “ジェネリクス”List<String> の <String> の部分がジェネリクス。型を汎用的にしながら型安全性を保てる。
// ジェネリクスなし(古い書き方):実行時に ClassCastException が起きる可能性List list = new ArrayList();list.add("hello");list.add(42); // 型が混在してしまうString s = (String) list.get(1); // ClassCastException!
// ジェネリクスあり:コンパイル時に型チェックされるList<String> typedList = new ArrayList<>();typedList.add("hello");typedList.add(42); // コンパイルエラー:型が合わないメソッドを引数として渡すための簡潔な書き方(第8章のアロー関数に相当)。
// 関数型インターフェース(抽象メソッドが1つだけのインターフェース)@FunctionalInterfaceinterface Transformer { int transform(int n);}
// 従来の書き方(匿名クラス)Transformer doubler = new Transformer() { @Override public int transform(int n) { return n * 2; }};
// ラムダ式:(引数) -> 処理Transformer doubler2 = n -> n * 2;
System.out.println(doubler2.transform(5)); // 10よく使う標準の関数型インターフェース
Section titled “よく使う標準の関数型インターフェース”import java.util.function.*;
// Function<T, R>:T を受け取り R を返すFunction<String, Integer> strLen = s -> s.length();strLen.apply("hello") // 5
// Predicate<T>:T を受け取り boolean を返す(フィルタ条件)Predicate<Integer> isEven = n -> n % 2 == 0;isEven.test(4) // true
// Consumer<T>:T を受け取り何も返さない(副作用のある処理)Consumer<String> printer = s -> System.out.println(s);printer.accept("hello"); // "hello" を出力
// Supplier<T>:何も受け取らず T を返す(値の生成)Supplier<String> greeting = () -> "こんにちは!";greeting.get() // "こんにちは!"
// BiFunction<T, U, R>:T と U を受け取り R を返すBiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);repeat.apply("abc", 3) // "abcabcabc"メソッド参照
Section titled “メソッド参照”既存のメソッドをラムダの代わりに使える短い書き方。
// ラムダ式list.forEach(s -> System.out.println(s));list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());
// メソッド参照(同じ意味)list.forEach(System.out::println); // インスタンスメソッド(特定インスタンス)list.stream().map(String::toUpperCase).collect(Collectors.toList()); // インスタンスメソッド(任意インスタンス)list.stream().map(Integer::parseInt).collect(Collectors.toList()); // staticメソッドlist.stream().map(String::new).collect(Collectors.toList()); // コンストラクタ参照Stream API
Section titled “Stream API”コレクションを宣言的に処理するAPI。JavaScriptの map・filter・reduce に相当する。
Stream の処理フローコレクション → stream() → 中間操作(0個以上) → 終端操作(1個) (遅延評価) (ここで初めて実行)基本的な使い方
Section titled “基本的な使い方”import java.util.List;import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter → map → collectList<Integer> result = numbers.stream() .filter(n -> n % 2 == 0) // 偶数だけ .map(n -> n * n) // 二乗する .collect(Collectors.toList()); // Listに集める// [4, 16, 36, 64, 100]
// reduce:合計int sum = numbers.stream() .reduce(0, Integer::sum); // 55
// anyMatch / allMatch / noneMatchnumbers.stream().anyMatch(n -> n > 9) // truenumbers.stream().allMatch(n -> n > 0) // truenumbers.stream().noneMatch(n -> n < 0) // true
// countlong evenCount = numbers.stream().filter(n -> n % 2 == 0).count(); // 5
// sortedList<String> sorted = List.of("banana", "apple", "cherry").stream() .sorted() .collect(Collectors.toList());// ["apple", "banana", "cherry"]
// distinct:重複除去List<Integer> unique = List.of(1, 2, 2, 3, 3, 3).stream() .distinct() .collect(Collectors.toList());// [1, 2, 3]オブジェクトのコレクション操作
Section titled “オブジェクトのコレクション操作”実務でよく使うパターン。
record Book(String title, String author, int price, String genre) {}
List<Book> books = List.of( new Book("Java入門", "田中", 2800, "tech"), new Book("Spring実践", "佐藤", 3200, "tech"), new Book("SQL基礎", "鈴木", 2500, "tech"), new Book("小説ABC", "高橋", 1200, "novel"), new Book("Python応用", "伊藤", 3500, "tech"));
// techジャンルのタイトル一覧List<String> techTitles = books.stream() .filter(b -> b.genre().equals("tech")) .map(Book::title) .collect(Collectors.toList());
// 平均価格OptionalDouble avgPrice = books.stream() .mapToInt(Book::price) .average();avgPrice.ifPresent(avg -> System.out.printf("平均: %.0f円%n", avg));
// 価格でソートして最も高い本を取得Optional<Book> mostExpensive = books.stream() .max(Comparator.comparingInt(Book::price));
`Comparator` は「何を基準に大小を比べるか」を表す道具である。`Comparator.comparingInt(Book::price)` は「`price` の整数値で比較するルール」を作っている。
// ジャンル別にグルーピングMap<String, List<Book>> byGenre = books.stream() .collect(Collectors.groupingBy(Book::genre));
// ジャンル別の合計金額Map<String, Integer> totalByGenre = books.stream() .collect(Collectors.groupingBy(Book::genre, Collectors.summingInt(Book::price)));
// 条件で2グループに分けるMap<Boolean, List<Book>> byExpensive = books.stream() .collect(Collectors.partitioningBy(book -> book.price() >= 3000));// true には3000円以上、false には3000円未満が入るgroupingBy() と partitioningBy() の違い
Section titled “groupingBy() と partitioningBy() の違い”groupingBy()はキーごとに複数グループへ分けるpartitioningBy()はtrue / falseの2グループへ分ける
「条件を満たすもの / 満たさないもの」で分けたいときは partitioningBy() が読みやすい。
Optional
Section titled “Optional”null の代わりに「値があるかもしれないし、ないかもしれない」を型で表現する。
// Optional の生成Optional<String> present = Optional.of("hello");Optional<String> empty = Optional.empty();Optional<String> nullable = Optional.ofNullable(null); // null OK
// 値の取り出しpresent.get() // "hello"(空なら NoSuchElementException)present.orElse("default") // "hello"(空なら"default")empty.orElse("default") // "default"empty.orElseGet(() -> "生成") // ラムダで遅延生成
// 値があれば処理するpresent.ifPresent(s -> System.out.println(s)); // "hello" を出力empty.ifPresent(s -> System.out.println(s)); // 何も起きない
// map と filterOptional<Integer> len = present.map(String::length); // Optional[5]Optional<String> filtered = present.filter(s -> s.startsWith("h")); // Optional[hello]| 項目 | ポイント |
|---|---|
| List | 順序あり・重複許可。ArrayList が一般的 |
| Map | キー→値のマッピング。HashMap が一般的 |
| Set | 順序なし・重複禁止。HashSet が一般的 |
| ジェネリクス | <T> で型安全なコレクションを使う |
| ラムダ式 | (引数) -> 処理 で関数を値として扱う |
| Stream API | filter/map/reduce で宣言的にコレクションを処理 |
| Optional | null の代わりに「値があるかもしれない」を型で表現 |