1. ラムダ式とコレクション
ラムダ式が登場する前、コレクションの処理がどのようだったかを思い出しましょう。文字列のリストがあり、それらを画面に表示したいとします:
List<String> list = Arrays.asList("cat", "dog", "ox");
for (String s : list) {
System.out.println(s);
}
簡単です。しかし例えば、リストから空文字列をすべて削除したい場合は条件付きのループを書かねばならず、場合によってはイテレータを使う必要もあります(そうしないと ConcurrentModificationException が発生します)。また、文字列の長さでソートしたいときもあります:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
こんなに簡単な課題でも、すでに5行のコードとノイズの多い括弧だらけ。どう最適化するか? 答えはご存じのとおり: ラムダ式です。
コレクションのメソッドにおけるラムダ式の適用
Java 8 以降、コレクションのインターフェースに関数型インターフェースを受け取る新しいメソッドが追加されました — つまり、そこにラムダ式を渡せます。代表的なものは次のとおりです:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
例: forEach
リストの全要素を表示(従来のやり方):
for (String s : list) {
System.out.println(s);
}
ではラムダで:
list.forEach(s -> System.out.println(s));
さらに短く、いわば「Java の達人ごっこ」をしたいなら:
list.forEach(System.out::println); // method reference。後で解説します
例: removeIf
空文字列をすべて削除:
List<String> animals = new ArrayList<>(Arrays.asList("cat", "", "dog", "ox", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [cat, dog, ox]
例: sort
文字列の長さでソート:
List<String> animals = Arrays.asList("cat", "dog", "ox", "elephant");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [ox, cat, dog, elephant]
例: replaceAll
すべて大文字に変換:
List<String> animals = new ArrayList<>(Arrays.asList("cat", "dog", "ox"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [CAT, DOG, OX]
2. Stream API とラムダ式
Java 8 で Stream API が登場しました — 関数型スタイルでコレクションを処理する強力な手段です。ストリームはフィルタ、変換、ソート、収集をメソッド連鎖で行えます。これらのメソッドはすべてラムダ式を受け取ります。
重要: Stream API の詳細は後ほど扱います。ここでは、ラムダの役割を理解するための基本例だけを示します。
例: フィルタリング
3 文字より長い文字列だけを残す:
List<String> animals = Arrays.asList("cat", "elephant", "ox", "crocodile");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // elephant, crocodile
例: 変換(map)
すべての文字列を大文字にする:
List<String> animals = Arrays.asList("cat", "elephant", "ox");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [CAT, ELEPHANT, OX]
例: ソート
(元のリストを変更せずに)長さ順で並べ替えたリストを得る:
List<String> animals = Arrays.asList("cat", "elephant", "ox", "crocodile");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [ox, cat, elephant, crocodile]
3. 匿名クラスとの比較
同じコードが匿名クラスとラムダ式でどう見えるか比べてみましょう。
文字列の長さでのソート
匿名クラス:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
ラムダ式:
animals.sort((a, b) -> a.length() - b.length());
結論:
ラムダ式は行数を大幅に節約し、コードをより読みやすくします。括弧もノイズも減り、要点が見えます。
空文字列の削除
匿名クラス:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
ラムダ式:
animals.removeIf(s -> s.isEmpty());
4. 実践: ラムダ式で解く短い課題
ユーザーのリストを扱うミニプログラムで、ラムダ式を実際に使ってみましょう。
例 1: ユーザーを年齢でフィルタ
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
例 2: ユーザーを名前でソート
List<User> users = Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alice (17), Bob (25), Charlie (15)]
例 3: ユーザーのリストを名前のリストに変換
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]
例 4: 未成年をすべて削除
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alice", 17),
new User("Bob", 25),
new User("Charlie", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. 役に立つ細かな注意点
スコープと「実質的に final」変数の注意点
ラムダ式は外側のメソッドにある変数を利用できますが、それは final または「実質的に final」(初期化後に変更されない)である必要があります。
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
この minAge をラムダで使った後に変更しようとすると、コンパイラはエラーを出します。
表: ラムダ式とともに使う主なコレクション/ストリームのメソッド
| コレクション/ストリームのメソッド | 概要 | ラムダの型 | 例 |
|---|---|---|---|
|
各要素に適用 | |
|
|
条件に合う要素を削除 | |
|
|
要素をソート | |
|
|
各要素を置換 | |
|
|
ストリームをフィルタ | |
|
|
要素を変換 | |
|
|
ストリームを走査 | |
|
|
ストリーム内でソート | |
|
7. ありがちなミス
誤り No.1: ラムダが長すぎる。 ラムダ式の中がすでに5行のコード、条件分岐、ループ、try-catch になっているなら、そのコードは別メソッドに切り出すべきでしょう。ラムダは短いロジックに向いています。
誤り No.2: 変更可能な変数を使ってしまう。 ラムダの中で外側のメソッドの変数(たとえばカウンタ)を変更しようとすると、コンパイラに拒否されます。変数は final か、実質的に final である必要があります。
誤り No.3: コレクション/ストリームのメソッドが元のコレクションを常に変更するとは限らないことを忘れる。 例えば、stream().filter(...) は元のリストを変更せず、新しいストリームを返します。コレクションが欲しいなら collect(Collectors.toList()) を使ってください。
誤り No.4: ラムダ式の型が一致しない。 例えばメソッドが Comparator<T> を受け取るのに、引数が1つ(本来は2つ)しかないラムダを渡してしまうと、コンパイルエラーになります.
誤り No.5: ラムダのネストで可読性を損なう。 map、filter、forEach のチェーンの中で、それぞれにさらに別のラムダがあると、コードは読みにくくなります。そういう場合は、式を段階に分けるか、一部をメソッドに切り出すとよいでしょう。
GO TO FULL VERSION