1. はじめに
ひと言で言えば、ラムダ式とは、別のクラスや匿名クラスを宣言せずに、その場で関数型インターフェースの実装を素早く作るための方法です。名前のない小さなメソッドのようなもので、引数として渡したり、変数に保存したりできます。
Java は長い間「古典的な」OOP 言語でした。ボタンが押されたときに何をするかといった振る舞いを渡したい場合は、次のように匿名クラスを書く必要がありました:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("ボタンが押されました!");
}
});
Java 8 からはラムダの魔法が登場しました:
button.setOnClickListener(() -> System.out.println("ボタンが押されました!"));
ここで () -> System.out.println("ボタンが押されました!") がラムダ式です。
形式的には: ラムダ式とは、関数型インターフェースのただ 1 つの抽象メソッドの実装を簡潔に記述するための構文です。
なぜ重要なのか?
- 振る舞いを値として渡せる(例: リストの各要素をどう処理するか)。
- 簡潔で読みやすいコードが書ける。
- Java のモダンな API(Stream API、イベント処理、非同期タスクなど)を活用できる。
2. ラムダ式の構文
基本形
(パラメータ) -> 式
// または
(パラメータ) -> { コードブロック }
さまざまなインターフェースでの例
1. 引数なし(例: Runnable):
Runnable r = () -> System.out.println("こんにちは、ラムダ!");
r.run();
2. 引数が 1 つ(例: Consumer):
Consumer<String> printer = s -> System.out.println("渡された値: " + s);
printer.accept("Java");
引数が 1 つで型が推論できる場合、かっこは省略できます。
String 型は、渡される変数(s)の型です。
3. 引数が複数(例: Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Integer 型は、渡される変数(a、b)の型です。
4. 複数行の本体(明示的な戻りがある場合は波かっこと return が必要):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
最初の Integer は関数の戻り型、2 つ目の Integer は引数の型です。
省略と簡潔さのポイント
- 本体が 1 つの式なら、波かっこと return を省略できます。
- 引数がない場合は空のかっこ: () -> ...
- 引数が 1 つならかっこ省略可: x -> ...
- 2 つ以上の引数ならかっこが必要: (a, b) -> ...
まとめ表
同じ発想をラムダと匿名クラスでどう書くかを比べてみましょう:
| インターフェース | ラムダ式(例) | 匿名クラスの同等表現 |
|---|---|---|
|
|
|
|
|
|
|
|
|
独自インターフェースの例
次の関数型インターフェースがあるとします:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
以前は次のように実装していました:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
今どきはこう書きます:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
標準インターフェースの例
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("りんご", "バナナ", "キウイ");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. スコープと変数キャプチャ
外側のコンテキストの変数(effectively final)
ラムダ式は外側のメソッドにある変数を利用できます。ただし、そうした変数は effectively final、つまり final と明示されているか、初期化後に変更されないものである必要があります。
例:
String prefix = "結果: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// ここで prefix は「凍結」される — この後は変更できない
System.out.println(f.apply(5)); // 結果: 10
prefix をラムダで使用した後に変更しようとすると、コンパイルエラーになります。
なぜこうなるのか?
ラムダ式は、その変数が宣言されたメソッドを抜けた後に呼び出される可能性があります。「不思議な」バグを避けるため、Java では不変の変数だけを使用できるようにしています。
匿名クラスとの違い
匿名クラスでも同じルールが適用され、外側のメソッドの変数は final/effectively final でなければなりません。ただしスコープには違いがあります。匿名クラスの中では this はその匿名クラス自身を指しますが、ラムダでは外側のクラスを指します。
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // 出力: Demo$1(匿名クラス)
}
};
Runnable r2 = () -> System.out.println(this); // 出力: Demo(外側のクラス)
r1.run();
r2.run();
}
}
ラムダとクラスのフィールド
ラムダ式は外側のクラスのフィールドに制限なくアクセスできます:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
ここでの base はフィールドなので変更可能です。
x は effectively final でなければなりません。
4. 実践: いくつかのラムダ式を書いてみる
例: 数値リストのフィルタリング
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("偶数: " + n));
Stream API の詳細はレベル30で学びます :P
例: 文字列変換関数
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
例: 自作の関数型インターフェース
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("こんにちは")); // こんにちは!
例: 外側のコンテキストの変数を使う
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// この後で factor を変更することはできません!
5. ラムダ式でよくある誤り
誤り1: ラムダがキャプチャした変数を変更しようとする。
このコードはコンパイルできません — その変数は effectively final でなければなりません:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // エラー: sum は final ではありません!
値を蓄積したい場合は、配列やラッパーオブジェクトを使いましょう。
誤り2: this のスコープを取り違える。
ラムダ式の中では、this はラムダ自身ではなく外側のクラスを指します(匿名クラスとは異なります)。
誤り3: 複数行のラムダ式で波かっこと return を忘れる。
本体が 1 つの式でない場合、波かっこと return が必要です:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
誤り4: ラムダ式の型を正しく決めていない。
ラムダ式は常に関数型インターフェースを実装します。次のように書くだけではいけません:
var f = x -> x + 1; // エラー!どのインターフェース型か不明です。
型を明示する必要があります:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION