📑

Javaにおけるラムダ式とメソッド参照

2021/03/20に公開

はじめに

開いていただきありがとうございます。
会社の後輩に「StreamAPIが分からないので教えてください!」と言われたので、「どうせ教える用に資料作成するならzennで公開するか」と思い作成しました。
本記事ではStreamAPIで必ず使用する「ラムダ式とメソッド参照」について、リストのイテレート処理を従来の方法からラムダ式、メソッド参照を使用した書き方に変換しながら、説明していこうと思います。
(本題のStreamAPIに関しては別記事で公開する予定です。)
※StreamAPIに関しては以下のページで公開しております。

https://zenn.dev/s_t_pool/articles/49200b65930aa8b28430

リストのイテレート処理を例にする

今回は「リストのイテレート」というコレクション処理の基本を従来の方法からラムダ式、メソッド参照へ進化させます。
今回の例で使う不変コレクションは以下の通りです。

final List<String> player = Arrays.asList("Imai","Takahasi","Masuda","Matsumoto","Matsuzaka");

;

JDK8以前のやり方で書いてみる

このリストをイテレートし各要素を出力する従来の方法です。

// for文でループを回す(普通これが最初に思いつく。)
for (int i = 0; i < player.size(); i++){
    System.out.println(player.get(i));
}

// 拡張for文(次にこれが思いつく。)
for (String name : player){
    System.out.println(name);
}

出力結果は以下の通りになります。

Imai
Takahasi
Masuda
Matsumoto
Matsuzaka

このコードを書くときにだいたい「i < だっけ? i> だったかな?」と迷うことが自分はよくあります。
for文で書かれたコードは冗長でエラーが発生しやすいものになってしまいます。
このパターンはコレクション内の特定のインデックスの要素を操作する際にのみ便利なパターンです。
(ここまでは前回の内容でも紹介したのと同じです。)
次に拡張for文です。このイテレーションは裏でIteratorインタフェースを使用し、その hasNextメソッドとnextメソッドを呼び出します。
for文よりも特定のインデックスにおいてコレクションを変更する必要がないので、for文を比較すると優れたスタイルです。
出力結果は同じになります。
しかし、両方とも命令型のコードであり、モダンなJavaでは捨て去ることができます。
ということで、関数型のコードに進化させます。

// playerコレクションに対してforEach()メソッドを呼び出し、匿名Consumerインスタンスを渡す。
player.forEach(new Consumer<String>(){
    // ↓冗長なコード。
    public void accept(final String name) {
        System.out.println(name);
    }
});

JDK8ではIterableインタフェースにforEachという特別なメソッドが追加されたのでそれを使用しています。
forEachメソッドはConsumer型を引数に取り、Consumerのインスタンスはacceptメソッドによって与えられたものを文字通り「消費」します。
今回はコレクションに対してforEachメソッドを呼び出し、匿名Consumerインスタンスを渡します。
forEach()メソッドが与えられたConsumerインタンスのacceptメソッドをコレクション内の各要素で呼び出し任意の処理を行います。
今回は単に与えられた値を出力します。出力結果はfor文の時と同じになります。

Imai
Takahasi
Masuda
Matsumoto
Matsuzaka

しかし、冗長なので(もっと複雑な処理になると書く気、見る気が失せるので)、これを何とかす方法がラムダ式となります。

ラムダ式について

ラムダ式とは・・・

JDK8以降で導入された構文です。
関数型インターフェースの変数に代入する箇所ではラムダ式を渡すことができます。
ラムダ式とは以下のような感じのものです。

friends.forEach(name -> System.out.print(name));

ラムダ式の構文

ラムダ式は引数部 -> 処理本体で表すことができます。
引数部分の書き方には以下のパターンがあります。

  1. 基本形
    • 引数部分は普通のメソッド定義の引数部分と同じように書きます。
    • 引数部の定義方法
      • 型1 引数名1, 型2 引数名2, …
      • friends.forEach((final String name) -> System.out.print(name));
  2. 型の省略
    • 型推論の恩恵によりラムダ式の引数自体は型を省略することができます。複数の引数がある場合は、全ての引数について型を省略する必要があります。
    • 引数部の定義方法
      • 引数名1, 引数名2, …
      • friends.forEach((name) -> System.out.print(name));
  3. かっこの省略
    • 引数が1個しか無い場合は、引数を囲む丸括弧を省略することができます。(この場合は、型も省略しなければいけません。)
    • 引数部の定義方法
      • 引数名1
      • friends.forEach(name -> System.out.print(name));

