Java : ラムダ式の基本

Java 8でラムダ式が言語仕様に追加されました。
そして、Java 8で追加されたAPIには、ラムダ式で使うことを想定しているものがあります。(StreamOptionalなど)

ぜひラムダ式を理解して、Java 8のAPIを使いこなしていきましょう。

本記事では、そんなラムダ式の基本的な使い方を解説していきます。


概要

さっそくコードを見てみましょう。

public void main() {
    final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("Run!");
        }
    };

    runnable.run(); // Run!
}

メソッド内でRunnableインタフェースの匿名クラス(無名クラス)を作成しています。

Runnableは、スレッドを使うときによくお世話になるインタフェースですね。
スレッドに渡すRunnableを、このように匿名クラスで作成することはよくあるのではないかな、と思います。

これがラムダ式だと、

public void main() {
    final Runnable runnable = () -> {
        System.out.println("Run!");
    };

    runnable.run(); // Run!
}

このように書けます。

関数型インタフェース

ラムダ式の前に、まず関数型インタフェースを解説します。

9.8. Functional Interfaces
A functional interface is an interface that has just one abstract method (aside from the methods of Object), and thus represents a single function contract.

関数型インタフェースは、Java言語仕様として組み込まれています。

それでは、標準APIの関数型インタフェースであるRunnableの定義を見てみましょう。

@FunctionalInterface
public interface Runnable {
    void run();
}

このように、1つの抽象メソッドを持つインタフェースを、関数型インタフェースといいます。
Runnableの例では、runメソッドが抽象メソッドですね。

そして、アノテーション @FunctionalInterface をつけます。

インタフェース型の宣言を、Java言語仕様に定義されている関数型インタフェースとすることを目的としていることを示すために使われる情報目的の注釈型です。

厳密には必須ではないのですが、関数型インタフェースということを明示するためにぜひつけましょう。
@FunctionalInterfaceをつけて、2つ以上の抽象メソッドを定義するとコンパイルエラーになってくれます。

コンパイルエラーで早期に間違いに気づけるのは大事です。

// コンパイルエラーが発生します。
@FunctionalInterface
public interface NgFuncSample {

    void run1();

    void run2();
}

抽象メソッドでなければ、複数定義されていても問題ありません。
具体的にはstaticメソッドやdefaultメソッドです。

@FunctionalInterface
public interface FuncSample {
    // 抽象メソッド
    void run();

    default void d1() {
        System.out.println("d1");
    }

    default void d2() {
        System.out.println("d2");
    }

    static void s1() {
        System.out.println("s1");
    }

    static void s2() {
        System.out.println("s2");
    }
}
final FuncSample func = () -> {
    System.out.println("Run!");
};

func.run(); // Run!

// defaultメソッド
func.d1(); // d1
func.d2(); // d2

// staticメソッド
FuncSample.s1(); // s1
FuncSample.s2(); // s2

ラムダ式

15.27. Lambda Expressions
A lambda expression is like a method: it provides a list of formal parameters and a body - an expression or block - expressed in terms of those parameters.

ラムダ式もJava言語仕様に組み込まれています。

ラムダ式は、関数型インタフェースのインスタンスを生成します。

() -> {
}

という書き方が基本となります。
ラムダ式はインスタンスを生成するので、結果を変数に代入できます。

final Runnable runnable = () -> {
    System.out.println("Run!");
};

runnable.run(); // Run!

もしくは、メソッドのパラメータに直接渡すことが多いかもしれません。

public void main() {
    func(() -> {
        System.out.println("abcd");
    }); // abcd
}

public void func(Runnable runnable) {
    runnable.run();
}

パラメータ

関数にパラメータがあるときは、( ) にパラメータを書きます。
クラスメソッドのパラメータ定義のような感じですね。

// int xとint yを受け取る関数型インタフェース
@FunctionalInterface
public interface FuncSample {
    void accept(int x, int y);
}
final FuncSample func = (int x, int y) -> {
    System.out.println(x + y);
};

func.accept(1, 2); // 3

さらに、パラメータの型(int)も省略できます。

final FuncSample func = (x, y) -> {
    System.out.println(x + y);
};

func.accept(1, 2); // 3

そしてさらに、処理が1文の場合は { } も省略できます。

final FuncSample func = (x, y) -> System.out.println(x + y);

func.accept(1, 2); // 3

戻り値

続いて、戻り値のある関数型インタフェースの例です。

// int xとint yを受け取りintを返す関数型インタフェース
@FunctionalInterface
public interface FuncSample {
    int apply(int x, int y);
}

ラムダ式で値を返すにはreturn文を使います。

final FuncSample func = (x, y) -> {
    return x + y;
};

final int result = func.apply(1, 2);
System.out.println(result); // 3

さらに、処理が1文の場合は { }return文も省略できます。

final FuncSample func = (x, y) -> x + y;

final int result = func.apply(1, 2);
System.out.println(result); // 3

標準で用意されている関数型インタフェース

このパッケージ内のインタフェースはJDKで使用される汎用の関数型インタフェースですが、これらはユーザーコードでも使用可能です。

標準APIのjava.util.functionパッケージには、汎用的に使える関数型インタフェースが多数用意されています。
基本的にはここにあるもので済むことが多いですね。

※Runnbaleはjava.langパッケージとなります。

よく使う関数型インタフェースをご紹介します。

API 戻り値 パラメータ コード例
Runnbale なし なし
final Runnable func = () -> System.out.println("abcd");

func.run(); // abcd
Consumer<T> なし (T param)
final Consumer<String> func = (s) -> System.out.println(s);

func.accept("abcd"); // abcd
Supplier<T> T なし
final Supplier<String> func = () -> "abcd";

final String result = func.get();
System.out.println(result); // abcd
Function<T,​R> R (T param)
final Function<String, Integer> func = (s) -> {
    // 16進数表記の文字列を数値に変換
    return Integer.decode(s);
};

final Integer result = func.apply("0xff");
System.out.println(result); // 255
Predicate<T> boolean (T param)
final Predicate<String> func = (s) -> {
    // すべて大文字であることをチェック
    final var upperCase = s.toUpperCase();
    return s.equals(upperCase);
};

System.out.println(func.test("abcd")); // false
System.out.println(func.test("ABcd")); // false
System.out.println(func.test("ABD")); // true

この他にも

  • 2つのパラメータを受け取るBiConsumerやBiFunction
  • プリミティブ型を扱うIntConsumerやLongSupplier

などがあります。

詳細は、公式API仕様の「java.util.function (Java SE 15 & JDK 15)」をご確認ください。

Streamでラムダ式を使う例

順次および並列の集約操作をサポートする要素のシーケンスです。
...
そのようなパラメータは常に関数型インタフェース(Functionなど)のインスタンスであり、通常はラムダ式やメソッド参照になります。

Java 8で追加されたAPIに、Streamインタフェースがあります。
このインタフェースでは、ふんだんに関数型インタフェースを使います。

API仕様にも、通常はラムダ式(またはメソッド参照)を使うことを想定している記述がありますね。

少しだけコード例をご紹介します。

// 文字のストリームです。
final var stream = Stream.of('A', 'b', 'C', 'D', 'e');

// 大文字だけを抽出(filter)して、
// その文字が3つ続く文字列に変換(map)して、
// 順番に(forEach)結果を出力します。

// AAA
// CCC
// DDD
stream.filter(c -> Character.isUpperCase(c))
        .map(c -> String.valueOf(c).repeat(3))
        .forEach(s -> System.out.println(s));

ラムダ式を使わないと、次のようなコードになります。

final var stream = Stream.of('A', 'b', 'C', 'D', 'e');

// AAA
// CCC
// DDD
stream.filter(new Predicate<Character>() {
    @Override
    public boolean test(Character c) {
        return Character.isUpperCase(c);
    }
}).map(new Function<Character, String>() {
    @Override
    public String apply(Character c) {
        return String.valueOf(c).repeat(3);
    }
}).forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

ラムダ式に比べて、ちょっと見た目がごちゃっとしますね。
あとは、記述量が多いと単純にコーディングが遅くなります。

もう少しStreamのコード例を見たいかたは、「Stream(ストリーム) - API使用例」の記事も参考にしていただけたら幸いです。

まとめ

ラムダ式、いかがでしたでしょうか?
最初はJavaらしからぬシンプルさに戸惑うかもしれません。

個人的には、最初は型がいろいろと省略されているので分かりづらいな、と思いました。
ただ、IDEを使っていれば型も追いやすいので、今はシンプルな表記も悪くはないなと思っています。

今回ご紹介しきれなかったものとして、メソッド参照、コンストラクタ参照があります。
メソッド参照の基本」の記事でご紹介しているので、そちらも合わせてご参照ください。


関連記事

ページの先頭へ