ラムダ式の基本

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • ラムダ式 (x) -> x * 2 は「引数 → 戻り値」を 1 行で書く関数オブジェクト
  • 関数型インタフェース (抽象メソッド 1 つ) の代わりに渡せる。Comparator Runnable
  • 外側のローカル変数は 実質 final なら参照できる (キャプチャ)
  • コードが短くなるが、デバッグやスタックトレースで匿名関数として現れる

ラムダ式の基本 とは

Java 8 から導入されたラムダ式の文法と関数型インターフェースの考え方を学び、Stream と組み合わせて配列の合計を一行で書く感覚を身につけよう。

ラムダ式とは ─ 関数を「値」として持ち運ぶ

Java 8 から、それまでクラスやメソッドの中にしか書けなかった処理を、まるで intString のように「値」として変数に入れたり、メソッドの引数として渡したりできるようになりました。それを実現する文法が ラムダ式 (lambda expression) です。

例えば「整数を 2 倍にする処理」を持ち運びたいとします。これまでの Java では、わざわざ Doubler のようなクラスを定義したり、Function 型を実装した 匿名クラス を書いたりする必要がありました。ラムダ式を使うと、n -> n * 2 というたった一行で同じものを表現できます。コードが短くなるだけでなく、「データを受け取って結果を返す」という関数の本質がそのまま見えるようになります。

ラムダ式は単なる文法糖衣 (syntax sugar) ではありません。背後では 関数型インターフェース を実装した Function オブジェクトが生成されています。次の章で詳しく見ますが、Java のラムダは「関数のように見えるオブジェクト」だと押さえておくのが正解です。

ラムダ式が普及したことで、Java は CollectionStream を扱うコードを劇的に短く書けるようになりました。本章ではまずラムダの読み方・書き方を覚え、Stream.mapToInt と組み合わせて配列の合計を求める一行コードを書けるところまで進みます。

匿名クラスからの進化

ラムダ式の気持ちを掴むには、Java 7 以前の書き方と比べるのがいちばん速いです。たとえば配列の合計を Function っぽい手段で求める場合、昔は 匿名クラス (anonymous inner class) を書いていました。

Java