ここで先ほどの「コレクションのイテレート処理」をラムダ式で書き直してみます。

ラムダ式で書いてみる

// ラムダ式の基本形
player.forEach((final String name) -> System.out.println(name));

1. 基本形で紹介した書き方になります。引数部に型を指定します。
続いて、2. 型の省略で紹介した書き方に変更してみます。

// パラメータの型情報を抜いた(型推論)
player.forEach((name) -> System.out.println(name));

引数部で指定した型がなくなりました。
この場合 Java コンパイラはコンテクストに基づいて、パラメータnameString型であると判断します。コンパイラは呼ばれたメソッドのシグネチャを検索し、そのメソッドが引数に取る関数型インタフェースを解析します。そして、インタフェースのabstractメソッドを参照してパラメータの数とそれぞれの型を判定します。
ラムダ式を記述する際にはすべてのパラメータに型情報を付与するか、まったく付与しないかのいずれか一方です。
渡された引数が1つであり、その型が推論される場合は次のように括弧を取り除くことができます。(3. かっこの省略)

// パラメータの型情報を抜き、括弧もなくす
player.forEach(name -> System.out.println(name));

出力結果はfor文、匿名Consumerインスタンスを使用した時と同じ結果になります。
ラムダ式で書く方法でも冗長と感じる人もいるかもしれません。理由としてはnameという変数が2回も出てきます。
ということで、さらにメソッド参照を使って書き換えます。(詳細は後述。)

処理本体の書き方には以下のパターンがあります。

  1. 基本形
    • 処理本体にメソッド本体と同じように処理を書くことができます。
    • ラムダ式がオーバーライドすべきメソッドは、代入先の関数型インターフェースからわかります。
    • 戻り値の型はそこで定義されているので、それに応じた値をreturn文で返すようにします。
    • 処理本体の定義方法
      • {文1; 文2; … return 戻り値;}
(int n) -> {
  System.out.println("test" + n);
  return n + 1;
}
  1. 戻り値がvoid
    • 関数型インターフェースのメソッドの戻り型がvoidの場合は、return文は省略することができます。
    • 処理本体の定義方法
      • {文1; 文2; … }
(int n) -> {
  System.out.println("test" + n);
}
  1. かっこの省略
    • 処理本体に文が1個しか無い場合は、処理本体を囲む波括弧を省略できます。
    • その場合は、returnと末尾のセミコロン「;」も記述しません。
    • 処理本体の定義方法
      • 戻り値がvoidの場合:
      • 戻り値がvoid以外の場合:戻り値の式
      • friends.forEach(name -> System.out.print(name));

先ほどラムダ式で書き換えた「コレクションのイテレート」処理は全て3. かっこの省略のパターンになります。

メソッド参照

メソッド参照とは・・・

JDK8以降で導入された構文です。
関数型インターフェースの変数にメソッドそのものを代入することができます。
メソッド参照とは以下のような感じのものです。

friends.forEach(System.out::print);

メソッド参照も見慣れないととっつきにくいものですが、慣れてしまえば読めるようになります。

メソッド参照の構文

メソッド参照は以下の構文で書くことができます。

// staticメソッドの場合
クラス名::メソッド名
// インスタンスメソッドの場合
インスタンス変数名::メソッド名

要するに、呼び出したいメソッド名の直前を::にし、メソッドの引数部分(丸括弧部分)を除去すれば、メソッド参照として渡すことができます。
基本的には、ラムダ式がただ引数を渡しているだけであれば、メソッド参照と入れ替えることができます。
しかし、パラメータを引数として渡す前に処理を行う場合呼び出しの結果を返す前にその内容を修正しなければならない場合はメソッド参照は利用できません。
「リストのイテレート処理」をメソッド参照を使って書いてみます。

// メソッド参照の登場
player.forEach(System.out::println);

出力結果はfor文、匿名Consumerインスタンス、ラムダ式の時と同じになります。
Javaではコード本体を任意のメソッドに置き換えることができます。
forEachメソッドの中にループ処理が隠れました。実装者が明示しなくていいということになります。「i < だっけ? i> だったかな?」と迷うことがなくなります。

最後に・・・

最後まで読んでいただきありがとうございました。
内容等の不備がございましたらコメントで教えていただきますと幸いです。

参考

Discussion