S-JIS[2014-03-23/2014-08-09] 変更履歴

Javaラムダ式

Javaのラムダ式について。


ラムダ式

ラムダ式は、JDK1.8で導入された構文。
関数型インターフェース(抽象メソッドが1つだけ定義されているインターフェース)の変数に代入する箇所ではラムダ式を渡すことが出来る。

見た目上は、無名内部クラス(匿名クラス)を短く記述できる記法と言える。

匿名クラスの例 ラムダ式の例
Runnable runner = new Runnable() {
  @Override
  public void run() {
    System.out.println("example");
  }

};
runner.run();
Runnable runner = () -> {
  System.out.println("example");
}
;
runner.run();
List<Integer> list = Arrays.asList(1, 3, 2);
Collections.sort(list, new Comparator<Integer>() {
  @Override
  public int compare(Integer o1, Integer o2) {
    return Integer.compare(o1, o2);
  }
});
List<Integer> list = Arrays.asList(1, 3, 2);
Collections.sort(list, (o1, o2) -> Integer.compare(o1, o2));

ラムダ式は関数型インターフェースのメソッドを実装(オーバーライド)することになる。
ラムダ式の代入先である関数型インターフェースには抽象メソッドが1つしか無い為、ラムダ式がどのメソッドを実装することになるのかは選択の余地なく決まる。
(関数型インターフェースでない場合、つまり抽象メソッドが0個か2個以上の場合は実装対象のメソッドを決定できない為、コンパイルエラーになる)


ちなみに、ラムダは「lambda」と書く。
「b」が入っているので「ラムブダ」に見えてしまうこともあるが、「らむだばだばだば」みたいにウルトラマンの音楽にからめたネタにするのは誰かがやっていると心得よう(笑)


ラムダ式の構文

ラムダ式は、「引数部 -> 処理本体」という形式で表す。
(引数部と処理本体を矢印っぽい記号で結ぶ。Scalaだと「=>」を使うのだが、数学的には「→」 (一本線の矢印)の方が正しいらしい?
 まぁ、JavaはC言語系の言語なので、C言語に在った「->」を使いまわすのは理解できる。あと、「=>」だと不等号「>=」と間違う人も居るかも?

いわば、メソッド名と戻り型の宣言が無いメソッド定義となる。

引数部分の書き方
引数部分の定義方法 説明 ラムダ式の例
基本形 (型1 引数名1, 型2 引数名2, …) 引数部分は普通のメソッド定義の引数部分と同じように書く。 () -> {}
(String s) -> {}
(int n1, int n2) -> {}
型の省略 (引数名1, 引数名2, …) ラムダ式がオーバーライドすべきメソッドは、代入先の関数型インターフェースから分かる。
引数の型はそこで定義されているので、ラムダ式の引数自体は型を省略することが出来る。
複数の引数がある場合は、全ての引数について型を省略する必要がある(一部だけの省略は出来ない)。
() -> {}
(s) -> {}
(n1, n2) -> {}
括弧の省略 引数名 引数が1個しか無い場合は、引数を囲む丸括弧を省略することが出来る。
(この場合は、型も省略しなければならない)
s -> {}

なお、ラムダ式の引数名には単一アンダースコア「_」を使うことが出来ない。
(ラムダ式の引数以外の場所で単一アンダースコアを識別子(変数名等)に使うとコンパイル時に警告になるが、ラムダ式の引数ではコンパイルエラーになる)
Scalaでは単一アンダースコアの引数名に特別な意味があるので、そういった他言語と紛らわしくないようにする為、あるいは将来的にScalaの様にプレースホルダーとして使えるようにする為かもしれない。

処理本体の書き方
処理本体の定義方法 説明 ラムダ式の例 Scalaの関数の例
基本形 {
  文1;
  文2;
  …
  return 戻り値;
}
処理本体は、基本的にはメソッド本体と同じ。

ラムダ式がオーバーライドすべきメソッドは、代入先の関数型インターフェースから分かる。
戻り値の型はそこで定義されているので、それに応じた値をreturn文で返すようにする。

Scalaだと最後のreturnを省略できるのだが、Java8ではそういう省略は出来ない)
(int n) -> {
  System.out.println("test" + n);
  return n + 1;
}
(n : Int) => {
  println("test" + n)
  n + 1
}
戻り型がvoid {
  文1;
  文2;
  …
  /*return;*/
}
関数型インターフェースのメソッドの戻り型がvoidの場合は
return文は(当然)省略可能。
(int n) -> {
  System.out.println("test" + n);
}
(n : Int) => {
  println("test" + n)
}
括弧の省略
(戻り型がvoidの場合)
処理本体に文が1個しか無い場合は、処理本体を囲む波括弧を省略できる。
その場合は、returnと末尾のセミコロン「;」も記述しない。
(int n) -> System.out.println("test" + n) (n : Int) => println("test" + n)
戻り値の式
(戻り型がvoid以外の場合)
(int n) -> n + 1 (n : Int) => n + 1

