@skrb氏のツイートで知った2011 JVM Language Summitのプレゼン資料のひとつ、From Lambdas to Bytecodeを読みました。ラムダ式をどうバイトコードに変換するか、そこでInvokedynamic(indy)がどう効いてくるかという話です。
今さらながらJVM Lang Summitの資料を見ている。これはおもしろい。 http://www.wiki.jvmlangsummit.com/Main_Page invokeDynamicを知りたいのであれば必見かな。
まだ内容の7割程度しか理解できていないように感じるのですが、現段階での理解をここにまとめておきます。
内部クラスでラムダ式を実現する
ラムダ式の実現方法として一番想像しやすいのは内部クラスを作る方法で、資料でも選択肢のひとつとして紹介されています。資料からコードを引用しますと、
// 3ページめから引用 #{ p -> p.age < TARGET }
を
// 3ページめから引用 class Foo$1 implements Predicate<Person> { private final int v0; Foo$1(int $v0) { this.$v0 = v0 } public boolean apply(Person p) { return (p.age < $v0); } }
という内部クラスを利用する形に書き換えるイメージですね。定数TARGETをコンストラクタで渡しておき、applyメソッドではこの定数と引数をつきあわせた結果をbooleanで返しています。
この方法は既存バイトコードで実現できますし直感的にも分かりやすいのですが、著者は「ラムダ式を1つ書くたびにクラスが1つ定義されるという不潔なもの(yuck)だ」と一蹴していらっしゃいます。確かにperm領域の消費は気になるところです。
staticメソッドでラムダ式を実現する
他の案としては単純なstaticメソッド呼び出しに変換するという方法が言及されています。例えば先ほどの例なら、以下のようなstaticメソッドを呼び出すことで同じことが実現できるというわけです。
static boolean lambda$1(Person p, int TARGET) { return p.age < TARGET; }
ただ呼び出し元が必要としているのはメソッドでもメソッドの戻り値でもなく、Opsクラス内のインタフェースを実装したインスタンスです。ですからこのメソッドを呼び出すクラスを別に定義しなければなりません。
じゃあ結局ラムダ式の数だけクラス定義が必要になるのでは?というように見えますが、indyを利用することで“ラムダ式の型ごとに”*11つだけクラスを定義すれば済むと書かれています。ここでラムダ式の型とは、引数と戻り値の型をまとめた表現です。例えば#{Integer num -> num * 2}
と#{Integer num -> num * 3}
はどちらもintを受け取ってintを返すラムダ式なので同じ型と言え、つまりこれらは同じクラスで対応できます。
具体的なコードは資料に載っていないのですが、つまりこういう実装なのでしょう。
/** * このクラスのインスタンスはmetaFactoryメソッドを通じて提供される。 * このクラスの他にも、Opsクラス内の各インタフェースに対応したクラスが存在すると考えられる。 * @see http://gee.cs.oswego.edu/dl/jsr166/dist/extra166ydocs/extra166y/Ops.html */ public class Predicate<A> implements extra166y.Ops.Predicate<A> { public Predicate (呼び出すメソッドを指定する情報) { /* ... */ } @Override boolean op (A arg) { // ... // コンストラクタで渡された情報をもとに、indyを使ってstaticメソッドを呼び出す // ... return returnedValueByInvokedStaticMethod; } }
これならばコンストラクタの引数によって振る舞いを変更できるため、ひとつのクラスで複数のラムダ式に対応できます。また、資料ではこの実現方法には他に以下のようなメリットがあるとされています。
- ステートレスなインスタンスはキャッシュして使い回すことができる
- 使われないラムダ式はパフォーマンスに影響しない
- 仕様と実装の切り分けを実現できる
*1:原文ではone wrapper class per SAM type