ラムダ式

近年JavaやC#のように関数型言語ではない言語も関数型言語の特徴を取り入れるようになりました。関数型言語では関数をオブジェクトとして変数に代入したりメソッドの引数や戻り値とすることができ、必要なときに関数の手続きを呼び出すことができます。関数オブジェクトは即席的なコールバックとして使われたり、ストラテジパターンのプラガブルなストラテジとして使われたりします。

Javaには関数オブジェクトの仕組みはなかったのですが、即席的なコールバックやストラテジは匿名クラスを使って実現されていました。つまり従来から関数オブジェクトと同じような機能を実現する手段はあったのですが、Java SE8でその枠組みを整備した形になります。ラムダ式はそのうちの1つです。

Javaでは関数オブジェクトの代わりとして匿名クラスを使います。Javaは変数の型定義が必要な言語であることから 関数の型を定義するために関数型インタフェースが規定されました。また、匿名クラス全体を記述しなくても関数の処理内容だけを記述できるようにラムダ式が導入されました。(ラムダ式は匿名クラスとほぼ同じですがthisの扱いなど若干の違いがあります。)加えて、ラムダ式を更に省略した形で記述できるメソッド参照も取り入れられました。

この章ではこれらについて見て行きます。

この章で登場する総称型クラスのメソッドの多くの型引数が境界ワイルドカード型として定義されていますが、簡易的に境界ワイルドカードの記述を省略しています(例:<? extends T>を単に<T>と記述)。正確な定義はJava APIドキュメントを参照してください。

匿名クラス

匿名クラスはクラスの定義とインスタンス化を同時に行うことができるため、即席的なコールバックやプラガブルなストラテジを記述するような場合に良く使われます。

メソッドの中やメソッドを呼び出す箇所など 式を書ける箇所でクラスの定義とインスタンスの生成を同時に行います。匿名クラスとして生成できるのは 定義済みのクラスのサブクラスか定義済みのインタフェースを実装したクラスのいずれかです。あるクラスのサブクラスの匿名クラスを生成する場合は必要なメソッドをオーバーライドし、あるインタフェースを実装した匿名クラスを生成する場合は抽象メソッドを全て実装します。

匿名クラスは通常のクラス定義として書き換えることができますが、その場でしか使わないコードの場合はコードの可読性が上がるというメリットがあります。ただし、あまり複雑な処理を持つクラスを匿名クラスにしてしまうと返って可読性を落としかねないため 比較的短いコードの処理を行う場合に向きます。

匿名クラスは明示的なコンストラクタを持つことができません。その代わりにインスタンスイニシャライザを記述することができ、インスタンスフィールドを定義することもできます。

 

関数型インタフェース(Java SE 8~)

匿名クラスを関数オブジェクトのように扱うために 関数の型を表すための特殊なインタフェースが規定されました。それが関数型インタフェースです。関数型インタフェースでは 抽象メソッドが1つだけに限定され、その抽象メソッドの引数と戻り値が関数の型として利用されます

 

関数型インタフェースの条件

関数型インタフェースは定義されている抽象メソッドが1つだけのインタフェースです。唯一の抽象メソッドの他にクラスメソッドやデフォルトメソッドは持っていても構いません。また、インタフェースのスーパータイプである Objectのメソッドをオーバーライドした抽象メソッドがあっても構いません。(abstract String toString(); など)

関数型インタフェースの条件に一致するインタフェースは従来からありました(java.util.Comparatorやjava.lang.Runnableなど)。Java SE8からこの条件に一致するインタフェースを関数型インタフェースと呼ぶようになりました。

 

関数型インタフェースを定義する

前述の関数型インタフェースの条件を満たせば、独自に関数型インタフェースを定義することができます。インタフェースの定義の仕方は 通常のインタフェースと全く同じです。その際にインタフェース定義に@FunctionalInterfaceアノテーションを付けると、コンパイラが関数型インタフェースの条件を満たしているかどうかチェックをしてくれます。そのため 独自に関数型インタフェースを定義する際には 必ず@FunctionalInterfaceアノテーションを付けるようにします。

 

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

