Can someone explain to me why in closure2 initVars('c') is not able to modify the referenced object if @Field is used in declaration?
import groovy.transform.Field;
@Field def lines4 = "a";
void initVars(String pref){
println('init:'+lines4+' '+pref) //*3.init:a b *7.init:b c
lines4 = pref;
}
println("closure1") ///1. closure1
1.times {
println(lines4) ///2. a
initVars('b') ///3. init:a b
lines4 += 'p1'
println(lines4) ///4. bp1
}
println("closure2") ///5. closure2
1.times {
println(lines4) ///6. bp1
initVars('c') ///7. init:b c
println(lines4) ///8. bp1 Why not c
lines4 += 'q1'
println(lines4) ///9. bp1q1 Why not cq1
}
Output:
C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a b
4. bp1
5. closure2
6. bp1
7. init:b c
8. bp1
9. bp1q1
Output without @Field and def, with just lines4 = "a" in script scope. This appears normal to me.
C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a
4. bp1
5. closure2
6. bp1
7. init:bp1
8. c
9. cq1
I saw same behavior in groovy2.5-beta and groovy 2.6-alpha.
Using @Field annotation on a script variable changes a scope of this variable from a local one to a Script class one:
Variable annotation used for changing the scope of a variable within a script from being within the run method of the script to being at the class level for the script.
The annotated variable will become a private field of the script class. The type of the field will be the same as the type of the variable. Example usage:
import groovy.transform.Field @Field List awe = [1, 2, 3] def awesum() { awe.sum() } assert awesum() == 6In this example, without the annotation, variable awe would be a local script variable (technically speaking it will be a local variable within the run method of the script class). Such a local variable would not be visible inside the awesum method. With the annotation, awe becomes a private List field in the script class and is visible within the awesum method.
Source: http://docs.groovy-lang.org/2.4.12/html/gapi/groovy/transform/Field.html
Every Groovy script extends groovy.lang.Script class and the body of the script is executed inside Script.run() method. Groovy passes variables to this script using Binding object. When you change a scope of a local script variable to a class level then there is no binding for this variable passed to a closure, because binding object contains only local-scoped variables. Compare these two screenshots I made. First one shows what the binding object looks like when we call initVars(String pref) for the first time and lines4 is a local script variable:

And here is same breakpoint but now lines4 is a @Field def lines4 variable:

As you can see there is no binding for lines4 variable in binding object, but there is a class field called lines4, while this binding is present in the first screenshot attached.
When you call
lines4 += 'p1'
in the first closure, local binding for lines4 is created and it is initialized with a current value of a this.lines4 value. It happens because Script.getProperty(String property) is implemented in following way:
public Object getProperty(String property) {
try {
return binding.getVariable(property);
} catch (MissingPropertyException e) {
return super.getProperty(property);
}
}
Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L54
So it firstly checks if there is a binding for a variable you access in the closure and when it does not exist it passes execution to a parent's getProperty(name) implementation - in our case it just returns class property value. At this point this.lines4 is equal to b and this is the value that is returned.
initVars(String pref) method accesses class field, so when you call it it always overrides Script.lines4 property. But when you call
lines4 += 'q1'
in the second closure, the binding lines4 for a closure already exists and its value is bp1 - this value was associated in the first closure call. That's why you don't see c after calling initVars('c'). I hope it helps.
binding works in a script explainedLet's get a little deeper to get better understanding what is going on under the hood. This is what your Groovy script looks like when it is compiled to a bytecode:
Compiled from "script_with_closures.groovy"
public class script_with_closures extends groovy.lang.Script {
java.lang.Object lines4;
public static transient boolean __$stMC;
public script_with_closures();
public script_with_closures(groovy.lang.Binding);
public static void main(java.lang.String...);
public java.lang.Object run();
public void initVars(java.lang.String);
protected groovy.lang.MetaClass $getStaticMetaClass();
}
Two things worth mentioning at this moment:
@Field def lines4 is compiled to a class field java.lang.Object lines4;
void initVars(String pref) method is compiled to public void initVars(java.lang.String); class method.For a simplicity you can assume that the rest content (excluding lines4 and initVars method) of your script is inlined to public java.lang.Objectrun() method.
initVars always accesses class field lines4 because it has direct access to this field. Decompiling this method to a bytecode shows us this:
public void initVars(java.lang.String);
Code:
0: invokestatic #19 // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
3: astore_2
4: aload_2
5: ldc #77 // int 5
7: aaload
8: aload_0
9: aload_2
10: ldc #78 // int 6
12: aaload
13: aload_2
14: ldc #79 // int 7
16: aaload
17: aload_2
18: ldc #80 // int 8
20: aaload
21: ldc #82 // String init:
23: aload_0
24: getfield #23 // Field lines4:Ljava/lang/Object;
27: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
32: ldc #84 // String
34: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
39: aload_1
40: invokeinterface #67, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
45: invokeinterface #52, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.callCurrent:(Lgroovy/lang/GroovyObject;Ljava/lang/Object;)Ljava/lang/Object;
50: pop
51: aload_1
52: astore_3
53: aload_3
54: aload_0
55: swap
56: putfield #23 // Field lines4:Ljava/lang/Object;
59: aload_3
60: pop
61: return
Operation 56 is a opcode for a assigning value to a field.
Now let's understand what happens when both closures gets called. First thing worth mentioning - both closures have delegate field set to the script object that is being executed. We know that it extends groovy.lang.Script class - a class that uses binding private field to store all bindings (variables) available in the script runtime. This is important observation, because groovy.lang.Script class overrides:
public Object getProperty(String property)public void setProperty(String property, Object newValue)Both methods use binding to lookup and store variables used in the script runtime. getProperty gets called any time you read local script variable and setProperty gets called any time you assign a value to script local variable. That's why code like:
lines4 += 'p1'
generates sequence like:
getProperty -> value + 'p1' -> setProperty
In your example first attempt of reading lines4 ends up with returning a value from parent class (it happens if binding is not found, then GroovyObjectSupport.getProperty(name) is called and this one returns a value of a class property with given name). When closure assigns a value to a lines4 variable then a binding is created. And because both closures share same binding object (they use delegate to the same instance), when second closure reads or writes line4 variable then it uses previously created binding. And initVars does not modify binding because as I shown you earlier it accesses class field directly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With