SJ blog
beginner
S

信頼度ランク

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:yy = 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

ローカル変数 valuethis なしではアクセスしていないので関係ない。

パターン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 と組み合わせると強力
filtermapreduceforEach などすべてラムダ式(関数型インターフェース)を引数に取る。

読みやすくなる(慣れれば)
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 は外側のクラスを指す(匿名クラスとは違う)

省略ルールで迷ったら「引数は何個か」「処理は何行か」「値を返しているか」の順に確認する。それだけで判断できる。