ラムダ式やメソッド参照の受け渡しを行いたいときに 毎回独自に関数型インタフェースを定義するのは面倒です。良く使われる標準的な関数型インタフェースが java.util.functionパッケージに用意されています。標準で用意されている関数型インタフェースに合致するものがあれば 極力それを使うべきです。そうすることによって 関数型インタフェースを独自に定義する手間を省くと共に、コードの読み手も理解し易くなります。また、標準で用意されている関数型インタフェースの多くはデフォルトメソッドを持っていて、それらを利用することもできます。

java.util.functionパッケージには 大別して次のような関数型インタフェースが用意されています。(T、U、R は総称型の型引数を表します)

標準で用意されている総称型の関数型インタフェース
引数 戻り値 インタフェース メソッド 概要
なし T Supplier get 引数なしで戻り値を返します。(supply:供給する)
T なし Consumer accept 引数一つ受け取り、戻り値は返しません。(consume:消費する)
T、U なし BiConsumer accept 引数二つ受け取り、戻り値は返しません。(bi:二つ)
T boolean Predicate test 引数一つ受け取り、boolean を返します。(predicate:断定する)
T、U boolean BiPredicate test 引数二つ受け取り、boolean を返します。
T R Function apply 引数一つ受け取り、任意の型の戻り値を返します。
引数と戻り値が同じ型でも構いませんが、同じ型の場合は別のインタフェースも用意されています。
T、U R BiFunction apply 引数二つ受け取り、任意の型の戻り値を返します。
引数と戻り値と全て同じ型でも構いませんが、同じ型の場合は別のインタフェースも用意されています。
T T UnaryOperator apply 引数一つ受け取り、同じ型の戻り値を返します。(unary:単項の)
T、T T BinaryOperator apply 引数二つ受け取り、同じ型の戻り値を返します。(binary:二項の)

上の表では引数、戻り値が総称型の型引数となっていますが、それぞれプリミティブ型に置き換えた関数型インタフェースも用意されています。例えばSupplierの場合は次のようになります。インタフェース名とメソッド名にプリミティブ型を表す名前が付け加えられています。

標準で用意されているプリミティブ型の関数型インタフェース(例:Supplierの場合)
引数 戻り値 インタフェース メソッド 概要
なし boolean BooleanSupplier getAsBoolean 引数なしで boolean 値を返します。
なし int IntSupplier getAsInt 引数なしで int 値を返します。
なし long LongSupplier getAsLong 引数なしで long 値を返します。
なし double DoubleSupplier getAsDouble 引数なしで double 値を返します。

プリミティブ型の引数、戻り値のメソッドは、総称型の関数型インタフェースとプリミティブ型の関数型インタフェースの両方に適合する場合があります。例えばlongをintに変換するメソッドではFunction<Long, Integer>とLongToIntFunctionの両方の関数型インタフェースが適合します。その場合は、プリミティブ型の関数型インタフェースを選択するようにします。大量に操作する場合 オートボクシング、オートアンボクシングによるパフォーマンス劣化が無視できなくなるためです。

この他にも java.util.functionパッケージにはたくさんの関数型インタフェースが標準で用意されています。

 

専用の関数型インタフェースを定義する方が良い場合

標準で用意されている関数型インタフェースに適合するものがあっても、専用の関数型インタフェースを定義した方が良い場合もあります。その最たる例がComparatorです。Comparatorの抽象メソッドのシグニチャはToIntFunctionと同じになりますが、次の理由からToIntFunctionは使わずComparatorを使います。

  • 広く使われていて、名前が関数型インタフェースの説明を果たしている
    名前が「ToIntFunction」では 入力を異なる型の出力に変換することしか分かりませんが、「Comparator」であれば比較のための関数オブジェクトであることが明確になります。
  • 抽象メソッドに一般契約が定められている
    Comparatorの抽象メソッドcompareTo()には守るべき一般契約がありますが、ToIntFunctionにしてしまうと 一般契約があることが忘れられてしまいます。
  • 特別なデフォルトメソッドが用意されている
    Comparatorには 比較を行うための様々なデフォルトメソッドが用意されていて、Comparatorにすることによって それらのデフォルトメソッドを利用することができます。

