Groovy が裏で何をやっているのかを、 groovyc で生成した class ファイルをデコンパイルして勉強する。
裏の動きを理解すれば、プログラミングの幅も広がる気がするので。
Hello World
スクリプトをコンパイルすると生成されるクラス
script.groovy
println 'Hello Groovy!!'
これを groovyc
でコンパイルして class ファイルを生成する。
次に、 Jad を使ってデコンパイルし、 Java ソースコードを生成する。
すると、 こんなファイル が生成される。
ファイル名と同じ名前のクラスが生成されている。
script.java
public class script extends Script
{
...
Hello World はどこに行ったか
Hello World はどこにいったのかというと、 ここ。
script.java
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[1].callCurrent(this, "Hello Groovy!!");
}
- トップレベルに記述したコードは、全てこの
run()
メソッドに格納されることになる。 - つまり、意味はないけどこんな実装をすると、
script.groovy
println 'Hello Groovy!!'
def s = new script()
s.run()
Hello Groovy!!
Hello Groovy!!
Hello Groovy!!
Hello Groovy!!
:
:
at script.run(script.groovy:4)
at script$run.call(Unknown Source)
at script.run(script.groovy:4)
at script$run.call(Unknown Source)
at script.run(script.groovy:4)
at script$run.call(Unknown Source)
-
run()
メソッドが無限に呼び出されてプログラムが死亡する。
デフォルトのメソッド定義
-
acallsite[1].callCurrent(this, "Hello Groovy!!")
このコードのその後を追いかけて行くと、最終的にorg.codehaus.groovy.runtime.DefaultGroovyMethods
というクラスのprintln()
メソッドにたどり着く。 - クラスに定義されていないメソッドが呼ばれた際、 Groovy は最終的にこの
DefaultGroovyMethods
のメソッドを呼ぶようになっている。
DefaultGroovyMethods.println()
public static void println(Object self, Object value) {
// we won't get here if we are a PrintWriter
if (self instanceof Writer) {
final PrintWriter pw = new GroovyPrintWriter((Writer) self);
pw.println(value);
} else {
System.out.println(InvokerHelper.toString(value));
}
}
run() という関数は定義できない
また、 run()
という関数を定義すると、以下のようにコンパイルエラーが発生する。
def run() {
println 'run'
}
実行結果
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
script.groovy: 1: The method public java.lang.Object run() { ... } is a duplicate of the one declared for this script's body code
. At [1:1] @ line 1, column 1.
def run() {
^
- 独自に宣言した
run()
関数と、 Groovy が生成するrun()
メソッドが重複してしまうため、コンパイルエラーが発生する。
変数の宣言
def a = 1
int b = 2
def c = 100000000000
def d = 1.0
def e = 'string'
def f = true
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
Object a = Integer.valueOf(1);
Object _tmp = a;
int b = 2;
int _tmp1 = b;
Object c = Long.valueOf(0x174876e800L);
Object _tmp2 = c;
Object d = new BigDecimal("1.0");
Object _tmp3 = d;
Object e = "string";
Object _tmp4 = e;
Object f = Boolean.valueOf(true);
return f;
}
-
def
で定義した変数は、全てObject
型で定義されている。 - 型を明示した場合は、その型が使用されている。
- 数値は、値が小さければ
Integer
、あふれる場合は適宜 Long が使われている。 - 実数は
BigDecimal
が使用される。
関数の定義
func('hoge')
def func(param) {
println param
}
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[1].callCurrent(this, "hoge");
}
public Object func(Object param)
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[2].callCurrent(this, param);
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "runScript";
as[1] = "func";
as[2] = "println";
}
-
acallsite[1].callCurrent()
は、最終的にfunc()
メソッドを呼び出している。-
acallsite[]
には関数や変数を実行・参照するためのCallSite
オブジェクトが入っている。 - 配列の各インデックスに、どの関数(変数)が入っているかは、
$createCallSiteArray_1()
というメソッドを見ると分かる。
-
- トップレベルに定義した関数は、スクリプトのクラス(
script
)のメソッドとして定義される。
色々な関数の呼び出し方
ということは、こんな書き方もできる。
this.func('this.func()')
new script().func('new script().func()')
def func(param) {
println param
}
実行結果
this.func()
new script().func()
トップレベルで型付きで宣言した変数は、関数からは参照できない
また、この挙動から以下のような実装は動作しないことが簡単に理解できる。
int i=10
func()
def func() {
println 'i=' + i
}
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
int i = 10;
int _tmp = i;
if(__$stMC || BytecodeInterface8.disabledStandardMetaClass())
return acallsite[1].callCurrent(this);
else
return func();
}
public Object func()
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[2].callCurrent(this, acallsite[3].call("i=", acallsite[4].callGroovyObjectGetProperty(this)));
}
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: script
groovy.lang.MissingPropertyException: No such property: i for class: script
at script.func(script.groovy:6)
at script.run(script.groovy:3)
- 変数
i
はrun()
メソッドのローカル変数なので、func()
メソッドから参照することはできない。 - そのため、実行時に
MissingPropertyException
が発生する。 - JavaScript のクロージャを経験していると、ハマりやすいポイントです。
型を指定せずに宣言した変数
i=10
func()
def func() {
println 'i=' + i
}
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
byte byte0 = 10;
ScriptBytecodeAdapter.setGroovyObjectProperty(Integer.valueOf(byte0), script, this, "i");
byte _tmp = byte0;
if(__$stMC || BytecodeInterface8.disabledStandardMetaClass())
return acallsite[1].callCurrent(this);
else
return func();
}
public Object func()
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[2].callCurrent(this, acallsite[3].call("i=", acallsite[4].callGroovyObjectGetProperty(this)));
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "runScript";
as[1] = "func";
as[2] = "println";
as[3] = "plus";
as[4] = "i";
}
-
def
などを使わずにトップレベルで変数を宣言すると、ScriptBytecodeAdapter.setGroovyObjectProperty()
というメソッドを使って値が保存されている。 -
setGroovyObjectProperty()
の実装は以下のようになっている。
ScriptBytecodeAdapter.setGroovyObjectProperty()
public static void setGroovyObjectProperty(Object messageArgument, Class senderClass, GroovyObject receiver, String messageName) throws Throwable {
try {
receiver.setProperty(messageName, messageArgument);
} catch (GroovyRuntimeException gre) {
throw unwrap(gre);
}
}
-
receiver
にはthis
、すなわちscript
クラスのインスタンスが渡されている。 - つまり、型無しで宣言した変数は、その場所における
this
が指すオブジェクトのプロパティとして設定されることになる。 - 関数内ではプロパティから値を取得しているので、トップレベルで宣言された変数に参照できるようになっている。
実験
i = 10
func()
def func() {
println 'this.i = ' + this.i
}
実行結果
this.i = 10
クラスを宣言する
new Hoge()
def class Hoge {
}
- 逆コンパイルすると、
Hoge
クラスだけを定義したソースファイルがscript
とは別に出力される。 - 全文は長いので こちら を参照。
Hoge.java
public class Hoge
implements GroovyObject
{
...
}
-
script.groovy
内で定義しているが、script
クラスのインナークラスとして出力されるわけではない。
スクリプト内で定義したクラス内では、トップレベルで宣言した型付きの変数を参照できない
def i = 10
new Hoge().method()
def class Hoge {
def method () {
println 'i = ' + i
}
}
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: Hoge
groovy.lang.MissingPropertyException: No such property: i for class: Hoge
at Hoge.method(script.groovy:7)
at Hoge$method.call(Unknown Source)
at script.run(script.groovy:3)
-
Hoge
クラスは別クラスとして定義されるので、当然script
クラスのrun()
メソッド内に定義されるローカル変数にはアクセスできない。
スクリプト内で定義したクラス内では、トップレベルで宣言した型無しの変数も参照できない
i = 10
new Hoge().method()
def class Hoge {
def method () {
println 'i = ' + i
}
}
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: Hoge
groovy.lang.MissingPropertyException: No such property: i for class: Hoge
at Hoge.method(script.groovy:7)
at Hoge$method.call(Unknown Source)
at script.run(script.groovy:3)
- 関数の時は、型なしで定義すればトップレベルの変数を参照できた。
- しかし、クラスの場合は、それでも参照ができない。
- 理由は以下の通り。
script.groovy
i = 10 // ←この i は、 script インスタンスのプロパティになる
new Hoge().method()
def class Hoge {
def method () {
println 'i = ' + i // ←ここで参照しようとしている i は、 Hoge インスタンスのプロパティを参照しようとしている
}
}
クロージャ
def closure = { it ->
println 'it=' + it
}
closure('test')
長いので全体は こちら。
一部抜粋
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
class _run_closure1 extends Closure
implements GeneratedClosure
{
public Object doCall(Object it)
{
CallSite acallsite1[] = $getCallSiteArray();
return acallsite1[0].callCurrent(this, acallsite1[1].call("it=", it));
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "println";
as[1] = "plus";
}
...
}
Object closure = new _run_closure1(this, this);
Object _tmp = closure;
return acallsite[1].call(closure, "test");
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "runScript";
as[1] = "call";
}
-
Closure
を継承したローカルなクラスが定義されている。- (メソッドの途中でクラスが定義できるなんて、初めて知った...)
- クロージャの処理は、作成されたクラスの
doCall()
メソッド内にまとめられている。
クロージャーなら、トップレベルで型付きで宣言した変数を参照できる
int i = 10
def closure = {
println 'i = ' + i
}
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
Reference i = new Reference(Integer.valueOf(10));
Reference _tmp = i;
class _run_closure1 extends Closure
implements GeneratedClosure
{
public Object doCall(Object it)
{
CallSite acallsite1[] = $getCallSiteArray();
return acallsite1[0].callCurrent(this, acallsite1[1].call("i = ", i.get()));
}
...
}
Object closure = new _run_closure1(this, this, i);
return closure;
}
- トップレベルに型付きで宣言した変数は
run()
メソッドのローカル変数になるため、トップレベルで定義した関数からは参照できなかった。 - しかし、クロージャは
run()
メソッドの内部で定義されるので、トップレベルで型付きで宣言された変数を参照できる。 - 変数が
Reference
に格納されているのは、おそらくクロージャ内から値の変更ができるようにするためだと思う。- Java でコードを書いてみるとわかるが、ローカルクラス内で、外部で宣言された変数を参照する場合、その変数は
final
(変更不可)である必要がある。
- Java でコードを書いてみるとわかるが、ローカルクラス内で、外部で宣言された変数を参照する場合、その変数は
クロージャ内で参照する this
def closure = {
println this
}
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
class _run_closure1 extends Closure
implements GeneratedClosure
{
public Object doCall(Object it)
{
CallSite acallsite1[] = $getCallSiteArray();
return acallsite1[0].callCurrent(this, getThisObject());
}
...
}
Object closure = new _run_closure1(this, this);
return closure;
}
-
this
の参照は、getThisObject()
というメソッドに置き換えられている。 -
getThisObject()
は、親クラスのClosure
クラスに定義されている。
Closure
private Object delegate;
private Object owner;
private Object thisObject;
...
public Closure(Object owner, Object thisObject) {
this.owner = owner;
this.delegate = owner;
this.thisObject = thisObject;
...
}
...
public Object getThisObject(){
return thisObject;
}
-
thisObject
は、コンストラクタの第二引数で渡されたオブジェクトが設定されている。 - クロージャのコンストラクタの第二引数には、その場所における
this
オブジェクトが渡される。 - つまり、クロージャ内で参照する
this
は、クロージャを定義した場所でthis
を参照したときのモノと同じになる。
def closure = {
println 'this.class=' + this.class
}
def class Hoge {
def method(closure) {
closure()
}
}
new Hoge().method(closure);
実行結果
this.class=class script
- クロージャ自体は
Hoge
クラス内部で実行されている。 - しかし、クロージャ内で参照した
this
は、クロージャが宣言された場所におけるthis
、すなわち、script
クラスのインスタンスを参照している。
演算子オーバーロード
'left' << 'shift'
'plus' + 'plus'
'index'[1]
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
acallsite[1].call("left", "shift");
acallsite[2].call("plus", "plus");
return acallsite[3].call("index", Integer.valueOf(1));
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "runScript";
as[1] = "leftShift";
as[2] = "plus";
as[3] = "getAt";
}
- それぞれ、
CallSite
を使ったメソッドの呼び出しに置き換えられている。
真偽値の判定
boolean b = 'truth'
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
boolean b = DefaultTypeTransformation.booleanUnbox("truth");
return Boolean.valueOf(b);
}
- Groovy は、型が boolean でなくても、その値から良しなに boolean に読み替えて真偽を判定してくれる。
- その仕組は、
DefaultTypeTransformation.booleanUnbox()
によって実行されている。
DefaultTypeTransformation
public static boolean booleanUnbox(Object value) {
return castToBoolean(value);
}
...
public static boolean castToBoolean(Object object) {
// null is always false
if (object == null) {
return false;
}
// equality check is enough and faster than instanceof check, no need to check superclasses since Boolean is final
if (object.getClass() == Boolean.class) {
return ((Boolean)object).booleanValue();
}
// if the object is not null and no Boolean, try to call an asBoolean() method on the object
return (Boolean)InvokerHelper.invokeMethod(object, "asBoolean", InvokerHelper.EMPTY_ARGS);
}
-
null
なら常にfalse
。 -
Boolean
型なら、単純にその boolean 値。 - それ以外の場合は、対象オブジェクトの
asBoolean()
メソッドを実行して、その結果を返している。 - デフォルトの
asBoolean()
メソッドは、DefaultGroovyMethods
クラスに定義されている。
Groovy 独自の演算子
Spread Operator
'abcd'*.getAt(0)
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
return ScriptBytecodeAdapter.invokeMethodNSpreadSafe(script, "abcd", "getAt", new Object[] {
Integer.valueOf(0)
});
}
-
ScriptBytecodeAdapter.invokeMethodNSpreadSafe()
というメソッドに置き換えられている。 - 当該メソッドを見に行くと、以下のような実装になっている。
ScriptBytecodeAdapter.invokeMethodNSpreadSafe()
public static Object invokeMethodNSpreadSafe(Class senderClass, Object receiver, String messageName, Object[] messageArguments) throws Throwable {
if (receiver == null) return null;
List answer = new ArrayList();
for (Iterator it = InvokerHelper.asIterator(receiver); it.hasNext();) {
answer.add(invokeMethodNSafe(senderClass, it.next(), messageName, messageArguments));
}
return answer;
}
-
receiver
(ここでは、文字列 "abcd")をIterator
に変換し、それぞれの要素に対してメソッドを実行している(invokeMethodNSafe()
)。 - 結果は
ArrayList
に格納し、返却している。
Elvis Operator
'Elvis' ?: 'Operator'
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
String s;
return DefaultTypeTransformation.booleanUnbox(s = "Elvis") ? s : "Operator";
}
- これは単純で、そのまま三項演算子に置き換えられている。
Safe Navigation Operator
'str'?.a?.b
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
return acallsite[1].callGetPropertySafe(acallsite[2].callGetPropertySafe("str"));
}
...
private static void $createCallSiteArray_1(String as[])
{
as[0] = "runScript";
as[1] = "b";
as[2] = "a";
}
-
CallSite
のcallGetPropertySafe()
メソッドを呼び出している。 -
CallSite
自体はインターフェースで、実装はorg.codehaus.groovy.runtime.callsite.AbstractCallSite
にある。
AbstractCallSite.callGetPropertySafe()
public final Object callGetPropertySafe(Object receiver) throws Throwable {
if (receiver == null)
return null;
else
return callGetProperty(receiver);
}
-
receiver
(プロパティを取得しようとしているオブジェクト)が null の場合は、そのまま null を返し、そうでない場合にだけプロパティの取得を試みている。
比較演算子
'abc' == 'def'
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
if(!BytecodeInterface8.isOrigZ() || __$stMC || BytecodeInterface8.disabledStandardMetaClass())
return Boolean.valueOf(ScriptBytecodeAdapter.compareEqual("abc", "def"));
else
return Boolean.valueOf(ScriptBytecodeAdapter.compareEqual("abc", "def"));
}
-
==
の比較は、ScriptBytecodeAdapter.compareEqual()
に置き換えられる。 - この実装を辿って行くと
org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation
のcompareEqual()
メソッドに行き着く。
DefaultTypeTransformation.compareEqual()
public static boolean compareEqual(Object left, Object right) {
if (left == right) return true;
if (left == null || right == null) return false;
if (left instanceof Comparable) {
return compareToWithEqualityCheck(left, right, true) == 0;
}
// handle arrays on both sides as special case for efficiency
Class leftClass = left.getClass();
Class rightClass = right.getClass();
if (leftClass.isArray() && rightClass.isArray()) {
return compareArrayEqual(left, right);
}
if (leftClass.isArray() && leftClass.getComponentType().isPrimitive()) {
left = primitiveArrayToList(left);
}
if (rightClass.isArray() && rightClass.getComponentType().isPrimitive()) {
right = primitiveArrayToList(right);
}
if (left instanceof Object[] && right instanceof List) {
return DefaultGroovyMethods.equals((Object[]) left, (List) right);
}
if (left instanceof List && right instanceof Object[]) {
return DefaultGroovyMethods.equals((List) left, (Object[]) right);
}
if (left instanceof List && right instanceof List) {
return DefaultGroovyMethods.equals((List) left, (List) right);
}
if (left instanceof Map.Entry && right instanceof Map.Entry) {
Object k1 = ((Map.Entry)left).getKey();
Object k2 = ((Map.Entry)right).getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = ((Map.Entry)left).getValue();
Object v2 = ((Map.Entry)right).getValue();
if (v1 == v2 || (v1 != null && DefaultTypeTransformation.compareEqual(v1, v2)))
return true;
}
return false;
}
return ((Boolean) InvokerHelper.invokeMethod(left, "equals", right)).booleanValue();
}
- 比較する値が配列か、List か Map かなどを調べて、比較の仕方を切り替えている。
- 単純に
equals()
メソッドを呼んでいるのではなく、結構複雑な処理をしている。
GString
def str = "string"
"str = ${str}"
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
Object str = "string";
Object _tmp = str;
return new GStringImpl(new Object[] {
str
}, new String[] {
"str = ", ""
});
}
- 単純にダブルクォーテーションで括っただけの文字列は、普通に文字列リテラルとして置き換えられる。
-
${...}
の置換対象文字列があれば、GStringImpl
のインスタンスが生成される。- (ダブルクォーテーション文字列だと常に GString のインスタンスが生成されて、シングルクォーテーションの文字列よりコストが大きいのかなと思っていた)
- (でも、 GString にすべきかどうか判断する必要があるから、やっぱりシングルクォーテーション文字列の方がコストは小さい?)
- (GString にすべきかどうかの判定はコンパイル時に行うから、プリコンパイルしてから使うのであれば、どちらも一緒か)
ヒアドキュメント
def str = '''
hear
document
'''
public Object run()
{
CallSite acallsite[] = $getCallSiteArray();
Object str = "\n hear\n document\n ";
return str;
}
- 単純に、改行コード入りの文字列に置き換えられている。