ちなみに、Scalaではプレースホルダーを用いて変数名も省略できる(場合がある)。

ラムダ式の例 Scalaの関数の例 備考
(int n) -> { return n + 1; } (n : Int) => { n + 1 } 引数が1個の場合の基本形
(int n) -> n + 1 (n : Int) => n + 1 文が1つだけなので、波括弧(とreturn)を省略可能
(n) -> n + 1 (n) => n + 1 引数の型を省略
n -> n + 1 n => n + 1 引数が1個の場合は丸括弧を省略可能
  _ + 1 プレースホルダー
(int n1, int n2) -> { return n1 + n2; } (n1 : Int, n2 : Int) => { n1 + n2 } 引数が2個の場合の基本形
(int n1, int n2) -> n1 + n2 (n1 : Int, n2 : Int) => n1 + n2 文が1つだけなので、波括弧(とreturn)を省略可能
(n1, n2) -> n1 + n2 (n1, n2) => n1 + n2 引数の型を省略
  _ + _ プレースホルダー

ラムダ式の変数のスコープ

ラムダ式で使う変数は、ラムダ式が定義されている場所の変数とスコープ(範囲)が同じになる。

ラムダ式の例 説明 匿名クラスの例
void scope1() {
  int n = 123;

  Runnable runner = () -> {
    System.out.println(n);
  };
  runner.run();
}
ラムダ式の外側で定義されている変数を、
ラムダ式内部から参照することが出来る。
void scope1a() {
  int n = 123;

  Runnable runner = new Runnable() {
    @Override
    public void run() {
      System.out.println(n);
    }
  };
  runner.run();
}
匿名クラスの内側からも
外側の変数を参照できる。
(JDK1.7以前はfinal変数である必要があった)
void scope2() {
  int n = 123;

  Runnable runner = () -> {
    n++;
  };
  runner.run();
}
ただし、その変数の値を変える(再代入する)ことは出来ない。(コンパイルエラーになる)

ラムダ式内部で外側の変数を使う場合は、その変数はfinalもしくは実質的finalでなければならない。

void scope2a() {
  int n = 123;

  Runnable runner = new Runnable() {
    @Override
    public void run() {
      n++;
    }
  };
  runner.run();
}
匿名クラスでもエラーになる。
void scope3() {
  int n = 123;

  Runnable runner = () -> {
    System.out.println(n);
  };

  n++;
  runner.run();
}
変数に再代入する場所がラムダ式の外でもコンパイルエラーになる。 void scope3a() {
  int n = 123;

  Runnable runner = new Runnable() {
    @Override
    public void run() {
      System.out.println(n);
    }
  };

  n++;
  runner.run();
}
匿名クラスでもエラーになる。
void scope4() {
  int t = 123;

  Consumer<String> consumer = (String t) -> {
    System.out.println(t);
  };
  consumer.accept("abc");
}
ラムダ式の変数のスコープの話は、ラムダ式の引数の変数にも及ぶ。
左記の例では、ラムダ式の引数に使おうとした変数がラムダ式の外側で既に定義されているのでコンパイルエラーになる。
(いわばfor文の変数のスコープと同様)

なお、ラムダ式の引数に使った変数名を、ラムダ式の外側のメソッド内でラムダ式より後で定義して使うのは問題ない。
void scope4a() {
  int t = 123;

  Consumer<String> consumer = new Consumer<String>() {
    @Override
    public void accept(String t) {
      System.out.println(t);
    }
  };
  consumer.accept("abc");
}
匿名クラスでは別スコープになる。
void scopeThis() {
  Runnable runner = () -> {
    System.out.println(this);
  };
  runner.run();
}
thisは、「ラムダ式を定義したメソッド」が属しているクラスのインスタンスを指す。 void scopeThisA() {
  Runnable runner = new Runnable() {
    @Override
    public void run() {
      System.out.println(this);
    }
  };
  runner.run();
}
thisは、匿名クラスのインスタンスを指す。
static void scopeThis() {
  Runnable runner = () -> {
    System.out.println(this);
  };
  runner.run();
}
ラムダ式の外側のメソッドがstaticな場合は、thisを使うことが出来ない。[2014-06-21] static void scopeThisA() {
  Runnable runner = new Runnable() {
    @Override
    public void run() {
      System.out.println(this);
    }
  };
  runner.run();
}
thisは、匿名クラスのインスタンスを指す。

