受け渡しできる振る舞い
ラムダ式は、数値や文字列と同じように、メソッドへ渡したり、変数に格納したり、戻り値として返したりできるコンパクトなコードのかたまりです。ラムダが登場する前は、振る舞いを渡すために、たった1つのメソッドを包むだけのためにクラスまるごと(あるいは冗長な匿名クラス)を書く必要がありました。ラムダはそれを本質、つまり引数と本体だけに削ぎ落とします。
ラムダは常に関数型インターフェース、すなわち抽象メソッドをちょうど1つだけ持つインターフェースを実装します(インターフェースのページの最後で出会いました)。コンパイラは文脈からどのインターフェースを指しているかを判断し、あなたのラムダをその唯一のメソッドに対応づけます。
x -> x * 2 が apply の実装そのものです。new もクラス本体もメソッド名もありません。それらはすべてインターフェースが用意してくれます。
アロー構文
すべてのラムダは 引数 -> 本体 という形をしています。必要な分だけ、各部分は柔軟に変わります。
() -> 42 // 引数なし
x -> x + 1 // 引数が1つ、かっこは省略可
(x, y) -> x + y // 引数が2つ以上ならかっこが必要
(int x, int y) -> x + y // 型は省略可 - 通常は推論される
x -> { // ブロック本体には波かっこと return が必要
int doubled = x * 2;
return doubled + 1;
}
単一の式からなる本体(x -> x + 1)は、その値を暗黙的に返します。return キーワードは不要です。波かっこを使った瞬間、通常のブロックを書いていることになり、メソッドが何かを返すなら明示的に return しなければなりません。よくある間違いは両者を混ぜることです。x -> { x + 1 } はコンパイルできません。ブロックには文(return x + 1;)が必要だからです。
ラムダは匿名クラスを置き換える
ラムダが何をもたらすのかを最もはっきり示すのは、ビフォー/アフターです。独自の Comparator を使った並べ替えは、かつてこのように書いていました。
// ビフォー - 匿名クラス
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
同じことをラムダにすると、読みやすい1行になります。
Comparator は関数型インターフェース(唯一の抽象メソッドが compare)なので、ラムダがそのままはまります。new、クラス、メソッドシグネチャといった形式的な部分はすべて消え、比較ロジックだけが残ります。
java.util.function のツールボックス
自分で関数型インターフェースを宣言する必要はめったにありません。java.util.function パッケージには代表的な形が用意されており、Javaのライブラリ API のほとんどがそれらを受け付けます。
Function<T, R>-Tを受け取り、Rを返す(apply)Predicate<T>-Tを受け取り、booleanを返す(test)Supplier<T>- 何も受け取らず、Tを返す(get)Consumer<T>-Tを受け取り、何も返さない(accept)
これらはジェネリックなインターフェースです。Function<String, Integer> は、前のページで見たジェネリクスを再利用して型安全性を保ちます。コードが受け取り、生み出す必要があるものに合った形のものを選びましょう。
メソッド参照
ラムダが既存の1つのメソッドを呼び出すだけなら、:: を使ってメソッド参照に置き換えられます。同じ値を、より直接的に書いたものです。
メソッド参照にはいくつかの種類があります。String::toUpperCase(各引数に対して呼び出されるインスタンスメソッド)、Math::max(静的メソッド)、System.out::println(特定のオブジェクトのメソッド)、ArrayList::new(コンストラクタ)です。明快に読めるときだけ使いましょう。どの形が当てはまるか考え込むようなら、普通のラムダで十分です。
変数のキャプチャ
ラムダは外側のメソッドのローカル変数を使えますが、それがfinal または実質的に final、つまり一度だけ代入され二度と変更されない場合に限られます。Javaはラムダが作られた時点の値をキャプチャするため、あとから変わりうる変数はあいまいになってしまいます。
factor をどこかで再代入すると、コンパイラは「variable used in lambda expression should be final or effectively final」というエラーでラムダを拒否します。本当に共有する可変状態が必要なときは、代わりにオブジェクト(フィールド、配列の要素、または AtomicInteger)をキャプチャしてください。中身が変わっても参照自体は固定されたままだからです。なお、ラムダは匿名クラスと違って独自のスコープを作りません。ラムダ内の this は、ラムダ自身ではなく、外側のインスタンスを指します。
次へ: Streams
ラムダは構成要素であって、目的地ではありません。その真価は Streams API で発揮されます。そこでは filter、map、reduce のような操作(それぞれがラムダを受け取ります)を連ねて、データ変換を、ループの絡まりではなく読みやすいパイプラインとして表現します。それが次のページです。
よくある質問
Javaのラムダ式とは何ですか?
ラムダは、関数型インターフェース(抽象メソッドを1つだけ持つインターフェース)のインスタンスを短く書く方法です。匿名クラスをまるごと書く代わりに、引数 -> 本体 と書きます。コンパイラはラムダをインターフェースの唯一のメソッドに対応づけるので、Runnable r = () -> System.out.println("hi"); は完全な Runnable になります。ラムダを使うと、振る舞いをデータのように受け渡しできます。
Javaにおけるラムダとメソッド参照の違いは何ですか?
どちらも関数型インターフェースのインスタンスを生成します。ラムダには明示的な引数と本体があり(s -> s.toUpperCase())、メソッド参照は既存の1つのメソッドを呼び出すだけのラムダを短縮した記法です(String::toUpperCase)。ラムダが引数を名前付きの1つのメソッドへ渡すだけなら、メソッド参照を使いましょう。より短く、読みやすくなります。
Javaのラムダで使う変数が final または実質的に final でなければならないのはなぜですか?
ラムダは外側のメソッドのローカル変数をキャプチャできますが、代入後に一度も変更されない場合に限られます。これが「実質的に final」の意味です。Javaは変数への生きた参照ではなく値をキャプチャするため、再代入を許すとあいまいになり、スレッド間で安全でなくなります。共有する可変状態が必要な場合は、代わりにフィールドや配列、AtomicInteger のようなラッパーを使ってください。