信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
ゼロイチ Java ラムダ式
ラムダ式の構文・省略ルール全種・effectively final・thisの挙動まで、0知識からJava Silver合格レベルへ徹底解説。
一言結論
ラムダ式は匿名クラスの省略形。(引数) -> 処理 が基本形で、省略ルールとeffectively finalを完全に理解すれば試験は解ける。
ラムダ式とは何か
ラムダ式とは 「処理(コード)を変数に入れて渡せるようにする書き方」 だ。
普通の変数には「値」を入れる:
int age = 25;
String name = "太郎";
boolean flag = true;
ラムダ式を使うと、「処理そのもの」を変数に入れられる:
Runnable r = () -> System.out.println("処理が変数に入っている");
r.run(); // ← ここで初めて実行される
「代入した瞬間に処理が実行される」わけではない。変数 r に処理を入れておいて、r.run() を呼んだときに初めて実行される。これが重要な点だ。
なぜラムダ式が必要なのか
プログラムを書いていると「処理そのものを引数として渡したい」場面が頻繁に出てくる。
例1:ソートのルールを渡す
List<String> names = Arrays.asList("田中", "鈴木", "佐藤", "山田");
// 「名前の長さで並べる」というルールを渡したい
names.sort(???);
ソートのアルゴリズムは Java が持っている。でも「どんな順番で並べるか」のルールは自分で決める。このルールが「処理」であり、渡す必要がある。
例2:フィルタリングの条件を渡す
List<Integer> numbers = Arrays.asList(1, -3, 5, -2, 8, -7);
// 「正の数だけ取り出す」という条件を渡したい
numbers.stream().filter(???).forEach(System.out::println);
フィルタの仕組みは Java が持っている。でも「何を通すか」の条件は自分で書く。
例3:後で実行する処理を登録する
// ボタンを押したときに何をするか、先に登録しておく
button.setOnClickListener(???);
「処理を後で実行するために登録する」というのも、処理を変数のように渡す操作だ。
これらすべて「処理(関数)を引数に渡す」という操作だ。ラムダ式はこれをシンプルに書ける。
ラムダ式が生まれるまでの歴史
Step 1:クラスを別に定義する(Java 8 以前)
interface Greeter {
void greet(String name);
}
class FormalGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("はじめまして、" + name + "様。");
}
}
// 使うとき
Greeter g = new FormalGreeter();
g.greet("田中");
「はじめまして、田中様。」を表示するためだけに、クラスを1個定義しなければならない。1回しか使わないのに。
Step 2:匿名クラス(その場で定義)
クラスファイルを別に作るのが嫌なので、「その場で名前なしのクラスを定義する」匿名クラスが登場した:
Greeter g = new Greeter() {
@Override
public void greet(String name) {
System.out.println("はじめまして、" + name + "様。");
}
};
g.greet("田中");
クラスファイルは不要になった。でも問題は残っている。
本当にやりたいこと: System.out.println("はじめまして、" + name + "様。")
実際に書くコード:
new Greeter() { ← ボイラープレート
@Override ← ボイラープレート
public void greet(String name) { ← ボイラープレート
System.out.println("はじめまして、" + name + "様。"); ← やりたいこと
} ← ボイラープレート
}; ← ボイラープレート
やりたいこと1行のために、まわりに5行のボイラープレートを書かされる。
Step 3:ラムダ式(Java 8 から)
Greeter g = name -> System.out.println("はじめまして、" + name + "様。");
g.greet("田中");
ボイラープレートが全部消えた。処理の本質だけが残っている。動作はまったく同じだ。
コンパイラが「Greeter 型の変数に name -> ... を代入しようとしている」から「Greeter の抽象メソッド greet の実装だ」と判断して、裏で匿名クラスに変換する。開発者の書くコードが減っただけで、実行されるものは同じだ。
ラムダ式の構文
ラムダ式は -> (アロー演算子)を境に左右に分かれる:
(引数リスト) -> 処理
- 左側:メソッドに渡ってくる引数(パラメータ)
->:「これを受け取って、これをする」という矢印- 右側:実際に行う処理(ラムダ本体)
この3つで1セットだ。
引数なし
Runnable r = () -> System.out.println("引数なし");
r.run(); // → 引数なし
引数がないときは空の丸カッコ () を書く。省略はできない。
引数が1つ
Consumer<String> c = (name) -> System.out.println("こんにちは、" + name);
c.accept("太郎"); // → こんにちは、太郎
引数が2つ以上
BiFunction<String, Integer, String> f = (name, age) -> name + "(" + age + "歳)";
System.out.println(f.apply("田中", 30)); // → 田中(30歳)
処理が複数行
処理が1行に収まらないときは波カッコ { } で囲む:
Consumer<String> c = name -> {
String greeting = "こんにちは、" + name + "さん!";
System.out.println(greeting);
System.out.println("よろしくお願いします。");
};
c.accept("太郎");
// → こんにちは、太郎さん!
// → よろしくお願いします。
省略ルール
ラムダ式には「書いても書かなくてもいい」部分がたくさんある。この省略ルールが試験で最も頻繁に問われる。
省略ルール1:引数の型は省略できる
コンパイラは左辺の型(関数型インターフェースの型パラメータ)から引数の型を推論できる。だから明示しなくていい:
// 型あり(省略しない)
Function<String, Integer> f1 = (String s) -> s.length();
// 型なし(推論に任せる)← どちらも同じ
Function<String, Integer> f2 = (s) -> s.length();
重要:型の指定は「全員書く」か「全員省略する」のどちらかだけ。
複数の引数がある場合、「一部だけ型を書く」という混在はできない:
// ❌ エラー:s1 だけ型あり、s2 は型なし
BiFunction<String, String, Integer> bad =
(String s1, s2) -> s1.length() + s2.length();
// ✅ 全員書く
BiFunction<String, String, Integer> ok1 =
(String s1, String s2) -> s1.length() + s2.length();
// ✅ 全員省略
BiFunction<String, String, Integer> ok2 =
(s1, s2) -> s1.length() + s2.length();
省略ルール2:引数が1つのときカッコを省ける
// カッコあり
Consumer<String> c1 = (s) -> System.out.println(s);
// カッコなし(引数が1つのときだけ省略できる)
Consumer<String> c2 = s -> System.out.println(s);
引数が0個や2個以上のときはカッコ省略不可:
// ❌ 引数0個でカッコを省略するとエラー
Runnable bad = -> System.out.println("NG");
// ✅ 引数0個はカッコ必須
Runnable ok = () -> System.out.println("OK");
// ✅ 引数2つもカッコ必須
BinaryOperator<Integer> add = (a, b) -> a + b;
ただし、引数が1つでも型を書くときはカッコが必要:
// 型を書くときはカッコが必要
Consumer<String> c = (String s) -> System.out.println(s); // ✅ OK
// ❌ 型あり + カッコなし はエラー
Consumer<String> bad = String s -> System.out.println(s);
省略ルール3:処理が1文のとき波カッコを省ける
// 波カッコあり(処理が1文)
Consumer<String> c1 = s -> { System.out.println(s); };
// 波カッコなし(処理が1文のとき省略できる)
Consumer<String> c2 = s -> System.out.println(s);
波カッコを省略するとき、文末の セミコロンも省略 する:
// ❌ 波カッコなしでセミコロンあり → 構文エラー
Consumer<String> bad = s -> System.out.println(s);; // ← セミコロンが余分
波カッコを省略した場合の -> 右辺は「文」ではなく「式」として扱われる。式はセミコロンを必要としない。
省略ルール4:return文と波カッコはセットで省ける
処理が「値を1つ返す1行」のとき、return と波カッコを両方まとめて省略できる。省略すると式の評価値が自動でreturnされる:
// return あり + 波カッコあり
Function<String, Integer> f1 = s -> { return s.length(); };
// return なし + 波カッコなし(式の結果が自動で返される)
Function<String, Integer> f2 = s -> s.length();
return だけ残して波カッコを省くのはNG。逆に波カッコだけ残して return を省くのもNG(その場合は void として扱われエラーになる):
// ❌ return だけ書いて波カッコを省くとエラー
Function<String, Integer> bad1 = s -> return s.length();
// ❌ 波カッコはあるのに return がない(int を返せない)
Function<String, Integer> bad2 = s -> { s.length(); };
// ✅ 両方書く
Function<String, Integer> ok1 = s -> { return s.length(); };
// ✅ 両方省略
Function<String, Integer> ok2 = s -> s.length();
省略ルールまとめ
| 内容 | 省略できるか | 条件・注意 |
|---|---|---|
| 引数の型 | できる | 全員省略 or 全員書く(混在不可) |
| 引数のカッコ | できる | 引数が1つのときだけ(型を書く場合はカッコ必須) |
| 波カッコ | できる | 処理が1文のとき |
| セミコロン | できる | 波カッコを省いたとき(セットで省く) |
return + 波カッコ | できる | 1行 return のとき(セットで省く) |
| 型の一部だけ省略 | できない | - |
return だけ省略(波カッコ残す) | できない | - |
ラムダ式と関数型インターフェースの関係
ラムダ式は、関数型インターフェース型の変数や引数にしか代入・渡しできない。
// ✅ 関数型インターフェース型の変数に代入できる
Runnable r = () -> System.out.println("OK");
// ❌ 型が決まっていない変数には代入できない
var bad = () -> System.out.println("NG"); // エラー:どの関数型インターフェースか不明
コンパイラは「代入先の型」を見てラムダ式の型を決める。これを ターゲット型(target type) という。
引数に渡す場合も同じ:
void execute(Runnable r) {
r.run();
}
execute(() -> System.out.println("渡せる!")); // ✅ OK
メソッドの引数型が Runnable なので、ラムダ式を渡せる。
effectively final(実質的にfinal)
ラムダ式の中から、外側のスコープで定義された変数を参照できる。これを 変数のキャプチャ(capture) という。
String prefix = "Hello, ";
Consumer<String> greeter = name -> System.out.println(prefix + name);
// ↑ 外側の変数を参照している
greeter.accept("太郎"); // → Hello, 太郎
ただし、参照できる変数には条件がある。
final とは
final を付けた変数は、一度値を代入したら変更できない:
final int MAX = 100;
MAX = 200; // ❌ エラー:final変数は再代入できない
effectively final(実質的にfinal)
final キーワードがついていなくても、宣言後に一度も値が変更されていない変数を「実質的にfinal(effectively final)」という。Java 8 から導入された概念だ。
int x = 10; // final なし
// x に対してこれ以降、再代入がなければ「実質的にfinal」
String name = "太郎"; // final なし
// name が変更されなければ「実質的にfinal」
ラムダ式でキャプチャできるのは「実質的にfinal」な変数だけ
int a = 10; // 変更なし → 実質的にfinal ✅
int b = 20;
b = 30; // ← 変更した → 実質的にfinalではない ❌
final int c = 40; // final を明示 → もちろん OK ✅
Runnable r = () -> {
System.out.println(a); // ✅ OK
System.out.println(b); // ❌ コンパイルエラー
System.out.println(c); // ✅ OK
};
ラムダ式の中で外側の変数を変更しようとしてもエラー:
int count = 0;
Runnable r = () -> {
count++; // ❌ コンパイルエラー:外側の変数を変更しようとしている
count = 10; // ❌ コンパイルエラー:同上
};
これをやろうとしたとき count は「実質的にfinal」でなくなるため(ラムダが変更するから)エラーになる。
なぜこの制約があるのか
ラムダ式は「後で実行される」ことがある。たとえばボタンのクリック処理は、ラムダを登録した後、しばらくしてから実行される:
int count = 0; ← この時点では 0
Runnable r = () -> ...; ← 登録する
(時間が経過、count が変わる可能性)
r.run(); ← ここで実行。count は今いくつ?
もし変数が変えられると「どの値を使うべきか」が不明確になる。それを防ぐために、ラムダ式がキャプチャできる変数は「値が絶対に変わらないもの」に限定されている。
インスタンスフィールドはキャプチャの制約を受けない
ローカル変数(メソッド内で宣言した変数)には effectively final の制約があるが、インスタンスフィールドにはない:
public class Counter {
int count = 0; // インスタンスフィールド
Runnable createIncrementer() {
// フィールドなのでキャプチャの制約なし
return () -> count++; // ✅ コンパイルエラーにならない
}
}
フィールドはオブジェクトのライフサイクルで管理されるため、ローカル変数とは扱いが異なる。
ラムダ式の中での this
ラムダ式と匿名クラスでは this の意味が異なる。
匿名クラスの this
匿名クラスは「名前のないクラス」なので、匿名クラス内の this はその匿名クラスのインスタンスを指す:
public class Outer {
String name = "Outer";
void test() {
Runnable r = new Runnable() {
String name = "Anonymous"; // 匿名クラス内のフィールド
@Override
public void run() {
System.out.println(this.name); // → Anonymous(自分のフィールド)
}
};
r.run();
}
}
ラムダ式の this
ラムダ式は自分専用の this を持たない。外側のクラスの this をそのまま引き継ぐ:
public class Outer {
String name = "Outer";
void test() {
Runnable r = () -> {
System.out.println(this.name); // → Outer(外側のクラスのフィールド)
};
r.run();
}
}
ラムダ式の中の this は常に外側のクラスのインスタンスを指す。これは「ラムダ式はあくまで匿名クラスの省略形ではなく、メソッドのインライン記述に近い」という設計思想から来ている。
ラムダ式でできないこと
ラムダ式は便利だが、匿名クラスにできてラムダ式にできないことがある。
インターフェースを実装した型として扱えない
匿名クラスは独自の型を持つので、インスタンスの型情報を使えるが、ラムダ式にはそれがない:
// 匿名クラスはキャストできる
Runnable r = new Runnable() {
public void run() {}
public void extra() {} // 追加メソッド(外から呼べないが定義できる)
};
// ラムダ式では追加メソッドは定義できない
Runnable r2 = () -> {}; // これだけ
ステート(状態)を持てない
匿名クラスはフィールドを持てるが、ラムダ式は持てない:
// 匿名クラスはフィールドを持てる
Runnable counter = new Runnable() {
int count = 0; // フィールド
@Override
public void run() {
System.out.println(++count);
}
};
counter.run(); // → 1
counter.run(); // → 2
counter.run(); // → 3
// ラムダ式でこれはできない(effectively finalの制約で外側の変数も変更できない)
試験ではどう聞かれるか
パターン1:このコードはコンパイルできるか(省略ルール)
@FunctionalInterface
interface Calc {
int compute(int a, int b);
}
Calc c1 = (a, b) -> a + b; // A
Calc c2 = (int a, b) -> a + b; // B
Calc c3 = (a, b) -> { return a + b; }; // C
Calc c4 = (a, b) -> { a + b; }; // D
Calc c5 = (a, b) -> return a + b; // E
Calc c6 = (int a, int b) -> a + b; // F
答え:A・C・F だけ正しい
- A:型省略、波カッコ省略 ✅
- B:型を一部だけ書いている → 全員書くか全員省略するかのどちらか → ❌
- C:波カッコ + return 両方あり ✅
- D:波カッコがあるのに
returnなし。intを返せない → ❌ - E:
returnを書くなら波カッコも必要 → ❌ - F:型を全員書いている ✅
パターン2:effectively final
// 問題:コンパイルエラーになる行はどれか
int x = 10; // 変更なし
int y = 20;
y = 30; // y を変更
Runnable r = () -> {
System.out.println(x); // 行A
System.out.println(y); // 行B
};
// 行C:以下を r の定義の前に追加したらどうなるか?
// x = 5;
- 行A:
xは変更されていない → ✅ エラーなし - 行B:
yはy = 30で変更されている → ❌ コンパイルエラー - 行C(
x = 5;を前に追加した場合):xが変更されることになるので、行Aもエラーになる
パターン3:どの型で受けるか
// 問題:各ラムダ式に対して正しい型はどれか
① () -> "hello"
② s -> s.length() > 3
③ (a, b) -> a + b
④ s -> System.out.println(s)
⑤ () -> new Random().nextInt()
答え:
- ①:引数なし、String を返す →
Supplier<String> - ②:String を受け取り boolean →
Predicate<String> - ③:2つの引数を受け取り同じ型を返す →
BinaryOperator<String>またはBiFunction<String, String, String> - ④:String を受け取り返さない →
Consumer<String> - ⑤:引数なし、int を返す →
Supplier<Integer>またはIntSupplier(プリミティブ特化型)
パターン4:this の挙動
public class MyClass {
String value = "MyClass";
public void run() {
String value = "local"; // ローカル変数
Runnable r1 = new Runnable() {
String value = "anonymous";
@Override
public void run() {
System.out.println(this.value); // A
}
};
Runnable r2 = () -> {
System.out.println(this.value); // B
};
r1.run();
r2.run();
}
}
new MyClass().run();
答え:A は anonymous、B は MyClass
- A:匿名クラス内の
thisは匿名クラスのインスタンス →anonymous - B:ラムダ式の
thisは外側のMyClassのインスタンス →MyClass
ローカル変数 value は this なしではアクセスしていないので関係ない。
パターン5:匿名クラスとラムダ式の対比
// 問題:以下の匿名クラスと同じ動作をするラムダ式はどれか
Comparator<String> c = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
選択肢:
// A
Comparator<String> c = (a, b) -> a.length() - b.length();
// B
Comparator<String> c = a, b -> a.length() - b.length();
// C
Comparator<String> c = (String a, String b) -> { return a.length() - b.length(); };
// D
Comparator<String> c = (a, b) -> { a.length() - b.length(); };
答え:A と C
- A:正しい省略形 ✅
- B:引数が2つなのにカッコがない → ❌
- C:型を全員書いて return + 波カッコ → ✅
- D:波カッコがあるのに
returnがない。intを返せない → ❌
ラムダ式 vs 匿名クラス:比較まとめ
| 観点 | 匿名クラス | ラムダ式 |
|---|---|---|
| 構文の長さ | 長い | 短い |
| this の意味 | 匿名クラス自身 | 外側のクラス |
| フィールドを持てるか | 持てる | 持てない |
| 追加メソッドを定義できるか | できる | できない |
| 複数の抽象メソッドを実装できるか | できる | できない(1つのみ) |
| コンパイル後 | 別クラスファイルが生成される | invokedynamic 命令(最適化される) |
メリット・デメリット
メリット
書く量が圧倒的に減る
ボイラープレートがなくなり、処理の本質だけを書ける。
Stream API と組み合わせると強力
filter・map・reduce・forEach などすべてラムダ式(関数型インターフェース)を引数に取る。
読みやすくなる(慣れれば)
list.forEach(item -> process(item)) は「リストの各要素に処理をする」と直感的にわかる。
遅延評価が簡単に書ける
Supplier<T> を使えば「必要になったときに初めて実行する」処理を自然に書ける。
デメリット
省略が多くて最初は読みにくい
s -> s.length() が何をしているのか、慣れるまでわかりにくいことがある。
デバッグがしにくい
スタックトレースに匿名のラムダ式として表示され、どのラムダか特定しにくい。
複雑な処理には向かない
複数行の複雑なロジックをラムダに詰め込むと読みにくくなる。そういう場合はメソッドを定義してメソッド参照(ClassName::methodName)で渡すべきだ。
状態を持てない
ラムダ式の中で外側のローカル変数を変更できないため、カウンターや累積値を扱うときに工夫が必要になる。
まとめ:試験前チェックリスト
□ ラムダ式は「処理を変数に入れる」書き方
□ 関数型インターフェース型の変数にしか代入できない
□ 代入しただけでは実行されない(メソッドを呼んで初めて実行)
□ 基本形は (引数) -> 処理
□ 引数の型は省略できる(全員省略 or 全員書く、混在NG)
□ 引数が1つのときだけカッコを省ける(型を書くときはカッコ必須)
□ 引数が0個のときはカッコ必須
□ 処理が1文なら波カッコを省ける(セミコロンも省く)
□ 1行 return なら return と波カッコをセットで省ける(or 両方書く)
□ 波カッコあり + return なしで値を返そうとするとエラー
□ 外側のローカル変数を使うには「実質的にfinal」が条件
□ ラムダ式の中で外側のローカル変数を変更しようとするとエラー
□ インスタンスフィールドには effectively final の制約はない
□ ラムダ式の this は外側のクラスを指す(匿名クラスとは違う)
省略ルールで迷ったら「引数は何個か」「処理は何行か」「値を返しているか」の順に確認する。それだけで判断できる。