ちなみに、Scalaでは関数内から外側の変数を変更する(再代入する)ことも出来る。
(ただし、Scala的なプログラミングでは、そもそも変数の再代入を行うことを良しとしない)

// Scalaの例
  def scope2() : Unit = {
    var n = 123

    val function = () => { n += 1 }
    function.apply()

    println(n)
  }
  def scope4() : Unit = {
    val t = 123

    val function = (t: String) => println(t) //tは別スコープ
    function.apply("abc")
  }

プリミティブ型とラッパークラスの注意点

「関数型インターフェースのメソッドの引数の型」と「ラムダ式の引数の型」に関して、プリミティブ型(int等)とラッパークラス(Integer等)は別物である(区別される)。
自動変換(オートボクシング)のような事は行ってくれない。

備考
定義したもの ラムダ式
interface IntArgument {
  public void method(int n);
}
IntArgument test11 = (int n) -> System.out.println(n);  
IntArgument test12 = (Integer n) -> System.out.println(n); 引数の型が異なるのでコンパイルエラー。
IntArgument test13 = (n) -> System.out.println(n); 引数の型を省略していれば問題ない。
interface IntegerArgument {
  public void method(Integer n);
}
IntegerArgument test21 = (int n) -> System.out.println(n); 引数の型が異なるのでコンパイルエラー。
IntegerArgument test22 = (Integer n) -> System.out.println(n);  
IntegerArgument test23 = (n) -> System.out.println(n); 引数の型を省略していれば問題ない。
void call(IntArgument func) {
  func.method(123);
}
IntArgument test31 = (n) -> System.out.println(n);
call(test31);
 
IntegerArgument test32 = (n) -> System.out.println(n);
call(test32);
IntArgumentとIntegerArgumentに互換性(継承関係)は無いのでコンパイルエラー。
call((int n) -> System.out.println(n));  
call((Integer n) -> System.out.println(n)); 引数の型が異なるのでコンパイルエラー。
call(n -> System.out.println(n)); 引数の型を省略していれば問題ない。

なお、メソッド(ラムダ式)の戻り値の型については自動変換(オートボクシング)される。


キャスト

ラムダ式は関数型インターフェースの変数に代入する形で使うので、代入先がObjectクラスだったりするとコンパイルエラーになる。
関数型インターフェースにキャストする形をとれば、Objectクラスの変数にも代入できる。

備考
Runnable object = () -> System.out.println("abc");  
Object object = () -> System.out.println("abc"); コンパイルエラー。
ラムダ式がどの関数型インターフェースのものなのか分からない為。
Scalaだとどの関数でもFunction型になるので、こういうエラーにはならない)
Object object = (Runnable) () -> System.out.println("abc"); キャストすればOK。

交差型キャスト

JDK1.8から交差型キャストという仕様が導入された。
(インターフェース名1 & インターフェース名2 & …)」という形で、キャストするときに「&」でインターフェース名を繋いでいく。

参考: bitter_foxさんのJavaSE8リリース記念!マイナーな言語仕様を紹介してみる(交差型キャスト,レシーバパラメータ(仮引数にthis))


ラムダ式の場合、交差型キャストを使うと、マーカーインターフェースと合成することが出来る。

出力結果 備考
Runnable runner = () -> System.out.println(123);
System.out.println(runner instanceof Runnable);
System.out.println(runner instanceof Serializable);
runner.run();
true
false
123
単なるラムダ式はシリアライズ可能ではない。
Object object = (Runnable) () -> System.out.println(123);
System.out.println(object instanceof Runnable);
System.out.println(object instanceof Serializable);
true
false
 
Runnable runner = (Runnable & Serializable) () -> System.out.println(123);
System.out.println(runner instanceof Runnable);
System.out.println(runner instanceof Serializable);
runner.run();
true
true
123
交差型キャストでSerializableを指定すると
Serializableが実装(合成)されている扱いになる。
Object object = (Runnable & Serializable & Cloneable) () -> System.out.println("abc");
System.out.println(object instanceof Runnable);
System.out.println(object instanceof Serializable);
System.out.println(object instanceof Cloneable);
true
true
true
CloneableでもOK。
interface D {
  public default int getValue() {
    return 123;
  }
}

