ラムダ式とは何か(まずイメージから)
ラムダ式は、Java 8 から入った「小さな“処理そのもの”を値として書くための記法」です。
「このタイミングで、こういう処理を実行してほしい」という“やりたいこと”だけを、短く書いて渡せるようにするためのものです。
昔は無名クラスで長々と書いていた場所を、1 行〜数行で表現できるようにしたもの、と捉えると分かりやすいです。
例えば、こういう無名クラス:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
Javaこれをラムダ式で書くと、こうなります。
Runnable r = () -> System.out.println("Hello");
Java「え、それだけ?」というくらい短くなっています。
ここから、「ラムダ式の構文」を丁寧に分解していきます。
ラムダ式の基本構文
一番大事な形:(引数) -> 処理
ラムダ式の基本形は、たったこれだけです。
(引数リスト) -> 式またはブロック
Java記号として覚えるべきは、ただ 2 つ。
丸括弧 (...) に並んだ「引数」。
矢印 -> の右側に書く「処理本体」。
具体例で見てみます。
(int x) -> x * 2
Javaこれは「int x を受け取って、x * 2 を返す処理」という意味です。
まだどこにも代入していないので、単独では「意味を持つけどまだ使われていない処理」です。
もう少し Java らしく使う例にすると、例えば Function<Integer, Integer> に代入できます。
import java.util.function.Function;
Function<Integer, Integer> f = (int x) -> x * 2;
Integer result = f.apply(10); // 20
Javaここで、
(int x) が「引数リスト」x * 2 が「本体(返す式)」
です。
複数行の処理を書くとき:{ } ブロックを使う
一行では書けないような処理は、メソッドと同じようにブロックで書きます。
(int x) -> {
System.out.println("受け取った値: " + x);
return x * 2;
}
Javaこのときのルールが重要です。
中括弧 { } を使ったブロックの中では、
普通のメソッドのように return を書かなければ値を返せません。
一方、中括弧を使わずに一行の式だけ書いている場合は、その式の値がそのまま戻り値になります。
ここ、初心者はよく混乱するので、切り分けて覚えてください。
一行ラムダ(ブロックなし)x -> x * 2 なら、x * 2 がそのまま戻り値。return は書かない。
ブロックラムダ({ ... } あり)x -> { return x * 2; } のように、明示的に return が必要。
省略ルール(シンタックスシュガー)を順番に理解する
ラムダ式は、最初は「全部きっちり書いて」から、少しずつ省略できる、と思ってください。
引数の型はだいたい省略できる
先ほどの例を、もう一度出します。
Function<Integer, Integer> f = (int x) -> x * 2;
Javaこれは、引数の型を省略して書けます。
Function<Integer, Integer> f = (x) -> x * 2;
Javaなぜかというと、左側で Function<Integer, Integer> と書いているので、
Java コンパイラは「このラムダの引数 x は Integer 型だな」と推論できるからです。
型が推論できる場合、ラムダ式の引数の型は書かなくて OK です。
ただし、省略は「分かりやすさとのバランス」です。
最初のうちはあえて型を書くのもアリですし、慣れてきたら省略してすっきり書く、という段階を踏めばいいです。
引数が 1 つなら、丸括弧も省略できる
引数が 1 つのときだけ、丸括弧も省略できます。
Function<Integer, Integer> f1 = (x) -> x * 2;
Function<Integer, Integer> f2 = x -> x * 2; // 括弧を省略
Java引数が 2 つ以上なら括弧は必須です。
(a, b) -> a + b // OK
a, b -> a + b // コンパイルエラー
Javaまた、引数が 0 個のときは必ず () が必要です。
() -> System.out.println("Hello");
Java処理が 1 行のときは {} と return を省略できる
これもよく使う省略です。
ブロックあり版:
x -> { return x * 2; }
Javaブロックなし版:
x -> x * 2
Java後者の方が圧倒的に読みやすいです。
実務のラムダ式は、この「一行で書ける形」がとても多いです。
一方、複数行になる場合はブロック必須です。
x -> {
System.out.println("log: " + x);
return x * 2;
}
Javareturn を忘れるとコンパイルエラーになるので、
「ブロックを書いたら return も忘れずに」とセットで頭に入れておいてください。
具体例で構文を体に入れる
例1:Runnable をラムダで書く
Runnable は引数なし・戻り値なしの関数型インターフェースです。
昔の書き方:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
Javaラムダ式の書き方(基本形):
Runnable r = () -> {
System.out.println("Hello");
};
Java一行に省略すると:
Runnable r = () -> System.out.println("Hello");
Java構文的には、
() が「引数なし」。-> の右側が「実行したい処理」。
これだけです。
例2:Comparator をラムダで書く
Comparator<T> は「2 つの T を受け取って int を返す」関数型インターフェースです。
匿名クラス版:
Comparator<String> cmp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Javaラムダ式版(型あり):
Comparator<String> cmp = (String a, String b) -> {
return a.length() - b.length();
};
Java型省略・一行版:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Javaポイントを整理すると、
引数が 2 つなので (a, b) と括弧付き。
戻り値があるが、処理が 1 行なので {} と return を省略。
という流れになっています。
例3:forEach にラムダを渡す
ラムダ式が「関数を引数に渡す」感覚をつかみやすいのが、forEach です。
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
}
}
Javaここで forEach の引数は Consumer<T> という関数型インターフェースです。
「T を 1 つ受け取って、何かする(戻り値なし)」という型です。
name -> System.out.println(name) は、
「String を 1 つ受け取って、println する処理」として、そのまま関数として渡されています。
ラムダ式は、「この場で簡単な関数を作って、そのまま引数として渡す」ための文法だ、とイメージするとスッと入ってきやすいです。
「関数型インターフェース」とラムダ式の関係(ここ大事)
ラムダ式は「関数型インターフェース」のインスタンスを作っているだけ
Java では、ラムダ式自体に「型」があるわけではありません。
実際には、ラムダ式は必ず「何かの関数型インターフェース」に代入されたり、
その引数に渡されたりすることで、その場の「型」が決まります。
例えば、
Runnable r = () -> System.out.println("Hello");
Javaこれは、「Runnable という関数型インターフェースのインスタンス」としてラムダが扱われています。
同じラムダでも、受け取る側の型によって意味が変わります。
Function<Integer, Integer> f = x -> x * 2;
UnaryOperator<Integer> op = x -> x * 2;
Javaどちらも x -> x * 2 ですが、
左側の型によって、「これは Function の実装」「これは UnaryOperator の実装」と解釈されます。
だからこそ、Java では「ラムダ式だけ単独で置いておく」はほぼなく、
必ず「どの関数型インターフェースとして扱うか」が決まる場所に書きます。
構文と「どのインターフェースにマッチしているか」を結びつけて見る
ラムダ式の構文((引数) -> 本体)だけを見ていると、
「これって何なんだ?」と感じやすいです。
必ずセットで、「このラムダはどの関数型インターフェースに対応しているか」を意識してください。
例をもう一度整理します。
Runnable r = () -> {...};
引数なし、戻り値なし ⇒ void run() に対応。
Comparator<String> c = (a, b) -> {...};
引数 2 つ、戻り値 int ⇒ int compare(T a, T b) に対応。
Function<Integer, String> f = x -> String.valueOf(x);
引数 1 つ、戻り値あり ⇒ R apply(T t) に対応。
構文としては全部「ラムダ式」ですが、
左側の型によって「何の関数の実装なのか」が決まります。
匿名クラスとの対応関係で構文を覚える
無名クラス → ラムダ に対応させて書き換えてみる
最後に、「無名クラスからラムダに変換する」という観点で、構文をまとめておきます。
無名クラス版:
new Something() {
@Override
public 戻り値型 メソッド名(引数リスト) {
本体;
}
}
Javaこれがラムダ式では、だいたいこう変わります。
(引数リスト) -> 本体
Javaつまり、
new 〜 の部分も@Override も
戻り値の型も
メソッド名も
全部省略されて、「引数」と「本体」だけが残る感じです。
例えば Comparator なら、
Comparator<String> c = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Javaが、
Comparator<String> c = (a, b) -> a.length() - b.length();
Javaになる。
無名クラスで慣れているなら、
「どの部分が省略されて、どこだけ残っているか」を意識しながらラムダ構文を見てみると、パターンがつかみやすくなります。
まとめ:ラムダ式の構文を自分の言葉で整理する
ラムダ式の構文を、初心者向けに自分の言葉でまとめるとこうなります。
基本形は「(引数) -> 本体」。
引数 0 個なら ()、1 個なら (x) か x、複数なら (a, b, c)。
本体が 1 行なら {} と return を省略できる。
本体が複数行なら {} と return を使う、普通のメソッドに近い書き方。
引数の型は、受け取る側の型(関数型インターフェース)から推論できるなら省略可。
そして、ラムダ式は必ず「どの関数型インターフェースとして使われているか」とセットで考える。

