Javaのラムダ式について。
|
||
|
ラムダ式は、JDK1.8(Java8)で導入された構文。
関数型インターフェース(抽象メソッドが1つだけ定義されているインターフェース)の変数に代入する箇所ではラムダ式を渡すことが出来る。
見た目上は、無名内部クラス(匿名クラス)を短く記述できる記法と言える。
匿名クラスの例 | ラムダ式の例 |
---|---|
Runnable runner = new Runnable() { |
Runnable runner = () ->
{ |
List<Integer> list = Arrays.asList(1, 3, 2); |
List<Integer> list = Arrays.asList(1, 3, 2); |
ラムダ式は関数型インターフェースのメソッドを実装(オーバーライド)することになる。
ラムダ式の代入先である関数型インターフェースには抽象メソッドが1つしか無い為、ラムダ式がどのメソッドを実装することになるのかは選択の余地なく決まる。
(関数型インターフェースでない場合、つまり抽象メソッドが0個か2個以上の場合は実装対象のメソッドを決定できない為、コンパイルエラーになる)
ちなみに、ラムダは「lambda」と書く。
「b」が入っているので「ラムブダ」に見えてしまうこともあるが、「らむだばだばだば」みたいにウルトラマンの音楽にからめたネタにするのは誰かがやっていると心得よう(笑)
ラムダ式は、「引数部 -> 処理本体
」という形式で表す。
(引数部と処理本体を矢印っぽい記号で結ぶ。Scalaだと「=>
」を使うのだが、数学的には「→」
(一本線の矢印)の方が正しいらしい?
まぁ、JavaはC言語系の言語なので、C言語に在った「->」を使いまわすのは理解できる。あと、「=>」だと不等号「>=」と間違う人も居るかも?)
いわば、メソッド名と戻り型の宣言が無いメソッド定義となる。
引数部分の定義方法 | JDK | 説明 | ラムダ式の例 | |
---|---|---|---|---|
基本形 | (型1 引数名1, 型2 引数名2, …) |
引数部分は普通のメソッド定義の引数部分と同じように書く。 型の前にアノテーションを付けることも出来る。[2018-10-01] |
() -> {} |
|
型の省略 | (引数名1, 引数名2, …) |
ラムダ式がオーバーライドすべきメソッドは、代入先の関数型インターフェースから分かる。 引数の型はそこで定義されているので、ラムダ式の引数自体は型を省略することが出来る。 複数の引数がある場合は、全ての引数について型を省略する必要がある(一部だけの省略は出来ない)。 |
() -> {} |
|
括弧の省略 | 引数名 |
引数が1個しか無い場合は、引数を囲む丸括弧を省略することが出来る。 (この場合は、型も省略しなければならない) |
s -> {} |
|
var | (var 引数名1, var 引数名2, …) |
11 | Java11から、引数にvarが付けられるようになった。[2018-10-01] varを使う場合、全ての引数をvarにする必要がある。 varにはアノテーションが付けられる。 (レシーバーパラメーター(引数のthis)と同目的[2020-06-24]) |
(var s) -> {} |
なお、ラムダ式の引数名には単一アンダースコア「_
」を使うことが出来ない。
(ラムダ式の引数以外の場所で単一アンダースコアを識別子(変数名等)に使うとコンパイル時に警告になるが、ラムダ式の引数ではコンパイルエラーになる)
Scalaでは単一アンダースコアの引数名に特別な意味があるので、そういった他言語と紛らわしくないようにする為、あるいは将来的にScalaの様にプレースホルダーとして使えるようにする為かもしれない。
→JEP 302: Lambda Leftovers (2020-11-19)
処理本体の定義方法 | 説明 | ラムダ式の例 | Scalaの関数の例 | |
---|---|---|---|---|
基本形 | { |
処理本体は、基本的にはメソッド本体と同じ。 ラムダ式がオーバーライドすべきメソッドは、代入先の関数型インターフェースから分かる。 戻り値の型はそこで定義されているので、それに応じた値をreturn文で返すようにする。 (Scalaだと最後のreturnを省略できるのだが、Java8ではそういう省略は出来ない) |
(int n) -> { |
(n : Int) => { |
戻り型がvoid | { |
関数型インターフェースのメソッドの戻り型がvoidの場合は return文は(当然)省略可能。 |
(int n) -> { |
(n : Int) => { |
括弧の省略 | 文 (戻り型が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() { |
ラムダ式の外側で定義されている変数を、 ラムダ式内部から参照することが出来る。 |
void scope1a() { |
匿名クラスの内側からも 外側の変数を参照できる。 (JDK1.7以前はfinal変数である必要があった) |
void scope2() { |
ただし、その変数の値を変える(再代入する)ことは出来ない。(コンパイルエラーになる) ラムダ式内部で外側の変数を使う場合は、その変数はfinalもしくは実質的finalでなければならない。 |
void scope2a() { |
匿名クラスでもエラーになる。 |
void scope3() { |
変数に再代入する場所がラムダ式の外でもコンパイルエラーになる。 | void scope3a() { |
匿名クラスでもエラーになる。 |
void scope4() { |
ラムダ式の変数のスコープの話は、ラムダ式の引数の変数にも及ぶ。 左記の例では、ラムダ式の引数に使おうとした変数がラムダ式の外側で既に定義されているのでコンパイルエラーになる。 (いわばfor文の変数のスコープと同様) なお、ラムダ式の引数に使った変数名を、ラムダ式の外側のメソッド内でラムダ式より後で定義して使うのは問題ない。 |
void scope4a() { |
匿名クラスでは別スコープになる。 |
void scopeThis() { |
thisは、「ラムダ式を定義したメソッド」が属しているクラスのインスタンスを指す。 | void scopeThisA() { |
thisは、匿名クラスのインスタンスを指す。 |
static void scopeThis() { |
ラムダ式の外側のメソッドがstaticな場合は、thisを使うことが出来ない。[2014-06-21] | static void scopeThisA() { |
thisは、匿名クラスのインスタンスを指す。 |
ちなみに、Scalaでは関数内から外側の変数を変更する(再代入する)ことも出来る。
(ただし、Scala的なプログラミングでは、そもそも変数の再代入を行うことを良しとしない)
// Scalaの例 def scope2() : Unit = { var n = 123 val function = () => { n += 1 } function.apply() println(n) }
def scope4() : Unit = { valt
= 123 val function = (t
: String) => println(t
) //tは別スコープ function.apply("abc") }
「関数型インターフェースのメソッドの引数の型」と「ラムダ式の引数の型」に関して、プリミティブ型(int等)とラッパークラス(Integer等)は別物である(区別される)。
自動変換(オートボクシング)のような事は行ってくれない。
例 | 備考 | |
---|---|---|
定義したもの | ラムダ式 | |
interface IntArgument { |
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 { |
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) { |
IntArgument test31 = (n) ->
System.out.println(n); |
|
IntegerArgument test32 = (n) ->
System.out.println(n); |
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); |
true |
単なるラムダ式はシリアライズ可能ではない。 |
Object object = (Runnable) () ->
System.out.println(123); |
true |
|
Runnable runner = (Runnable & Serializable) () ->
System.out.println(123); |
true |
交差型キャストでSerializableを指定すると Serializableが実装(合成)されている扱いになる。 |
Object object = (Runnable & Serializable &
Cloneable) () -> System.out.println("abc"); |
true |
CloneableでもOK。 |
interface D { |
abc |
デフォルトメソッドを含むインターフェースでも合成される。 |
普通、キャストする場合は、キャスト元のクラスがキャスト先のインターフェースを実装している場合だけ可能なはず。
しかしラムダ式の場合、ラムダ式インスタンス単体では他のインターフェースは実装していないと思われるのに、
交差型キャストでSerializableを指定すると、ラムダ式インスタンスがSerializableを実装している扱いになる(Serializableが合成される)。
けっこう不思議だ。
関数型インターフェースがSerializableを継承していると、その関数型インターフェース(のオブジェクト)はシリアライズすることが出来る。[2017-07-26]
実際にシリアライズする場合は、その関数型インターフェースに代入したラムダ式はシリアライズ可能である必要がある。
(ラムダ式内部から外側の変数を使用する(キャプチャーする)場合は、そのインスタンスがシリアライズ可能である必要がある)
// シリアライズ可能な関数型インターフェース @FunctionalInterface public interface MySupplier<T> extends Serializable { public T get(); }
class MyFactory implements Serializable { public String create() { return "zzz"; } }
MyFactory factory = new MyFactory(); MySupplier<String> f = () -> factory.create(); // factoryをキャプチャーしている byte[] buf = serialize(f);
ちなみに、内部クラス等をキャプチャーするシリアライズは非推奨らしい。
参考: OracleのJava TutorialsのLambda ExpressionsのSerialization
ラムダ式が交差型キャストによってSerializableを実装できるということは、そのラムダ式はシリアライズできるのだろうか?[2014-03-23]
試してみた。
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
」という例外が発生した。
ラムダ式がシリアライズ可能ということは、プログラム間でラムダ式を受け渡し可能、つまり分散環境(複数マシン間)で“処理”を受け渡すことが出来るってことになる。
何か面白いフレームワークが作れるかも?(笑)
ラムダ式は、表面上は無名内部クラス(匿名クラス)を短く記述できる記法であり、匿名クラスに置き換えることが出来る。
しかし、コンパイルされたバイトコードでは、ラムダ式と匿名クラスの扱いは異なる。
匿名クラスをコンパイルすると「クラス名$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 class Example { |
ちなみに、ラムダ式のインスタンスのクラスを表示してみると、「Example$$Lambda$1/640070680」みたいなクラス名になっていた。
ただし、仕様上は必ずinvokedynamicが使われると決まっている訳ではないらしい。[2014-08-09]
→宮川 拓さんのラムダ式は必ずしも invokedynamic に翻訳されるわけではない