Runnable runner = (Runnable & D) () -> System.out.println("abc");
runner.run();
System.out.println(((D) runner).getValue());
abc
123
デフォルトメソッドを含むインターフェースでも合成される。

普通、キャストする場合は、キャスト元のクラスがキャスト先のインターフェースを実装している場合だけ可能なはず。
しかしラムダ式の場合、ラムダ式インスタンス単体では他のインターフェースは実装していないと思われるのに、
交差型キャストでSerializableを指定すると、ラムダ式インスタンスがSerializableを実装している扱いになる(Serializableが合成される)。
けっこう不思議だ。


シリアライズ

ラムダ式が交差型キャストによってSerializableを実装できるということは、ラムダ式はシリアライズできるのだろうか?
試してみた。

package example;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;
public class LambdaSerialize {

	public static void main(String... args) throws Exception {
		byte[] buf1 = serialize("abc");
		byte[] buf2 = serialize("def");

		Supplier<String> supplier1 = deserialize(buf1);
		Supplier<String> supplier2 = deserialize(buf2);

		System.out.println(supplier1.get());
		System.out.println(supplier2.get());
	}
	// シリアライズ
	static byte[] serialize(String s) throws IOException {
		Object object = (Supplier<String> & Serializable) () -> "test-" + s; //ラムダ式

		try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
			try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
				oos.writeObject(object);
			}
			return bos.toByteArray();
		}
	}
	// デシリアライズ
	static Supplier<String> deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
		try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
		     ObjectInputStream ois = new ObjectInputStream(bis)) {
			@SuppressWarnings("unchecked")
			Supplier<String> supplier = (Supplier<String>) ois.readObject();
			return supplier;
		}
	}
}

実行結果:

test-abc
test-def

という訳で、ラムダ式はシリアライズ可能らしい!
ラムダ式を作る際に渡した値もシリアライズ(保存)されているようだ。

なお、Serializableをキャストから外したら、想定通りにoos.writeObject()で「java.io.NotSerializableException: example.LambdaSerialize$$Lambda$1/640070680」という例外が発生した。


ラムダ式がシリアライズ可能ということは、プログラム間でラムダ式を受け渡し可能、つまり分散環境(複数マシン間)で“処理”を受け渡すことが出来るってことになる。
何か面白いフレームワークが作れるかも?(笑)


invokedynamic

ラムダ式は、表面上は無名内部クラス(匿名クラス)を短く記述できる記法であり、匿名クラスに置き換えることが出来る。

しかし、コンパイルされたバイトコードでは、ラムダ式と匿名クラスの扱いは異なる。

匿名クラスをコンパイルすると「クラス名$1」みたいなclassファイルが生成されるが、
ラムダ式をコンパイルしても、ラムダ式専用のclassファイルは生成されない。

変数へ代入する為にラムダ式もインスタンスを生成する必要があるが、
ラムダ式は(classファイルが無いので)invokedynamicという命令を使ってインスタンスが生成される。
「invoke」というと、普通はinvokevirtual(メソッド呼び出し)とかinvokespecial(privateメソッドやコンストラクターの呼び出し)とかinvokestatic(staticメソッドの呼び出し)を想像するので
invokedynamicもラムダ式を呼び出す為の命令だと思っていたのだけれども、違うらしい。
(invokedynamicは実行時に(動的に)呼び出し先を変える為の命令らしい。ラムダ式のインスタンス生成の為にそれを利用しているということのようだ。[2014-08-09]
→宮川 拓さんのラムダと invokedynamic の蜜月

なお、ラムダ式の実体は、「ラムダ式が定義されたメソッド」が属するクラスにprivate staticメソッドとして定義されるようだ。
そして、そのメソッドを呼び出せるようにinvokedynamicがよろしくやってくれるのだろう。

ソース コンパイル後のイメージ
Example.java Example.class
public class Example {

  public void example() {
    Runnable runner = () -> System.out.println("abc");
    runner.run();

    System.out.println(runner.getClass());
  }
}
public class Example {

  public void example() {
    Runnable runner = new ラムダ式; //invokedynamic
    runner.run();

    System.out.println(runner.getClass());
  }

  private static void lambda$example$0() {
    System.out.println("abc");
  }
}

ちなみに、ラムダ式のインスタンスのクラスを表示してみると、「Example$$Lambda$1/640070680」みたいなクラス名になっていた。


ただし、仕様上は必ずinvokedynamicが使われると決まっている訳ではないらしい。[2014-08-09]

→宮川 拓さんのラムダ式は必ずしも invokedynamic に翻訳されるわけではない


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま