広告

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 is not declared sealed and has just one abstract method (aside from the methods of Object), and thus represents a single function contract.

ラムダ式の前に、まずは関数型インタフェースを見ていきましょう。
関数型インタフェースはラムダ式の基本になります。

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

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

このように、1つの抽象メソッド を持つインタフェースを、関数型インタフェースといいます。
関数型インタフェースでは、2つ以上の抽象メソッドは持てません。

上記の Runnable の例では、run メソッドが抽象メソッドですね。


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

厳密には必須ではないのですが、関数型インタフェースであることを明示するのに @FunctionalInterface アノテーションが使えます。
@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.

ラムダ式とは、簡単にいうと

  • 関数型インタフェースの インスタンスを生成

するための簡易的な書きかたです。

次のように書くのが基本となります。

() -> {
}

ラムダ式はインスタンスを生成するので、結果を変数に代入できます。

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();
}

パラメータ

関数にパラメータが必要となる例を見てみましょう。
FuncSample は、int 型の x と 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つのパラメータを受け取る BiConsumerBiFunction
  • プリミティブ型を扱う IntConsumer や LongSupplier

などがあります。

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

Stream でラムダ式を使う例

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

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

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

少しだけコード例をご紹介します。
まずはラムダ式を使った例です。

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

// 大文字だけを抽出(filter)して、
// その文字が3つ続く文字列に変換(map)して、
// 結果をリストで取得します。

final var result = stream
        .filter(c -> Character.isUpperCase(c))
        .map(c -> String.valueOf(c).repeat(3))
        .toList();

System.out.println(result); // [AAA, CCC, DDD]

次に、ラムダ式を使わない例です。

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

final var result = 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);
            }
        }).toList();

System.out.println(result); // [AAA, CCC, DDD]

ラムダ式を使ったコードに比べて、ちょっと見た目がごちゃつきますよね。
あとは、記述量が多くなるので、単純にコーディングが遅くなりそうです。

関連 : ストリームの基本 (Stream)

まとめ

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

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

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


関連記事

ページの先頭へ