import java.util.function.IntUnaryOperator; public class Old { public static void main(String[] args) { IntUnaryOperator doubler = new IntUnaryOperator() { @Override public int applyAsInt(int n) { return n * 2; } }; System.out.println(doubler.applyAsInt(10)); // 20 } }

new IntUnaryOperator() { ... } という匿名クラスは、ちょっと値を渡すだけなのに 5 行も使います。これを Java 8 のラムダで書き換えると、こうなります。

Java

import java.util.function.IntUnaryOperator; public class Modern { public static void main(String[] args) { IntUnaryOperator doubler = n -> n * 2; System.out.println(doubler.applyAsInt(10)); // 20 } }

なんと本質はたった n -> n * 2 の一行です。new@OverrideapplyAsInt も全部消えました。Java コンパイラが「doublerIntUnaryOperator 型だから、これは applyAsInt(int) の実装だな」と推論して、匿名クラス相当のコードを内部で組み立ててくれます。

匿名クラスとラムダは「同じものを生み出す書き方違い」だと考えると分かりやすいです。コンパイル後のバイトコードは少し違いますが (invokedynamic が使われる)、利用者から見ると振る舞いはほぼ同じです。

ラムダの構文 ─ 矢印 -> の左と右

ラムダ式の基本形は次のとおりです。

Java

(引数リスト) -> 式 または { 文の並び }

左側に「受け取る引数」、右側に「やる処理」を書き、-> (アロー) でつなぎます。具体例を並べてみます。

  • () -> 42 — 引数なし、42 を返す
  • n -> n * 2 — 引数 1 つの場合は () を省略可、式を返す
  • (int n) -> n * 2 — 型を明示してもよい
  • (a, b) -> a + b — 引数 2 つはカンマ区切り
  • n -> { int r = n * n; return r; } — 中括弧を使うときは return が必要

ここでよく出るのが、「式形式」と「ブロック形式」の使い分けです。右側が単一の式なら -> の後にそのまま値を書けば、その値が戻り値になります (return 不要)。一方、複数行の処理を書きたいときは { ... } で囲み、その中で return を明示します。

Java

// 式形式 — return を書かない Function<Integer, Integer> square1 = n -> n * n; // ブロック形式 — return が必須 Function<Integer, Integer> square2 = n -> { int r = n * n; return r; };

見た目は似ていますが、{} を付けた瞬間に文法ルールが切り替わることを覚えてください。{} の中で return を書き忘れるとコンパイルエラーになります。

関数型インターフェース (SAM)

ラムダ式が代入できるのは「関数型インターフェース (functional interface)」と呼ばれる特別なインターフェースだけです。難しそうな名前ですが、定義は単純で「抽象メソッドが 1 つだけのインターフェース」 = SAM (Single Abstract Method) のことを指します。

Java

@FunctionalInterface interface Calc { int apply(int n); } Calc plusOne = n -> n + 1; int result = plusOne.apply(10); // 11

@FunctionalInterface というアノテーションは、コンパイラに「これは関数型インターフェースとして使うので、抽象メソッドが 2 つ以上になったら教えてね」と頼むための印です。付けなくても動きますが、付けておくと事故が減ります。

java.util.function パッケージには、よく使う関数型インターフェースが揃っています。代表的なものは次のとおりです。

  • Function<T, R>T を受け取って R を返す。apply メソッドで呼ぶ
  • Predicate<T>T を受け取って boolean を返す。test で呼ぶ
  • Consumer<T>T を受け取って何も返さない (void)。accept で呼ぶ
  • Supplier<T> — 引数なしで T を返す。get で呼ぶ
  • IntUnaryOperatorintint の特化版。プリミティブで高速
diagram (will load when visible)

ラムダの型は「左辺の型」で決まります。同じ n -> n + 1 でも、左辺が Function<Integer, Integer> なら FunctionIntUnaryOperator なら IntUnaryOperator になります。型推論のおかげで、書く側は中身だけ意識すればよい仕組みです。

Stream と組み合わせて配列の合計

ラムダがもっとも輝くのは、Stream API と組み合わせたときです。今回の課題では、配列を Stream に変換してから mapToInt でラムダを適用し、sum で合計を取り出すという流れを使います。

Java

import java.util.Arrays; public class Sum { public static int lambdaSum(int[] arr) { return Arrays.stream(arr) .boxed() .mapToInt(n -> n) .sum(); } }

Arrays.stream(arr)int[]IntStream に変換します。IntStream には mapToInt がない (もう int の流れだから) ので、.boxed() を一度挟んで Stream<Integer> に持ち上げ、そのうえで mapToInt(n -> n) を呼びます。Stream<Integer>mapToIntInteger から int への ToIntFunction を受け取るので、ラムダ式 n -> n (オートアンボクシングで Integerint) がぴったり合います。最後に sum() で全要素を足し上げて結果を返す、という流れです。

mapToInt(n -> n)n -> n は「受け取った値をそのまま返す」恒等関数のラムダで、ここで何か計算を挟みたいときは n -> n * n (二乗の合計) や n -> n + 1 のように書き換えるだけです。空配列 (new int[0]) を渡したときも安心で、Streamsum() は要素が 1 つもなければ自動的に 0 を返してくれます。if で空チェックを書く必要はありません。

よくある間違い

ラムダ式は短く書ける反面、初学者がハマる落とし穴がいくつかあります。事前に知っておくと事故が減ります。

  • 式形式とブロック形式を混同するn -> n * 2return を書かない、n -> { return n * 2; } は書く、というルールです。n -> { n * 2; } のように return を抜くとコンパイルエラーになります。{} を付けたら必ず return を書く、と覚えてください
  • ローカル変数を後から書き換えてしまう — ラムダの中で参照する外側のローカル変数は final または effectively final (代入が 1 度きり) でなければなりません。int x = 10; Function<Integer,Integer> f = n -> n + x; まではよくても、その後 x = 20; と書き換えるとエラーになります。インスタンスフィールドなら自由に書き換えできますが、ローカル変数は固定です
  • return を書ける場所と書けない場所を混同する — 式形式は値そのものを返すので return を書きません。ブロック形式は普通のメソッドと同じく return で値を返します。式形式の右側にうっかり return n * 2 と書くとコンパイルエラーになるので注意してください

もう一つは、ラムダの中で例外を投げたいときの作法です。Function などの標準関数型インターフェースは throws を宣言していないので、IOException のような検査例外をそのまま投げることができません。投げたい場合は try/catch でラップするか、独自の関数型インターフェースを作って throws を付ける必要があります。

ラムダはあくまで「軽い処理を渡すための道具」と割り切るのがコツです。複雑なロジックや例外処理が多い場合は、無理にラムダにせず通常のメソッドに切り出した方が読みやすくなることもあります。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • Stream API のオペレータlist.stream().filter(x -> x.age > 18).map(x -> x.name) のように引数で渡すのが定番
  • コールバックbutton.setOnClickListener(e -> handleClick()) のように UI ライブラリのイベント登録
  • 並列処理executor.submit(() -> doWork()) で別スレッドに処理を投げる
  • ソート条件の動的指定list.sort((a, b) -> a.score - b.score) のように比較ロジックを 1 行で渡す

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • ラムダが長すぎる ─ 3 行を超えるラムダは可読性が落ちる。private メソッドに切り出すよう指摘
  • メソッド参照で書けるのにラムダx -> System.out.println(x)System.out::println で書ける
  • 例外を投げるラムダ ─ 検査例外を投げると関数型インタフェースが受けられない。専用ラッパーや Function の境界を意識
  • ラムダ内で外側の mutable 変数を変更しようとする ─ コンパイルエラー。AtomicInteger などで包む必要がある

パフォーマンス考慮事項

  • ラムダはほぼ無料 ─ JIT が静的呼び出しに最適化するため、メソッド呼び出しとほぼ同等の速度
  • キャプチャは GC コスト ─ 外側変数をキャプチャするラムダは、その変数を保持するためのオブジェクト生成が発生
  • ホットパスでは method reference を優先x -> x.toString() より Object::toString のほうが軽い (実装は同じだが GC が少ない)
  • stream() 内のラムダは並列向き ─ 副作用なしのラムダなら parallelStream で簡単に並列化できる
この章のポイント

ここまでの要点 (args) -> expr で関数を 1 行で書く。関数型インタフェースの代わりとして渡し、外側変数は実質 final なら参照可能。

やってみよう

それでは課題に挑戦してみましょう。やることは次のとおりです。

  1. Solution.lambdaSum(int[] arr) のシグネチャを保つ
  2. Arrays.stream(arr)IntStream に変換する
  3. .boxed()Stream<Integer> に変える
  4. mapToInt(n -> n) でラムダを通す (恒等関数で OK)
  5. sum() を呼んで合計を return する

Arrays を使うので、ファイル先頭で import java.util.Arrays; を忘れずに書いてください。空配列 (new int[0]) のときは sum() が自動で 0 を返してくれるので、特別な分岐は不要です。

慣れてきたら mapToInt(n -> n * 2) にして全要素を 2 倍してから合計したり、filter(n -> n % 2 == 0) を挟んで偶数だけの合計を出したりと、ラムダを差し替えて遊んでみてください。同じ流れのまま、計算の中身だけをラムダで自由に差し替えられるのが Stream + ラムダのいちばんの魅力です。

よくある質問

Q. lambda と def の使い分けは?

A. 1 行で書ける小さな関数なら lambda、複数行や名前を付けて再利用する関数は def を使います。sorted(items, key=lambda x: x.age) のように key 引数や map/filter と組み合わせるのが典型的な使い道です。

Q. lambda に複数文書けますか?

A. 書けません。lambda は式(expression)一つだけで構成されるため、文(statement)や複数行は def 関数に切り出します。複雑になったら必ず def に変えるのが可読性のコツです。

Q. lambda のスコープはどう決まりますか?

A. 定義された場所の外側スコープの変数を参照できます(クロージャ)。ループ内で lambda を作るとループ変数が遅延参照されるトラップ(全部最後の値になる)があるため、デフォルト引数で値を固定する(lambda x=i: x)回避策を覚えておくと安全です。

次のレッスン

次は メソッド参照 :: で、メソッド参照 :: を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. Lambda の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Lambda とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution.lambdaSum(int[] arr)static メソッドとして実装すること
  2. Arrays.stream(arr)int[]IntStream に変換し、.boxed()Stream<Integer> に変えてから mapToInt(n -> n) でラムダ式を適用すること
  3. 最終的に sum() を呼んで合計を int で返すこと (空配列なら自動的に 0 が返る)

入出力例

test-cases.txt

lambdaSum([1,2,3])6 lambdaSum([])0 lambdaSum([10,20,30,40])100 lambdaSum([-5,5,-10,10])0 lambdaSum([7])7

ヒント

main.java
main.java
学習モード

メモ

ラムダ式の基本

⌘S で保存