Javaラムダ式とは?書き方から実務の使い方まで徹底解説

Mirai

Javaのラムダ式って、矢印とか出てきて意味不明なんだけど…何のために使うの?

Zetto

コードをシンプルに書くための構文だよ。ラムダ式を使えると処理の意図が一目で伝わるから、実務でもかなり重宝する。

この記事では、Javaラムダ式とは何かを基本構文から実務での使い方まで解説します。

Zettoのライタープロフィール

本記事の専門性
現役エンジニアのZettoです。Java SilverとJava Goldの資格を持ち、Javaを使ったフリーランス案件を複数経験してきました。

この記事を読めば、ラムダ式の書き方・匿名クラスとの違い・実務での使われ方まで、一通り理解できます。

ぜひ参考にしてみてください。

目次

Javaラムダ式とは

ラムダ式とは

Javaラムダ式の概念と、登場した背景を整理します。

  • ラムダ式が登場した理由
  • ラムダ式でできること・できないこと

順番に見ていきましょう。

ラムダ式が登場した理由

ラムダ式は、Java 8(2014年リリース)で導入された機能です。

それ以前のJavaでは、「ちょっとした処理を別のメソッドに渡したい」というだけでも、匿名クラス(Anonymous Class)という長い書き方が必要でした。

たとえば、リストを並び替えるときはこう書いていました。

