1. はじめに
正直に言うと、たった数行のコードのために無名クラスを書くのは、パン屋から店までパンを一つ運ぶのに巨大なトラックを雇うようなものです。
例えば、文字列のリストを長さで並べ替えたい場合、Java 8 以前は次のように書く必要がありました:
List<String> list = Arrays.asList("apple", "banana", "kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
単純な課題なのにコードは画面の半分を占めてしまいます。特にこの手の処理が多いと煩雑で本質が見えにくくなります。開発者は嘆き、そしてラムダ式が生まれました。すでに少し学びましたが、ここでは復習しつつ理解を深めます。
ラムダ式とは
ラムダ式は、関数型インターフェース(抽象メソッドを1つだけ持つインターフェース。例えば Comparator、Runnable、Consumer など)の実装を簡潔に記述するための構文です。
要するに、必要な場所でその場で関数を書けるので、別クラスや別メソッドを宣言する必要がありません。
基本構文:
(parameters) -> { body }
例:
- (a, b) -> a + b — 2 つの数を加算する関数
- x -> x * x — 平方にする関数
- () -> System.out.println("Hello!") — 引数なしの関数
関数型インターフェースとの関係:
ラムダ式は、関数型インターフェース型の変数に代入したり、そのようなインターフェースを受け取るメソッドの引数として渡したりできます。
2. ラムダ式の構文
引数なし
Runnable r = () -> System.out.println("こんにちは、世界!");
r.run(); // 出力: こんにちは、世界!
引数が 1 つ
引数が 1 つの場合、かっこは省略できます:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java はすごい!");
複数引数
かっこは必須です:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
本体が単一の式
本体が 1 つの式だけなら、波かっこや return は不要です:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
ブロック本体
複数の文が必要な場合は波かっこを使い、戻り値があるなら return を書きます:
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
パラメーターの型
多くの場合、パラメーター型は省略できます(コンパイラが文脈から推論します)。もちろん明示することも可能です:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
戻り値のないラムダ
インターフェースが void を返す場合は、文だけを書きます:
list.forEach(s -> System.out.println("要素: " + s));
表: ラムダ式の書き方のバリエーション
| 何をするか | 例 | コメント |
|---|---|---|
| 引数なし | |
例: Runnable など |
| 引数が 1 つ | |
かっこ省略可 |
| 複数引数 | |
かっこ必須 |
| 単一式 | |
return と波かっこ不要 |
| コードブロック | |
結果がある場合は return |
3. 用途: ラムダ式はどこでどう使うか
ラムダ式は、引数として「振る舞い」— 関数 — を渡したい場面で最もよく使われます。これはコレクションやストリーム(Stream API)、イベント処理などに革命をもたらしました。
リストのソート
Java 8 以前:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
ラムダ式を使うと:
list.sort((a, b) -> a.length() - b.length());
スレッド(Thread)の作成
Thread t = new Thread(() -> System.out.println("スレッドが開始されました!"));
t.start();
コレクションの処理
list.forEach(s -> System.out.println(s.toUpperCase()));
リストのフィルタリング
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
例: 学習用アプリを発展させる
ユーザーのリストがあるとします:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
名前が 4 文字より長いユーザーをすべて出力する:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("ユーザー: " + name));
4. ラムダ式における変数のスコープ
ラムダ式は外側のメソッドの変数を使用できますが、注意点があります。
変数とラムダ
Java のラムダは、初期化後に変更されない変数だけを「キャプチャ」できます。変数が final なら明らかです。さらに、final と書かれていなくても、コンパイラは値が変更されるかどうかをチェックし、変更されないなら「実質的に final」とみなしてラムダでの使用を許可します.
例:
int minLength = 4; // 値はどこでも変更されない
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
ここでは問題ありません。minLength は同じ値のままです。
しかし、ラムダで使用した後に minLength に再代入しようとすると、コンパイルエラーになります:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // エラー! ラムダがその値をすでに「固定」しています
要点はシンプルです: ラムダに取り込む変数は不変である必要があります。
無名クラスとの違い
無名クラスとラムダ式では、外側のメソッドの変数の扱いは同じで、final/実質的 final のみが使用可能です。
注意!
ラムダ式では this は外側のオブジェクト(現在のクラスのインスタンス)を指しますが、無名クラスでは無名クラス自身のインスタンスを指します。ラムダ内で現在のクラスのフィールドやメソッドにアクセスする場合に重要な違いです。
例:
public class Example {
String name = "外部クラス";
void demo() {
Runnable r1 = new Runnable() {
String name = "無名クラス";
@Override
public void run() {
System.out.println(this.name); // "無名クラス"
}
};
Runnable r2 = () -> System.out.println(this.name); // "外部クラス"
r1.run();
r2.run();
}
}
5. ラムダ式でよくあるミス
エラー1: final/実質的 final でない変数の使用。 ラムダで使用した後に変数を変更しようとすると、コンパイラはすぐにエラーにします。安全性のためで、そうでなければどの値を使うべきかが不明確になるからです。
エラー2: this の取り違え。 ラムダ式における this は外側のクラスを指し、無名クラスでは無名クラス自身を指します。ラムダから外側のクラスのメソッドを呼び出すのは問題ありませんが、無名クラスから同じつもりで呼ぶと期待通りのコンテキストにはなりません。
エラー3: コンテキストのないラムダ。 ラムダ式はそれ単体では使えません。関数型インターフェース型の変数に代入するか、その型が期待される場所へ渡す必要があります。単に x -> x + 1 とだけ書いても文脈がなければエラーになります。
エラー4: 複雑すぎるラムダ。 ラムダ式が 3〜5 行を超えるようになったら、可読性が落ちます。その場合はロジックを別メソッドに切り出す方がよいでしょう。
GO TO FULL VERSION