Javaのラムダ式と戦う

ラムダ関数難しすぎわろた。

知っている人にしかわからない、矢印を使った例の式ですね。最初見た時は何の悪ふざけかと思いました。

それとなく使ってきましたが、ビルダーとか使い始めると避けては通れないので整理しようと思います。

ラムダ式の基本

【例題】

以下のラムダ式について考える。

Runnable runner -> { System.out.println("Hi!"); };

Runnableはインターフェイス

この式と同じ意味で、以下の形式でもよく見かける。

Runnable runner = () -> { System.out.println("Hi!"); };

これを見ると、runnerに何かの戻り値を代入しているように見えるがそうではない。

runnerは、クラスインスタンスであり、以下のようにして処理を実行できる。

runner.run();  // Hi!

run()はどこから出てきたのか?というと、 Runnableインターフェイスが持つメソッドである。

改めて式を見てみる。

Runnable runner -> { System.out.println("Hi!"); };

ここまでの内容をまとめると、左辺のrunner は、

  • run()というメソッドを持つ
  • Runnableインターフェイスを実装した
  • 無名クラスのインスタンス

である。

ラムダ式の大きな2つの利点 

インターフェイスをそのままインスタンス化できる

インターフェイスは本来、以下のように継承 (implement) してクラス化し、メソッドを実装 (オーバーライド) しなければ使用できない。

public class Runner implements Runnable {
  @Override
  public void run() {
    System.out.println("Hi");
  }
}
Runner runner = new Runner();
runner.run(); // "Hi"

このインターフェイスに付随する、継承~クラス化~メソッド実装を1手に行えるのが、ラムダ式の1つ目の利点。

Runnable runner -> { System.out.println("Hi!"); };

右辺が、 Runnableインターフェイスの実装 (オーバーライド) となっているわけである。

実装を都度変更できる

改めて式を見てみる。

Runnable runner -> { System.out.println("Hi!"); };

先ほど話した通り、右辺にあたる式が、run() メソッドの実装 (オーバーライド)となっている。

そして、この実装はインスタンスを生成するたびに、 自由に変更が効く。

Runnable runner -> { System.out.println("Hi!"); };
Runnable runner2 -> { System.out.println("Bye!"); };
runner.run();  // Hi!
runner2.run();  //Bye!

このようにインターフェイス1つあれば、メソッドを都度、動的に指定して使用できる。

これが2つ目の、 そしてラムダ式の本質的な最大の利点となっている。

ラムダ式の制約

改めて式を見てみる。

Runnable runner -> { System.out.println("Hi!"); };

右辺で渡した処理がメソッドの実装、つまり、run()のオーバーライド部分にあたる。

ここで1つ疑問が浮かぶ。

オーバーライドでは、上書きするメソッドを指定するはず。この式では明確に、『run() メソッドを上書きする』とは書かれていない。

Runnableインターフェイスの、どのメソッドをオーバーライドしているのかはどのようにしてわかるのか?

もちろん、この記述ではわかりようがなく、ラムダ式を使用する際には、1つ条件がある。

それは、ラムダ式で使用できるインターフェイスは、1つのメソッドしか持っていないという条件だ。

1つのメソッドしかないのだから、 必然的にそのメソッドをオーバーライドした扱いになる。

改めて式を見てみる。

Runnable runner -> { System.out.println("Hi!"); };

ここまで理解した内容から、改めてrunnerインスタンスの特徴をまとめる。

  • runnerは、Runnableインターフェイスのメソッドを実装している
  • runnerが使用できるメソッドは、run() の1つだけ(Runnableはそもそもメソッドを1つしか持っていない)
  • runner.run() は、右辺で渡した処理を実行する(引数で渡した処理でオーバーライドしている)

型が書かれていない場合

さらに情報が少ないケースもある。

a -> b

これしか書かれていないこともある。

ラムダ式は、以下のようなケースでさらに省略することができる。

引数 (a) を必要とするインターフェイス Samepleがあるとする。また、Sapmeleをラムダ式を使用して実装する。 処理はただbを返すのみとする。

Sample sample (a) -> {return b};

このとき、以下のように、さらに省略できる余地がある。

  • 左辺の引数が1つであれば、型の記載を省略できる
  • 右辺の処理が1つだけなら{}とreturnを省略できる
sample a -> b;

ここで、sampleが無名関数なら、変数名も必要としないので、以下となる。

a -> b;

無名関数は、前提知識として省略する。すまない。

ラムダ式とインターフェイス

ラムダ式は、インターフェイスを手軽にインスタンス化できるという点が大きな特徴だ。

この仕様を利用して、BuilderやFactoryの実装で良く登場する。

引数に渡すBuilderによって動的に処理を変える実装は、ラムダ式の目的と一致するので、Builderの実装にはよくラムダ式が使われるのだ。

例えば以下は、JUitでContextをMockする際に使用するコードだ。細かい仕様は置いておいて、ラムダ式の動きについて着目する。

NamingManager.setInitialContextFactoryBuilder (env -> factory);

まず、NamingManagerの仕様を見ると、メソッド setInitialContextFactoryBuilder は、以下のように使用する。

void setInitialContextFactoryBuilder (InitialContextFactoryBuilder builder)

引数として与えるInitialContextFactoryBuilderについて調べると、InitialContextFactoryBuilderはインターフェイスであることがわかる。

ここで、引数にインターフェイスを渡す???と混乱するが、ビルダーやファクトリーの実装はこのようになっていることが多い。

細かい実装は説明のしようがないので、引数に渡すビルダーで、処理の内容を変更できる、とだけざっくり覚えておき、ここでは、『インターフェイスを引数に渡す』をどのように実装するのか?に着目する。

改めて式を見てみる。

NamingManager.setInitialContextFactoryBuilder (env -> factory);

ラムダ式無しでこれを実装すると、 以下となる。

// 無名関数でInitialContextFactoryBuilder インターフェイスを実装した無名クラスのインスタンスを生成
InitialContextFactoryBuilder factoryBuilder = new InitialContextFactoryBuilder() {
  @Override
  public InitialContextFactory create InitialContextFactory (Hashtable environment) {
    return factory;
  }
};
setInitialContextFactoryBuilder(factoryBuilder);

これをラムダ式に変換すると、2行で済む。
※個人的には、ソースコードから意味の伝わるこのレベルの記載に留めておくのがおすすめ

InitialContextFactoryBuilder factoryBuilder = env -> factory;
setInitialContextFactoryBuilder (factoryBuilder);

2行にfactoryBuilderが共通して存在するので、以下のように記述できる。

setInitialContextFactoryBuilder (env -> factory);

この式だけではもう何をやっているのかよくわからないが、 ドキュメントではえてしてこのような記載となっていることが多い。

コメント

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