I am reading the Groovy closure documentation in https://groovy-lang.org/closures.html#this. Having a question regarding with GString behavior.
- Closures in GStrings
The document mentioned the following:
Take the following code:
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
The code behaves as you would expect, but what happens if you add:
x = 2
assert gs == 'x = 2'
You will see that the assert fails! There are two reasons for this:
a GString only evaluates lazily the toString representation of values
the syntax ${x} in a GString does not represent a closure but an expression to $x, evaluated when the GString is created.
In our example, the GString is created with an expression referencing x. When the GString is created, the value of x is 1, so the GString is created with a value of 1. When the assert is triggered, the GString is evaluated and 1 is converted to a String using toString. When we change x to 2, we did change the value of x, but it is a different object, and the GString still references the old one.
A GString will only change its toString representation if the values it references are mutating. If the references change, nothing will happen.
My question is regarding the above-quoted explanation, in the example code, 1 is obviously a value, not a reference type, then if this statement is true, it should update to 2 in the GString right?
The next example listed below I feel also a bit confusing for me (the last part) why if we mutate Sam to change his name to Lucy, this time the GString is correctly mutated?? I am expecting it won't mutate?? why the behavior is so different in the two examples?
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
def gs = "Name: ${p}"
assert gs == 'Name: Sam'
p = Lucy. //if we change p to Lucy
assert gs == 'Name: Sam' // the string still evaluates to Sam because it was the value of p when the GString was created
/* I would expect below to be 'Name: Sam' as well
* if previous example is true. According to the
* explanation mentioned previously.
*/
sam.name = 'Lucy' // so if we mutate Sam to change his name to Lucy
assert gs == 'Name: Lucy' // this time the GString is correctly mutated
Why the comment says 'this time the GString is correctly mutated? In previous comments it just metioned
the string still evaluates to Sam because it was the value of p when the GString was created, the value of p is 'Sam' when the String was created
thus I think it should not change here?? Thanks for kind help.
These two examples explain two different use cases. In the first example, the expression "x = ${x}"
creates a GString
object that internally stores strings = ['x = ']
and values = [1]
. You can check internals of this particular GString
with println gs.dump()
:
<org.codehaus.groovy.runtime.GStringImpl@6aa798b strings=[x = , ] values=[1]>
Both objects, a String
one in the strings
array, and an Integer
one in the values
array are immutable. (Values are immutable, not arrays.) When the x
variable is assigned to a new value, it creates a new object in the memory that is not associated with the 1
stored in the GString.values
array. x = 2
is not a mutation. This is new object creation. This is not a Groovy specific thing, this is how Java works. You can try the following pure Java example to see how it works:
List<Integer> list = new ArrayList<>();
Integer number = 2;
list.add(number);
number = 4;
System.out.println(list); // prints: [2]
The use case with a Person
class is different. Here you can see how mutation of an object works. When you change sam.name
to Lucy
, you mutate an internal stage of an object stored in the GString.values
array. If you, instead, create a new object and assigned it to sam
variable (e.g. sam = new Person(name:"Adam")
), it would not affect internals of the existing GString
object. The object that was stored internally in the GString
did not mutate. The variable sam
in this case just refers to a different object in the memory. When you do sam.name = "Lucy"
, you mutate the object in the memory, thus GString
(which uses a reference to the same object) sees this change. It is similar to the following plain Java use case:
List<List<Integer>> list2 = new ArrayList<>();
List<Integer> nested = new ArrayList<>();
nested.add(1);
list2.add(nested);
System.out.println(list2); // prints: [[1]]
nested.add(3);
System.out.println(list2); // prints: [[1,3]]
nested = new ArrayList<>();
System.out.println(list2); // prints: [[1,3]]
You can see that list2
stores the reference to the object in the memory represented by nested
variable at the time when nested
was added to list2
. When you mutated nested
list by adding new numbers to it, those changes are reflected in list2
, because you mutate an object in the memory that list2
has access to. But when you override nested
with a new list, you create a new object, and list2
has no connection with this new object in the memory. You could add integers to this new nested
list and list2
won't be affected - it stores a reference to a different object in the memory. (The object that previously could be referred to using nested
variable, but this reference was overridden later in the code with a new object.)
GString
in this case behaves similarly to the examples with lists I shown you above. If you mutate the state of the interpolated object (e.g. sam.name
, or adding integers to nested
list), this change is reflected in the GString.toString()
that produces a string when the method is called. (The string that is created uses the current state of values stored in the values
internal array.) On the other hand, if you override a variable with a new object (e.g. x = 2
, sam = new Person(name:"Adam")
, or nested = new ArrayList()
), it won't change what GString.toString()
method produces, because it still uses an object (or objects) that is stored in the memory, and that was previously associated with the variable name you assigned to a new object.
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