前回記事「Groovyのクロージャはちゃんとしているか?の続きです。
前回検討したGroovyのクロージャの振る舞いは、実際にJavaバイトコードとしてどのように実現されているでしょうか、ってのが今回のテーマです。
本来ならここで、
- groovycコマンドでコンパイル
- 生成された.classファイルをjadなどで逆コンパイル
という「黄金の二連コンボ」で生成される等価なJavaコードを示せばそれで済む話…だったはずなのですが、残念ながら、クロージャの使用コードから生成されるコードについてはjadは正しいコードを逆生成してくれません。バイトコード命令が延々と出てくるのはまだ良いとして、ローカル変数の型推論に失敗して嘘コードを吐きやがります*1
というのが、前回の記事を書いた経緯なのです。つまり、うまくgroovyc & jadできなかったので、実験して動作から推定するしかなかったのです*2。本記事ではjadの生成結果とも合せて検討した結果を紹介します。推定なので本当にこうである保証は無いのっす。
クロージャの実装推定
ということで、以下のようなクロージャを用いたGroovyコード:
class Test { def methodA() { int localVariable = 0 return {-> localVariable++ } } }
これは以下のようなJavaコードが生成するであろうバイトコードと同等のバイトコードにコンパイルされるんじゃないかと思います。あくまで推定です。本論に関わらないコードはバサッと省略しています。
import groovy.lang.Reference; import groovy.lang.GroovyObject; import groovy.lang.Closure; public class Test implements GroovyObject { public Object methodA() { Reference<Integer> localVariable = new Reference<Integer>(new Integer(0)); return new _methodA_closure1(localVariable) { } class _methodA_closure1 extends Closure { Reference<Integer> localVariable; public Object doCall() { Object obj = localVariable.get(); localVariable.set(localVariable.get().next()) return obj; } public Integer getLocalVariable() { return localVariable.get() } public _methodA_closure1(Reference<Integer> localVariable) { this.localVariable = localVariable } } }
ここで、groovy.lang.Reference
public class Reference<T> extends GroovyObjectSupport implements Serializable { private T value; public Reference(T value) { this.value = value; } : public T get() { return value; } public void set(T value) { this.value = value; } }
こういう単一要素のコレクションでgroovy.langパッケージにあります。間接参照を表します。
クロージャの動作
言葉でまとめますと、
- プリミティブのローカル変数は、ラッパー型にboxingされた上に*3、参照Reference
を介したものとして定義される(localVariable変数は型が変更される)。 - クロージャ定義(.. {-> localVariable++ })は、Closureを継承した内部クラス(「クロージャクラス」と呼ぶ)をnewするコードに変換される。
- Reference化されたローカル変数は、前述のクロージャクラスのコンストラクタにパラメータとして渡され、インスタンス変数として保持される(参照がコピーされる)。
- クロージャ中のコードは、前述のクロージャクラスでOverride定義されたdoCallメソッドのコードに展開される。
- doCall中からのローカル変数の参照や変更は、クロージャクラスがインスタンス変数として保持するReferenceオブジェクトを通じて行われる。
いかがでしょう。予想どおりだったでしょうか。
クロージャであるために
この実装では、前回の記事の「クロージャが満すべき条件」はすべて解決されているのではないかと思います。
ポイントは、
- (1)ローカル変数の値の保持領域をスタックからヒープに移動させる(プリミティブ型はラッパーになる)
- → リターンしてもクロージャから参照される変数が生存している
- (2)複数クロージャからの「変数の共有」を実現するために、「変更可能な間接参照」を表現するオブジェクト(groovy.lang.Reference)」を介在させる。
- → 複数クロージャ間で書き換え可能な変数の共有が可能になる
- (3)クロージャは出現するたびに毎回newする(再利用しない)。そしてローカル変数を保持する参照は、クロージャインスタンスに保持させる
- → 再帰呼び出しを可能に
です。
ちなみに、Javaでも内部クラスからfinal変数を変更するために、int型(final int a=1)を1要素のint配列(final int[] a={1})にしたりしますが、上は本質的に言ってそれと同様です。int配列を使う場合には、(1)ヒープへの移動と、(2)書き換え可能な間接参照は、書き換え可能な参照型である配列が実現します。また、int配列を(3)コピーすることは、内部クラスの仕様として、裏でjavacがやってくれています。
では、次に気が向いたら「クロージャとは」的な議論を書きます。