Why are there different behaviors for the ways of addressing GString keys in maps?
Asked Answered
B

1

2

While studying the Groovy (2.4.4) syntax in the official documentation, I came across the special behavior concerning maps with GStrings as identifiers. As described in the documentation, GStrings are a bad idea as (hash)map identifiers, because the hashcodes of a non-evaluated GString-object differs from a regular String-object with the same representation as the evaluated GString.

Example:

def key = "id"
def m = ["${key}": "value for ${key}"]

println "id".hashCode() // prints "3355"
println "${key}".hashCode() // prints "3392", different hashcode

assert m["id"] == null // evaluates true

However, my intuitive expectation was that using the actual GString identifier to address a key in the map will in fact deliver the value - but it does not.

def key = "id"
def m = ["${key}": "value for ${key}"]

assert m["${key}"] == null // evaluates also true, not expected

That made me curious. So I had several suggestions concerning this issue and did some experiments.

(pls keep in my mind that I am new to Groovy and I was just brainstorming on the fly - continue to Suggestion #4 if you do not want to read how I tried to examine the cause of the issue)

Suggestion #1. hashcode for GString objects works/is implemented somewhat non-deterministic for whatever reason and delivers different results depending on the context or the actual object.

That turned out to be nonsense quite fast:

println "${key}".hashCode() // prints "3392"
// do sth else
println "${key}".hashCode() // still "3392"

Suggestion #2. The actual key in the map or the map item does not have the expected representation or hashcode.

I took a closer look at the item in the map, the key, and its hashcode.

println m // prints "[id:value for id]", as expected
m.each { 
    it -> println key.hashCode() 
} // prints "3355" - hashcode of the String "id"

So the hashcode of the key inside the map is different from the GString hashcode. HA! or not. Though it is nice to know, it is actually not relevant because I still do know the actual hashcodes in the map index. I just rehashed a key that has been transformed to a string after being put into the index. So what else?

Suggestion #3. The equals-method of a GString has an unknown or non- implemented behavior.

No matter whether two hashcodes are equal, they may not represent the same object in a map. That depends on the implementation of the equals method for the class of the key-object. If the equals-method is, for instance, not implemented, two objects are not equal even if the hashcode is identical and therefore the desired map key cannot be adressed properly. So I tried:

def a = "${key}"
def b = "${key}"

assert a.equals(b)  // returns true (unfortunate but expected)

So two representations of the same GString are equal by default.

I skip some others ideas I tried and continue with the last thing I tried just before I was going to write this post.

Suggestion #4. The syntax of access matters.

That was a real killer of understanding. I knew before: There are syntactically different ways two access map values. Each way has its restrictions, but I thought the results stay the same. Well, this came up:

def key = "id"
def m = ["${key}": "value for ${key}"]

assert m["id"] == null // as before
assert m["${key}"] == null // as before
assert m.get("${key}") == null // assertion fails, value returned

So if I use the get-method of a map, I get the actual value in the way I expected it to in the first place.

What is the explanation for this map access behavior concerning GStrings? (or what kind of rookie mistake is hidden here?)

Thanks for your patience.

EDIT: I am afraid that my actual question is not clearly stated, so here is the case in short and concise:

When I have a map with a GString as a key like this

def m = ["${key}": "value for ${key}"]

why does this return the value

println m.get("${key}")

but that does not

println m["${key}"]

?

Bioecology answered 30/7, 2015 at 0:26 Comment(0)
H
1

You can look at this matter with a very different approach. A map is supposed to have immutable keys (at least for hashcode and equals), because the map implementation depends on this. GString is mutable, thus not really suited for map keys in general. There is also the problem of calling String#equals(GString). GString is a Groovy class, so we can influence the equals method to equal to a String just fine. But String is very different. That means calling equals on a String with a GString will always be false in the Java world, even if hashcode() would behave the same for String and GString. And now imagine a map with String keys and you ask the map for a value with a GString. It would always return null. On the other hand a map with GString keys queried with a String could return the "proper" value. This means there will always be a disconnection.

And because of this problem GString#hashCode() is not equal to String#hashCode() on purpose.

It is in no way non-deterministic, but a GString hashcode can change, if the participating objects change their toString representation:

def map = [:]
def gstring = "$map"
def hashCodeOld = gstring.hashCode()
assert hashCodeOld == gstring.hashCode()
map.foo = "bar"
assert hashCodeOld != gstring.hashCode()

Here the toString representation of map will change for Groovy and GString, thus the GString will produce a different hashcode

Hebdomadary answered 30/7, 2015 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.