関数オブジェクトが 上の特性を1つでも持つのであれば、専用の関数型インタフェースを用意した方が良いかも知れません。

 

ラムダ式(Java SE8~)

ラムダ式は匿名クラスを簡潔に記述できるようにしたもので、匿名クラス全体を記述しなくても関数オブジェクトのように扱いたい関数部分だけをラムダ式で記述することができます。(ただしラムダ式と匿名クラスでは若干相違点があります。)

関数型インタフェースを匿名クラスで実装すると次のようになります。

これをラムダ式で記述すると次のようになります。

関数オブジェクトとして扱いたい関数部分だけを記述すれば良いので、書く方も楽ですし 読む方も読み易くなります。

ラムダ式ではSomeInterfaceのgetMessage()メソッドを実装しているのですが、メソッド名の指定がありません。これは 関数型インタフェースは抽象メソッドを一つしか持たないため ラムダ式で実装するメソッドは明確であり 指定が不要なためです。また、引数のidやnameの型指定がありませんが、ラムダ式で実装するメソッドがgetMessage()と明確になっていてメソッドの定義から型を推論できるため、型の指定は不要です。

尚、ラムダ式の引数に「_」(アンダースコア)一文字を使うことはできません(コンパイルエラーになります)。

 

ラムダ式の記法

引数の型の省略

