コンテンツにスキップ

9-3. コレクション・ストリーム・ラムダ

  • List・Map・Set の特徴と使い分けを理解する
  • ジェネリクスによる型安全なコレクションの扱い方を学ぶ
  • ラムダ式と関数型インターフェースで処理を簡潔に書けるようになる
  • Stream API を使った宣言的なデータ処理パターンを習得する
  • Optional による null 安全な値の扱い方を理解する

配列は要素数が固定だが、実務ではデータ数が動的に変わることが多い。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() // 3
fruits.contains("banana") // true
fruits.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") // 95
scores.getOrDefault("Carol", 0) // 0(存在しないキーのデフォルト値)
scores.containsKey("Bob") // true
scores.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") // true
tags.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}

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つだけのインターフェース)
@FunctionalInterface
interface 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"

既存のメソッドをラムダの代わりに使える短い書き方。

// ラムダ式
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()); // コンストラクタ参照

コレクションを宣言的に処理するAPI。JavaScriptの mapfilterreduce に相当する。

Stream の処理フロー
コレクション → stream() → 中間操作(0個以上) → 終端操作(1個)
(遅延評価) (ここで初めて実行)
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 → collect
List<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 / noneMatch
numbers.stream().anyMatch(n -> n > 9) // true
numbers.stream().allMatch(n -> n > 0) // true
numbers.stream().noneMatch(n -> n < 0) // true
// count
long evenCount = numbers.stream().filter(n -> n % 2 == 0).count(); // 5
// sorted
List<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() が読みやすい。

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 と filter
Optional<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 APIfilter/map/reduce で宣言的にコレクションを処理
Optionalnull の代わりに「値があるかもしれない」を型で表現