// Java 8以前の書き方(匿名クラス)
Collections.sort(names, new Comparator() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

やりたいことは「aとbを比較する」だけなのに、コードの大半が「お作法の記述」になっています。これが「冗長すぎる」という課題だったんですよね。

ラムダ式が登場したことで、同じ処理をこう書けるようになりました。

// Java 8以降の書き方(ラムダ式)
Collections.sort(names, (a, b) -> a.compareTo(b));

処理の意図だけが残って、ぐっと読みやすくなりましたよね。

Javaはもともと「オブジェクト指向言語」として設計されていましたが、Java 8から「関数型プログラミング(処理をデータのように扱う考え方)」の要素も取り入れました。ラムダ式はその中心的な機能に位置しています。

ラムダ式でできること・できないこと

ラムダ式のできること・できないことを整理しておきましょう。

できること

  • 関数型インターフェース(抽象メソッドが1つだけのインターフェース)の実装を短く書く
  • 処理を変数に代入したり、引数として渡したりする
  • Stream APIと組み合わせてリスト操作をスッキリ書く

できないこと・注意が必要なこと

  • 抽象メソッドが2つ以上あるインターフェースには使えない
  • ラムダ式の中で参照する外部変数は「実質final(後から変更しない変数)」でないといけない
  • thisの指す対象が匿名クラスと違うため、注意が必要な場面がある

「できないこと」の詳しい内容は、後半の「初心者がつまずく落とし穴」セクションで解説します。

まずはラムダ式=処理を短く書くための仕組みという理解で大丈夫です。

ラムダ式の背景と基本的な役割が整理できたところで、次は実際の書き方を見ていきましょう。

Zetto

ラムダ式は「短く書ける」だけでなく、「コードが何をやっているかを直接表現できる」のが最大の強みです。

ラムダ式の基本構文と書き方

基本構文と書き方

ラムダ式の書き方を、パターン別に解説します。

  • 3パターンで覚えるラムダ式の基本形
  • 引数・戻り値パターン別の書き方一覧
  • 関数型インターフェースとの関係

順番に見ていきましょう。

3パターンで覚えるラムダ式の基本形

ラムダ式の基本構文は、左が入力(引数)、右が処理です。この「矢印(->)」を境に読むと、ラムダ式はかなり読みやすくなります。

ラムダ式には3段階の省略ルールがあります。

省略パターン1:処理が1行なら{}を省略できる

{}あり

{}なし(1行処理ならOK)

省略パターン2:コンパイラが型を推測できるなら引数の型を省略できる

型あり

型なし(型推論が効く場合)

省略パターン3:引数が1つなら()も省略できる

()あり

()なし(引数が1つのときだけ):s -> System.out.println(s)

この3段階を組み合わせると、最終的にs -> System.out.println(s)という非常にコンパクトな形になります。

省略できる場所が多いので最初は戸惑うかもしれませんが、「どこまで省略できるか」より「何をやっているか」を先に読めるようになることを優先してください。

引数・戻り値パターン別の書き方一覧

実際のコードでよく使う形をパターン別にまとめます。

パターンラムダ式の書き方具体例
引数なし・戻り値なし() -> 処理() -> System.out.println("Hello")
引数1つ・戻り値なしx -> 処理x -> System.out.println(x)
引数1つ・戻り値ありx -> 式x -> x * 2
引数2つ・戻り値あり(x, y) -> 式(x, y) -> x + y
複数行の処理x -> { ... return ...; }下記参照

複数行になる場合は{}returnが必要です。

x -> {
    int result = x * 2;
    System.out.println("計算結果: " + result);
    return result;
}
Zetto

表を見るとパターンが多く感じますが、現場で頻出なのは「引数1つ・戻り値あり」と「引数2つ・戻り値あり」の2パターンです。

関数型インターフェースとの関係

ラムダ式を使うには、「関数型インターフェース(Functional Interface)」の理解が必要です。

関数型インターフェースとは、抽象メソッドがちょうど1つだけあるインターフェースのことです。@FunctionalInterfaceアノテーション(注釈)で明示されることが多く、Javaの標準ライブラリにはあらかじめよく使うものが用意されています。

特に覚えておきたい5つを以下にまとめます。

  • Runnable:引数なし・戻り値なし。() -> 処理の形で使う
  • Consumer<T>:引数Tを受け取り・戻り値なし。x -> 処理の形で使う
  • Supplier<T>:引数なし・戻り値Tを返す。() -> 値の形で使う
  • Function<T, R>:引数Tを受け取り・戻り値Rを返す。x -> 変換の形で使う
  • Predicate<T>:引数Tを受け取り・booleanを返す。x -> 条件の形で使う

実際のコードで見てみましょう。

// Consumer:受け取って処理するだけ
Consumer printer = s -> System.out.println(s);
printer.accept("Hello"); // Hello

// Function:受け取って変換して返す
Function doubler = x -> x * 2;
System.out.println(doubler.apply(5)); // 10

// Predicate:条件判定してbooleanを返す
Predicate isPositive = x -> x > 0;
System.out.println(isPositive.test(-1)); // false

ラムダ式を使う前提として、Javaのメソッドの概念をしっかり理解しておくことが重要です。メソッドの基礎が不安な方は、こちらの記事も参考にしてみてください。

Zetto

関数型インターフェースの種類を全部暗記しようとしなくて大丈夫です。最初はConsumer・Function・Predicateの3つを押さえておけば、現場で困ることはほとんどありませんよ。

匿名クラスとの違いとラムダ式のメリット

匿名クラスとの違い

匿名クラスとラムダ式を並べて比較し、ラムダ式が何を変えるのかを解説します。

  • 匿名クラスをラムダ式に書き換えてみよう
  • ラムダ式で何が変わるのか

順番に見ていきましょう。

匿名クラスをラムダ式に書き換えてみよう

匿名クラス(Anonymous Class)とは、クラスを定義しながら同時にインスタンス化する書き方です。

ラムダ式が登場する前のJavaでは、インターフェースを手軽に実装したいときに頻繁に使われていました。

僕がJavaを学び始めた頃、現場のコードで匿名クラスをよく見かけて「なんでこんなに長く書くんだろう」と感じていたのが正直なところです。

実際に比べてみましょう。

匿名クラスで書いた場合

List names = Arrays.asList("Taro", "Hanako", "Jiro");

Collections.sort(names, new Comparator() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

ラムダ式で書き換えた場合

List names = Arrays.asList("Taro", "Hanako", "Jiro");

Collections.sort(names, (a, b) -> a.compareTo(b));

処理の意図はまったく同じです。匿名クラスには「クラス定義・メソッド名・戻り値の型・@Override」など、本質的な処理と関係ない記述が大半を占めていました。ラムダ式ではそのノイズが消えて、「aとbを比較する」という核心だけが残ります。

Zetto

ラムダ式に書き換えるコツは、「処理の本体だけを抽出する」イメージを持つことです。

ラムダ式で何が変わるのか

ラムダ式を使うと、コードの見た目が変わるだけでなく「処理の渡し方」の概念が広がります。

処理を「値」のように扱える

ラムダ式を使うと、処理そのものを変数に代入したり、引数として渡したりできます。

// 処理を変数に代入する
Runnable greet = () -> System.out.println("こんにちは");

// 処理を引数として渡す
Thread t = new Thread(greet);
t.start();

これは「関数型プログラミング」の考え方で、処理をデータと同じように動かせる点がポイントです。

Stream APIとの組み合わせで真価を発揮する

ラムダ式が最も力を発揮するのは、Stream API(Java 8から使えるコレクション操作の仕組み)との組み合わせです。

List numbers = Arrays.asList(1, 2, 3, 4, 5);

// 偶数だけを取り出して2倍にする
List result = numbers.stream()
    .filter(n -> n % 2 == 0)   // 偶数だけ抽出
    .map(n -> n * 2)            // 2倍にする
    .collect(Collectors.toList());

// result = [4, 8]

for文でゼロから書くと10行近くになる処理が、3行のチェーンでスッキリ書けます。しかも「フィルタして・変換して・集める」という処理の流れが、コードをそのまま読むだけで伝わる点が大きなメリットです。

Zetto

ラムダ式の本当の強みはStream APIと組み合わせたときに出ます。この2つはセットで理解しておくと、コレクション操作の表現力がかなり上がります。

実務で使えるラムダ式の活用と注意点

実務での活用法

ここからは、現場でよく使われる書き方と初心者がつまずきやすいポイントを解説します。

  • 現場でよく見るラムダ式の使われ方
  • 初心者がつまずく落とし穴3つ
  • メソッド参照との使い分け基準
  • Java Silver試験で出るラムダ式のポイント

順番に見ていきましょう。

現場でよく見るラムダ式の使われ方

実務でラムダ式が使われる場面は主に以下の通りです。

  • リストの並び替え:Comparatorをラムダ式で渡す
  • コレクションのフィルタリング:Stream APIのfilterで条件に合う要素を絞る
  • リストの変換(マッピング):mapで別の形に変換したリストを作る
  • スレッド処理:Runnableをラムダ式でシンプルに書く
  • イベントリスナーの登録:UIフレームワークで処理を渡す

僕がJavaの現場案件で最もよく目にしたのは、リスト操作とStream APIの組み合わせでした。たとえば、ユーザーリストからアクティブ状態のユーザーだけ名前を取り出す処理はこう書きます。

List activeUserNames = users.stream()
    .filter(user -> user.isActive())
    .map(user -> user.getName())
    .collect(Collectors.toList());

このような書き方はビジネスロジックの中に普通に登場します。ラムダ式を読めないと、コードレビューやバグ調査のときに苦労しがちです。

ラムダ式とStream APIはArrayListなどのコレクションと合わせて使う場面が多いです。ArrayListの基礎を確認しておきたい方は、こちらの記事もあわせてチェックしてみてください。

初心者がつまずく落とし穴3つ

ラムダ式を使い始めるとよく出てくるつまずきポイントを解説します。

  • 変数キャプチャのfinal制約
  • thisの挙動の違い
  • 処理が複雑になると可読性が下がる

以下の3つを順番に見ていきましょう。

変数キャプチャのfinal制約

ラムダ式の中から外側のローカル変数を参照することを「変数キャプチャ」と言います。

このとき、参照できるのは実質的にfinalな変数(後から値が変更されない変数)だけです。

int count = 0;

Runnable r = () -> {
    count++; // コンパイルエラー!変数が実質finalでないため
};

解決策としては、インスタンス変数を使う方法や、AtomicInteger(スレッドセーフに値を更新できるクラス)を使う方法があります。

// AtomicIntegerを使う例
AtomicInteger count = new AtomicInteger(0);
Runnable r = () -> count.incrementAndGet(); // OK

thisの挙動の違い

ラムダ式と匿名クラスでは、thisが指すものが異なります。

public class MyClass {
    String name = "MyClass";

    void test() {
        // 匿名クラスのthis → 匿名クラス自身を指す
        Runnable anon = new Runnable() {
            @Override
            public void run() {
                System.out.println(this.getClass()); // 匿名クラス
            }
        };

        // ラムダ式のthis → MyClass(外側のクラス)を指す
        Runnable lambda = () -> {
            System.out.println(this.getClass()); // MyClass
        };
    }
}

ラムダ式は独自のスコープを持たないため、thisは必ず外側のクラスを指します。この挙動の違いを知らないと、思わぬバグにつながるので注意が必要です。

処理が複雑になると可読性が下がる

ラムダ式は短くてシンプルな処理に向いています。処理が長くなるほど、逆に読みにくくなる点に注意が必要です。

NG例:複雑な処理をラムダに詰め込む

List result = users.stream()
    .filter(u -> {
        if (u.getAge() > 20 && u.isActive() && u.getScore() >= 80) {
            // さらに長い処理...
            return true;
        }
        return false;
    })
    .collect(Collectors.toList());

OK例:複雑な処理は別メソッドに切り出す

List result = users.stream()
    .filter(u -> isValidUser(u))
    .collect(Collectors.toList());

「ラムダ式の中身が3行を超えてきたら、別メソッドに切り出す」が現場でよく使われる目安です。

Zetto

最初から全部覚えなくていいので、エラーが出たときに思い出せる程度の理解で十分ですよ。

メソッド参照との使い分け基準

ラムダ式と似た書き方に「メソッド参照(Method Reference)」があります。クラス名::メソッド名の形で書きます。

// ラムダ式
names.forEach(name -> System.out.println(name));

// メソッド参照(同じ意味)
names.forEach(System.out::println);

使い分けの基準はシンプルです。

  • 既存のメソッドをそのまま渡すだけのとき → メソッド参照
  • 引数を加工したり、条件を組み合わせたりするとき → ラムダ式

無理にメソッド参照に変えると逆に読みにくくなるケースもあるので、「どちらが処理の意図を伝えやすいか」で選ぶのが正解です。

Javaラムダ式の演習問題

演習問題

理解を深めるために、演習問題を3つ用意しました。

演習問題1:ラムダ式の基本構文を自分で書いてみよう

問題

  • Function<String, Integer>型のラムダ式を作る
  • 引数として受け取った文字列の長さ(文字数)を返す
  • 変数名はstrLengthとする
  • applyメソッドで"Hello"を渡して結果を表示する

解答例

Function strLength = s -> s.length();
System.out.println(strLength.apply("Hello")); // 5

解説

Function<String, Integer>は「String型を受け取り、Integer型を返す」関数型インターフェースです。s.length()が文字数を返す1行の式なので、{}returnを省略して書けます。

引数が1つなので()も省略し、s -> s.length()という形になります。

演習問題2:匿名クラスをラムダ式に書き換えよう

問題

以下の匿名クラスを使ったコードをラムダ式に書き換えてください。

List numbers = Arrays.asList(5, 2, 8, 1, 9, 3);

Collections.sort(numbers, new Comparator() {
    @Override
    public int compare(Integer a, Integer b) {
        return b - a; // 降順(大きい順)に並び替え
    }
});

解答例

List numbers = Arrays.asList(5, 2, 8, 1, 9, 3);

Collections.sort(numbers, (a, b) -> b - a);

解説

Comparator<Integer>は抽象メソッドがcompare1つだけの関数型インターフェースです。compareの処理本体(return b - a)だけをラムダ式として渡せます。

戻り値がある1行処理の場合、{}returnの両方を省略できます。引数は2つなので()は残します。

演習問題3:関数型インターフェースを使い分けよう

問題

以下の3つの処理に対して、適切な関数型インターフェースを選んでラムダ式を書いてください。

  1. 引数なしで、1〜100のランダムな整数を返す処理
  2. Stringを受け取り、大文字に変換して返す処理
  3. Integerを受け取り、0より大きければtrueを返す処理

解答例

// 1. Supplier:引数なし・値を返す
Supplier randomNum = () -> (int)(Math.random() * 100) + 1;
System.out.println(randomNum.get());

// 2. Function:Stringを受け取りStringを返す
Function toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("hello")); // HELLO

// 3. Predicate:Integerを受け取りbooleanを返す
Predicate isPositive = n -> n > 0;
System.out.println(isPositive.test(5));   // true
System.out.println(isPositive.test(-1));  // false

解説

使い分けのポイントは「引数と戻り値の組み合わせ」です。引数なし・値を返すならSupplier、型を変換して返すならFunction、条件判定してbooleanを返すならPredicateと覚えておくと、現場でもJava Silver試験でも迷わなくなります。

Zetto

演習問題を通じて、関数型インターフェースの種類とラムダ式の書き方がかなり身についたはずです。

ラムダ式を使いこなしてJavaの表現力を上げよう

使いこなし方

この記事では、Javaラムダ式の基本から実務での使い方まで解説しました。

  • ラムダ式はJava 8から登場:匿名クラスの冗長な書き方をシンプルにするための構文
  • 基本構文は(引数) -> {処理}:引数が1つなら()省略可、1行処理なら{}return省略可
  • 関数型インターフェースとセットで理解する:Consumer・Function・Predicateの入出力を覚えておく
  • 落とし穴は3つ:変数キャプチャのfinal制約・thisの挙動の違い・複雑処理での可読性低下
  • Stream APIと組み合わせると強力:filter・map・collectの組み合わせが現場の定番

ラムダ式は書いて動かしてみることで一番早く身につきます。演習問題のコードを実際にIDEで動かして、エラーが出たら読んで直して、を繰り返すのがおすすめです。

ラムダ式と並んでよく使われるfor文の使い方については、こちらの記事で詳しく解説しています。Stream APIとの違いも含めて確認しておくと、コレクション操作の引き出しが増えます。

Mirai

ラムダ式って最初は記号だらけで難しそうに見えたけど、仕組みがわかったら読めるようになってきた!

Zetto

その調子!「矢印の左が入力・右が処理」と読めるようになると、現場のコードがだいぶクリアに見えてくるよ。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次