ラムダ式はメソッド引数の型を省略することができます(記述しても問題ありません)。これは関数型インタフェースでメソッドの引数の型が決まっていて引数の型は明確になっているためです。(型推論

ほとんどの場合でラムダ式の型を省略することはできますが、場合によってはコンパイラが型を推論できないことがあります。その場合に限り 型を明示する必要があります

総称型の要素にラムダ式を適用するような場合は、総称型の型情報から型を推論します。そのため、原型を使うと型情報を取得できず型推論ができなくなるため注意が必要です。

 

引数を囲む () の省略

引数が一つの場合は 引数を囲む () を省略することができます(記述しても問題ありません)。引数が0または2つ以上の場合は必ず () が必要です。

 

 

ブロックとreturnの省略

処理が1文だけの場合は ブロックを表す{} を省略することができます。その場合はreturnも省略する必要があり、 returnを記述すると逆にコンパイルエラーになります。

 

ラムダ式の外のローカル変数へのアクセス

ラムダ式では ラムダ式の外で定義されたローカル変数を参照することができますが 値を変更することができません(プリミティブ型の場合は値を、参照型の場合は参照先を変更することができません)。また、ラムダ式から参照されるローカル変数は ラムダ式の外でも変更することができず 実質finalである必要があります

ラムダ式の外のローカル変数が参照型の場合は 参照先を変えることはできないのですが 参照先のオブジェクトの状態を変更することはできます。

ただし、ラムダ式の中で副作用を伴う処理を行うことは 関数型言語の思想から外れるため、このような使い方は望ましくありません。

また、匿名クラスの場合と同様に 非staticな文脈であれば ラムダ式からエンクロージングクラスのインスタンスフィールドにアクセスしたり、インスタンスメソッドを呼び出したりすることが可能です。

 

匿名クラスとの違い

thisが指し示す物

匿名クラスとラムダ式では thisが指し示す物が異なります匿名クラスの場合 thisは匿名クラス自身のインスタンスを表しますが、ラムダ式の場合 thisはラムダ式を記述したメソッドを持つクラス(エンクロージングクラス)のインスタンスを表します。そのため、staticな文脈でラムダ式を記述した場合 thisにはアクセスできないことになります。

 

 

 

匿名クラスでないとできないこと

関数オブジェクトが必要な箇所では ラムダ式を使う方が簡潔に記述でき望ましいです。しかし、ラムダ式は関数型インタフェースのインスタンスを作成する場合にしか使えません。また、ラムダ式ではthisで関数オブジェクト自身を参照することができません。したがって、次のような場合は ラムダ式で書くことはできず 匿名クラスを使う必要があります

  • 抽象クラスのサブクラスのインスタンスを作成したい場合
  • 複数の抽象メソッドを持つインタフェースのインスタンスを作成したい場合
  • thisで匿名クラス自身のインスタンスにアクセスが必要な場合

 

メソッド参照(Java SE 8~)

ラムダ式が1つのメソッド呼び出しで完結する場合、クラス(またはインスタンス)とメソッド名の指定だけで置き換えることができます。これをメソッド参照と呼びます。メソッド参照によって ラムダ式における決まりきったコードを取り除くことができ、記述を更に簡潔にすることができます。ラムダ式同様、メソッド参照も関数型インタフェースの変数や引数に渡すことができます。

メソッド参照を用いると、基本的には関数型インタフェースの抽象メソッドの引数と一致するメソッドを当てはめることができます(少し違った形もあります)。インスタンスメソッドの場合 2通りの指定方法があります。クラスメソッドとインスタンスメソッドに分けて見て行きます。

クラスメソッド

クラスメソッドのメソッド参照はシンプルです。

クラス名::メソッド名

クラスメソッドの引数の個数とそれぞれの型が関数型インタフェースの抽象メソッドと一致すればメソッド参照を渡すことができます。

Integerクラスのsum()クラスメソッドを例に挙げます。sum()クラスメソッドの定義は次の通りです。

public static int sum(int, int)

int型の引数を2つ取りint型の戻り値を返すため、IntBinaryOperator(int型の引数を2つ取りint型を返す)やBinaryOperator<Integer>(Integerの引数を2つ取りIntegerを返す)の変数に代入することができます。

 

インスタンスメソッド

インスタンスメソッドのメソッド参照は インスタンスの指定の仕方によって2通りの指定方法があります

  1. インスタンス変数名(またはインスタンスを返す式)::メソッド名(バウンド参照
  2. クラス名::メソッド名(アンバウンド参照

1番目のバウンド参照はクラスメソッドの場合と同様で、インスタンスメソッドの引数の個数とそれぞれの型が関数型インタフェースの抽象メソッドと一致すればメソッド参照を渡すことができます。

StringクラスのindexOf()メソッドを例に挙げます。indexOf()メソッドの定義は次の通りです。

public int indexOf(String)

String型の引数を1つ取りint型の値を返すため、ToIntFunction<String>(Stringの引数を1つ取り int型を返す)やFunction<String, Integer>(Stringの引数を1つ取り Integerを返す)の変数に代入することができます。

2番目のアンバウンド参照では レシーバインスタンスを特定するための引数を追加する必要があります。インスタンスメソッドの引数リストの先頭に レシーバインスタンスを特定する引数を追加し、その引数リストの個数と型が関数型インタフェースの抽象メソッドと一致すればそのメソッド参照を渡すことができます。

同じくStringクラスのindexOf()メソッドを例に挙げます。indexOf()メソッドの定義は次の通りです。(再掲)

public int indexOf(String)

引数はString型が1つですが、この先頭にレシーバインスタンスを特定する引数を追加するため indexOf(String, String)になり String型が2つになります。String型の引数を2つ取りint型の値を返すため、BiFunction<String, String, Integer>(Stringの引数を2つ取り Integerを返す)の変数に代入することができます。

アンバウンド参照はメソッド参照で混乱が生じ易いポイントだと思います(インスタンスメソッドと 関数型インタフェースの抽象メソッドで引数の個数と型が一致しないため)。

以上、クラスメソッドやインスタンスメソッドをメソッド参照として どのような関数型インタフェースに渡せるかを見てきました。続いて、コードを読むときに メソッド参照が指すメソッドを特定する方法を見ていきます。

 

コードを読むときのポイント:メソッド参照が指すメソッドの特定

メソッド参照が「インスタンス変数名(またはインスタンスを返す式)::メソッド名」の形の場合は、インスタンスメソッドのバウンド参照に限定されるためシンプルです

メソッドがオーバーロードされていなければ 名前でメソッドを特定できます。オーバーロードされている場合は、メソッド参照を渡した関数型インタフェースの抽象メソッドの引数と戻り値を確認し、一致するメソッドを特定します。

関数型インタフェースの抽象メソッドが呼び出された場合、引数がそのまま特定したメソッドに渡されますので、どのような処理になるのかは理解し易いと思います。

 

一方、メソッド参照が「クラス名::メソッド名」のときは クラスメソッド参照の場合と インスタンスメソッドのアンバウンド参照の場合があるので どちらか判別する必要があります

メソッドがオーバーロードされていなければ名前でメソッドを特定できます。オーバーロードされている場合は、メソッド参照を渡した関数型インタフェースの抽象メソッドの引数と戻り値を確認し、次のメソッドのいずれかと一致する物を探し特定します。

  • 関数型インタフェースの抽象メソッドと一致する引数と戻り値を持つクラスメソッド
  • 関数型インタフェースの抽象メソッドの先頭の引数を除いたものと一致する引数と戻り値を持つインスタンスメソッド

両方のメソッドを持つクラスを定義することはできますが、コンパイルが通るコードであれば 必ずどちらかに特定できます(これについては後述します)。

該当するメソッドがクラスメソッドの場合、関数型インタフェースの抽象メソッドが呼び出されると引数がそのまま特定したメソッドに渡されますので、どのような処理になるのか理解し易いと思います。

一方 該当するメソッドがインスタンスメソッドの場合、関数型インタフェースの抽象メソッドが呼び出されると先頭の引数がメソッドを呼び出すレシーバインスタンスとなり、残りの引数がメソッドに渡されます。

慣れれば一目で理解できるようになると思いますが、ロジカルに判別すると以上のような流れになります。

 

曖昧なメソッド参照

前述の通り クラスメソッド参照の場合と インスタンスメソッドのアンバウンド参照の場合、メソッド参照は同じように「クラス名::メソッド名」になります。一つのクラスで同じメソッドシグニチャのインスタンスメソッドとクラスメソッドを定義することはできません。しかし、インスタンスメソッドのアンバウンド参照では 引数リストの先頭にレシーバインスタンスを特定する引数を追加するため、メソッド参照の形が同じになるインスタンスメソッドとクラスメソッドを持ったクラスは問題なく定義することができてしまいます。そのようなクラスの例を次に挙げます。

SomeClassのsomeMethod()は 片方はクラスメソッドで もう片方はインスタンスメソッドですが、引数が異なるためオーバーロード可能で このクラスは問題なく定義できコンパイル可能です。そして、どちらのメソッドもメソッド参照で指定しようとすると「SomeClass::someMethod」になります。

このような場合は、このメソッドを「SomeClass::someMethod」の形のメソッド参照で参照しようとすると、どちらのメソッドを指すのか特定できないため 曖昧なメソッド参照としてコンパイルエラーとなります

そのため、コンパイルが通るコードであれば、メソッド参照が参照するメソッドは必ず一つに特定することができます

 

コンストラクタ参照

コンストラクタをメソッド参照として扱うこともできます。その場合はコンストラクタ参照と呼ばれます。コンストラクタ参照の場合はメソッド名が「new」になります。また、コンストラクタには戻り値がありませんが 生成するインスタンスを戻り値の型とみなします。コンストラクタの引数によって 使う関数型インタフェースは次のように異なります。

  • 引数なしのコンストラクタ:Supplier
  • 引数が1つでコンストラクタと同じ型:UnaryOperator
  • 引数が2つでコンストラクタと同じ型:BinaryOperator
  • 引数が1つでコンストラクタと異なる型:Function
  • 引数が2つでコンストラクタと異なる型:BinaryFunction

引数が多い場合などは 独自の関数型インタフェースを定義する必要があります。コンストラクタ参照の例を次に示します。

 

 

 

メソッド参照かラムダ式か

メソッド参照の方が記述が簡潔になるため、基本的にはメソッド参照が適用できる箇所ではメソッド参照が望ましいとされています。しかし、次のようにラムダ式の方が望ましい場合もあります。

  • 引数名が有益な情報を示す場合。
    例えば2つの同じ型の引数を取るメソッドで 引数の順番で結果が変わるような処理を行う場合、メソッド参照で渡すよりもラムダ式を渡した方が 読み易く保守もし易くなります。
  • クラス名が長い場合。
    クラスメソッド参照やインスタンスメソッドのアンバウンド参照では クラス名を記述する必要がありますが、クラス名が長い場合は ラムダ式の方が簡潔になることが多いです。

 

 

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