Java | Java 詳細・モダン文法:ラムダ式・関数型 – ラムダ式の構文

Java Java
スポンサーリンク

ラムダ式とは何か(まずイメージから)

ラムダ式は、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;
}
Java

return を忘れるとコンパイルエラーになるので、
「ブロックを書いたら 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 つ、戻り値 intint 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 を使う、普通のメソッドに近い書き方。
引数の型は、受け取る側の型(関数型インターフェース)から推論できるなら省略可。

そして、ラムダ式は必ず「どの関数型インターフェースとして使われているか」とセットで考える。

タイトルとURLをコピーしました