Menu

Javaのラムダ式: 簡潔な関数型コード

Javaのラムダ式とは何か、アロー構文、関数型インターフェースをどう実装するか、メソッド参照、そして変数のキャプチャについて。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

受け渡しできる振る舞い

ラムダ式は、数値や文字列と同じように、メソッドへ渡したり、変数に格納したり、戻り値として返したりできるコンパクトなコードのかたまりです。ラムダが登場する前は、振る舞いを渡すために、たった1つのメソッドを包むだけのためにクラスまるごと(あるいは冗長な匿名クラス)を書く必要がありました。ラムダはそれを本質、つまり引数と本体だけに削ぎ落とします。

ラムダは常に関数型インターフェース、すなわち抽象メソッドをちょうど1つだけ持つインターフェースを実装します(インターフェースのページの最後で出会いました)。コンパイラは文脈からどのインターフェースを指しているかを判断し、あなたのラムダをその唯一のメソッドに対応づけます。

x -> x * 2apply の実装そのものです。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 で発揮されます。そこでは filtermapreduce のような操作(それぞれがラムダを受け取ります)を連ねて、データ変換を、ループの絡まりではなく読みやすいパイプラインとして表現します。それが次のページです。

よくある質問

Javaのラムダ式とは何ですか?

ラムダは、関数型インターフェース(抽象メソッドを1つだけ持つインターフェース)のインスタンスを短く書く方法です。匿名クラスをまるごと書く代わりに、引数 -> 本体 と書きます。コンパイラはラムダをインターフェースの唯一のメソッドに対応づけるので、Runnable r = () -> System.out.println("hi"); は完全な Runnable になります。ラムダを使うと、振る舞いをデータのように受け渡しできます。

Javaにおけるラムダとメソッド参照の違いは何ですか?

どちらも関数型インターフェースのインスタンスを生成します。ラムダには明示的な引数と本体があり(s -> s.toUpperCase())、メソッド参照は既存の1つのメソッドを呼び出すだけのラムダを短縮した記法です(String::toUpperCase)。ラムダが引数を名前付きの1つのメソッドへ渡すだけなら、メソッド参照を使いましょう。より短く、読みやすくなります。

Javaのラムダで使う変数が final または実質的に final でなければならないのはなぜですか?

ラムダは外側のメソッドのローカル変数をキャプチャできますが、代入後に一度も変更されない場合に限られます。これが「実質的に final」の意味です。Javaは変数への生きた参照ではなく値をキャプチャするため、再代入を許すとあいまいになり、スレッド間で安全でなくなります。共有する可変状態が必要な場合は、代わりにフィールドや配列、AtomicInteger のようなラッパーを使ってください。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める