1. ラムダ式のミス: 変数キャプチャ
Java のラムダ式は外側のコンテキストの変数を使用できます。ただし制約があります。こうした変数は final または「実質的に final」(effectively final)、すなわち初期化後に変更されない必要があります。
エラーの例
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // コンパイルエラー!
なぜ?
コンパイラはこう指摘します。変数がラムダ内で使われているため、その変数は final または effectively final でなければなりませんが、sum はラムダ内で変更されています。
回避方法
- 外部変数を必要としないストリームの終端操作を使う: mapToInt + sum()。
- どうしても必要な場合は AtomicInteger のようなコンテナや 1 要素の配列を使う(ただしハックに近い)。
int sum = list.stream().mapToInt(Integer::intValue).sum();
たとえ
ラムダは「タイムトラベラー」のようなものだと考えてください。作成時点の変数の値を「記憶」し、その後の変化を観測できません。変更しようとするのは「祖父殺しのパラドックス」と同じで、コンパイラはプログラムをビルドさせてくれません。
2. スコープと this に関するミス
ラムダ式ではキーワード this は匿名クラスではなく外側のオブジェクトを指します(匿名クラスの場合とは異なります)。
例
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // this は Runnable ではなく Example を指す!
};
r.run();
}
}
重要: 匿名クラスからラムダへ書き換えると this の意味が変わります。予期しない結果を避けるため、これを考慮してください。
3. 可変状態(副作用)の問題
関数型アプローチでは副作用の排除が推奨されます。関数は自分の外側の状態を変えず、外部のコレクションや変数をミューテートしません。
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // 副作用!
}
});
このコードは「動き」ますが、予測しづらく、parallelStream() と組み合わせると危険です(レースや例外のリスク)。テストや保守も難しくなります。
正しいやり方: 外部状態を変更せずに新しい結果を明示的に形成する操作を使いましょう。
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. 型と generics のミス
Java は厳密な静的型付け言語です。ラムダやチェーンが複雑すぎると、コンパイラが型をうまく推論できないことがあります。
例
List<Object> objects = List.of(1, "string", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
一見もっともらしく見えますが、わずかなタイプミスや誤ったキャストでコンパイルエラー、あるいは実行時に ClassCastException を引き起こすおそれがあります。
回避策
- 型推論が「つまずく」場合は、明示的な型を追加する。
- <String> を明記したり、ラムダを (String s) -> ... のようにパラメータ化することを恐れない。
- 変換時の型の適合性を確認する。
Optional の典型例
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // 結果は Optional<Integer>
Optional<String> を期待していたのに Optional<Integer> になっているなら、関数が何を返しているかを確認してください。
5. ラムダの副作用と並列処理
並列ストリーム(parallelStream())と副作用の組み合わせは危険です。
例
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // 危険!
何が起きるか?
- データの欠落や重複。
- ConcurrentModificationException や「不可解な」バグ。
正しいやり方
- スレッドセーフなコレクションを使う: ConcurrentLinkedQueue、CopyOnWriteArrayList。
- さらに良いのは、副作用を避けて collect(...) で結果を集めること。
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. 可読性の低下: 「ストリーム・スパゲッティ」と長いチェーン
関数型スタイルは有用ですが、チェーンが「ハイパーマーケットのレシート」のように長くなると話は別です。
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
ヒント:
- チェーンを論理的なブロックに分割する。
- 複雑なラムダは、意味のある名前の別メソッドに切り出す。
- 必要に応じてコメントを追加する — Stream のコードでも。
7. よくない変数・関数名
過度に短い名前(x、y、z)は理解を妨げます。
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
特にラムダが複数行になったり、非自明なロジックを表す場合は、意味のある名前を使いましょう.
8. null と Optional のミス
Stream API と関数型インターフェースは null を好みません。ラムダやストリームに null を渡すことは、NullPointerException のよくある原因です。
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // ドカン! 2番目の要素で NPE
.forEach(System.out::println);
正しいやり方
- null を事前にフィルタする: .filter(Objects::nonNull)。
- 値の欠如を明示するために Optional を使う。
9. 合成関数の戻り型の問題
compose と andThen では、適用順序や期待する型を取り違えやすいです。
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// 動作: 先に parse、次に square
Function<String, Integer> squareThenParse = parse.compose(square);
// エラー! square は Integer を受け取り、parse は String を期待する
教訓: 適用順序と型の整合性を常に確認しましょう。
10. ラムダにおけるチェック例外の問題
java.util.function パッケージの関数型インターフェースはチェック例外(たとえば IOException)をスローできません。ラムダ内でそれらを使う必要がある場合は、例外を手動で処理してください。
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // もしくは別の処理にする
}
};
そうしないと、コンパイラはその関数をストリームやコレクションで使わせてくれません。
GO TO FULL VERSION