【Java】Javaのラムダ式の基本を分かりやすく解説

Java

こんにちは、かつコーチです。
匿名クラスで1行のロジックを書くためだけに何行もコードを書いて、冗長だと感じたことはありませんか。
今回はJavaのラムダ式(無名の関数をその場で定義できる書き方)の基本と、匿名クラスとの違いを解説します。

ラムダ式とは?

なぜラムダ式が必要なのか

Java 7以前、処理の一部を「あとで渡す」ときは匿名クラスを使うのが定番でした。
例えばRunnableを使ってスレッドの処理を渡す場合、次のように書く必要がありました。

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("処理を実行します");
    }
};
task.run();

run()の中身1行を書くためだけに、クラス名・アノテーション・メソッドのシグネチャを毎回書くのは冗長です。
Java 8で導入されたラムダ式は、この「処理そのものを値として渡す」という考え方をシンプルな構文で実現します。

ラムダ式の基本構文

Runnable task = () -> System.out.println("処理を実行します");
task.run();

(引数) -> 処理という形が基本です。
引数がない場合は()、1つの場合は括弧を省略できる、複数行の場合は{}で囲むなど、状況に応じて書き方が変わります。

基本の書き方

引数の数に応じた書き方

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

// 引数なし
Supplier<String> greeting = () -> "こんにちは";

// 引数1つ(括弧は省略可能)
Function<Integer, Integer> square = x -> x * x;

// 引数2つ(括弧は必須)
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

System.out.println(greeting.get());       // こんにちは
System.out.println(square.apply(5));      // 25
System.out.println(add.apply(3, 4));      // 7

引数が1つのときはx -> x * xのように括弧を省略できますが、2つ以上になると(a, b) -> a + bのように括弧が必須になります。
この違いを知らずに書いてコンパイルエラーになる方をよく見かけるので、最初に押さえておきましょう。

複数行の処理を書く場合

Function<Integer, String> judge = score -> {
    if (score >= 80) {
        return "合格";
    } else {
        return "不合格";
    }
};

System.out.println(judge.apply(85)); // 合格

処理が複数行になる場合は{}で囲み、値を返すときはreturnを明示的に書きます。
1行で完結する場合は{}returnを省略できるので、シンプルな処理ほど短く書けます。

よくあるつまずきポイント・エラー対処

ラムダ式の中から外の変数を書き換えようとしてエラーになった

実際に私が最初につまずいたのは、ラムダ式の中でループのカウンターを書き換えようとしたケースです。

// ❌Before:ラムダ式の外の変数を書き換えようとする
int count = 0;
Runnable increment = () -> {
    count++; // コンパイルエラー
};

このコードは error: local variables referenced from a lambda expression must be final or effectively final というエラーになります。
ラムダ式から参照する外部の変数は、実質的final(一度代入したあと値を変更しない変数)でなければならないというルールがあるためです。

// ✅After:配列やAtomicIntegerなど、書き換え可能な入れ物を使う
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
Runnable increment = () -> count.incrementAndGet();

increment.run();
System.out.println(count.get()); // 1

どうしても値を書き換えたい場合は、AtomicIntegerのように中身を書き換えられるオブジェクトを使うのが定石です。

応用・一歩先の使い方

ラムダ式が代入できる型:関数型インターフェース

ラムダ式は好きな型に代入できるわけではなく、抽象メソッドを1つだけ持つ関数型インターフェース@FunctionalInterfaceが付けられるインターフェース)にのみ代入できます。

@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

Calculator multiply = (a, b) -> a * b;
System.out.println(multiply.calc(3, 4)); // 12

自作の関数型インターフェースを作ることもできますが、次回以降解説するFunctionPredicateなど、java.util.functionパッケージに用意された標準の関数型インターフェースを使う方が再利用性は高くなります。

匿名クラスとラムダ式の使い分け

匿名クラスは複数の抽象メソッドを持つインターフェースやフィールドが必要な場合に使い、単一メソッドの処理を渡すだけならラムダ式を選ぶのが基本方針です。
可読性の面でも、単純な処理はラムダ式の方が意図が伝わりやすくなります。

まとめ

この記事のポイント

  • ラムダ式は処理そのものを値として渡せる、匿名クラスを簡潔にした書き方
  • 引数1つは括弧省略可、2つ以上は括弧必須、複数行は{}returnが必要
  • ラムダ式から参照する外部変数は実質的finalでなければならない
  • ラムダ式は抽象メソッドを1つだけ持つ関数型インターフェースにのみ代入できる

次に読むべき記事

  • 関数型インターフェース(Function・Predicate等)
  • Stream API基本

タグ: Java, 中級者向け, 関数型プログラミング

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