ラムダ式の基本
このレッスンで分かること
- ラムダ式
(x) -> x * 2は「引数 → 戻り値」を 1 行で書く関数オブジェクト- 関数型インタフェース (抽象メソッド 1 つ) の代わりに渡せる。
ComparatorRunnable等- 外側のローカル変数は 実質 final なら参照できる (キャプチャ)
- コードが短くなるが、デバッグやスタックトレースで匿名関数として現れる
ラムダ式の基本 とは
Java 8 から導入されたラムダ式の文法と関数型インターフェースの考え方を学び、Stream と組み合わせて配列の合計を一行で書く感覚を身につけよう。
ラムダ式とは ─ 関数を「値」として持ち運ぶ
Java 8 から、それまでクラスやメソッドの中にしか書けなかった処理を、まるで int や String のように「値」として変数に入れたり、メソッドの引数として渡したりできるようになりました。それを実現する文法が ラムダ式 (lambda expression) です。
例えば「整数を 2 倍にする処理」を持ち運びたいとします。これまでの Java では、わざわざ Doubler のようなクラスを定義したり、Function 型を実装した 匿名クラス を書いたりする必要がありました。ラムダ式を使うと、n -> n * 2 というたった一行で同じものを表現できます。コードが短くなるだけでなく、「データを受け取って結果を返す」という関数の本質がそのまま見えるようになります。
ラムダ式は単なる文法糖衣 (
syntax sugar) ではありません。背後では関数型インターフェースを実装したFunctionオブジェクトが生成されています。次の章で詳しく見ますが、Java のラムダは「関数のように見えるオブジェクト」だと押さえておくのが正解です。
ラムダ式が普及したことで、Java は Collection や Stream を扱うコードを劇的に短く書けるようになりました。本章ではまずラムダの読み方・書き方を覚え、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 も @Override も applyAsInt も全部消えました。Java コンパイラが「doubler は IntUnaryOperator 型だから、これは 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で呼ぶIntUnaryOperator—int→intの特化版。プリミティブで高速
ラムダの型は「左辺の型」で決まります。同じ
n -> n + 1でも、左辺がFunction<Integer, Integer>ならFunction、IntUnaryOperatorなら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> の mapToInt は Integer から int への ToIntFunction を受け取るので、ラムダ式 n -> n (オートアンボクシングで Integer → int) がぴったり合います。最後に sum() で全要素を足し上げて結果を返す、という流れです。
mapToInt(n -> n) の n -> n は「受け取った値をそのまま返す」恒等関数のラムダで、ここで何か計算を挟みたいときは n -> n * n (二乗の合計) や n -> n + 1 のように書き換えるだけです。空配列 (new int[0]) を渡したときも安心で、Stream の sum() は要素が 1 つもなければ自動的に 0 を返してくれます。if で空チェックを書く必要はありません。
よくある間違い
ラムダ式は短く書ける反面、初学者がハマる落とし穴がいくつかあります。事前に知っておくと事故が減ります。
- 式形式とブロック形式を混同する —
n -> n * 2はreturnを書かない、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 なら参照可能。
やってみよう
それでは課題に挑戦してみましょう。やることは次のとおりです。
Solution.lambdaSum(int[] arr)のシグネチャを保つArrays.stream(arr)でIntStreamに変換する.boxed()でStream<Integer>に変えるmapToInt(n -> n)でラムダを通す (恒等関数で OK)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 つができることを確認しましょう。
- Lambda の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Lambda とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Solution.lambdaSum(int[] arr)のstaticメソッドとして実装することArrays.stream(arr)でint[]をIntStreamに変換し、.boxed()でStream<Integer>に変えてからmapToInt(n -> n)でラムダ式を適用すること- 最終的に
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