Why Map does not work for GString in Groovy?
Asked Answered
F

2

8

With the following snippet I cannot retrieve gString from a map:

def contents = "contents"
def gString = "$contents"

def map = [(gString): true]

assert map.size() == 1 // Passes
assert gString.hashCode() == map.keySet().first().hashCode() // Passes, same hash code
assert map[gString] // Fails

How on earth is that possible?

Assertion message clearly shows that there's something seriously wrong with Groovy:

assert map[gString] // Fails
       |  ||
       |  |contents
       |  null
       [contents:true]

It's not the same question as Why groovy does not see some values in dictionary? First answer there suggests:

You're adding GString instances as keys in your map, then searching for them using String instances.

In this question I clearly add GString and try to retrieve GString.

Also neither Why are there different behaviors for the ways of addressing GString keys in maps? nor Groovy different results on using equals() and == on a GStringImpl have an answer for me. I do not mutate anything and I do not mix String with GString.

Fireman answered 25/8, 2016 at 9:17 Comment(6)
#31713859Browne
#9682706Browne
Tim, these questions do not provide answer for me - they are about mutability or mixing String with GString. In my code I do not mutate or mix anything.Fireman
@tim_yates, I have to agree with Michal Kordas . The cited answers really do not have bearing on this problem.Similitude
@Michal Kordas, I think you have found a Groovy bug. As I traced through this code in the debugger I found that the expression map[gString] invoked the category method DefaultGroovyMethods.getAt(Object,String) (with the GString coerced to String, of course), rather than the expected (at least by me) DefaultGroovyMethods.getAt(getAt(Map<K,V>,K). I don't know how the Groovy runtime engine came to make such a decision, but it seems to me that it is the wrong decision.Similitude
Oddly enough, if you replace map[gString] with map.getAt(gString), then it still fails the same way. However, if you replace it with DefaultGroovyMethods.getAt(map, gString), then you get what seems to me to be the correct behavior.Similitude
S
10

tl;dr: You seem to have discovered a bug in Groovy's runtime argument overloading evaluation.

Answer:

map[gString] is evaluated as map.getAt(gString) at runtime straightforwardly via Groovy's operator overloading mechanism. So far, so good, but now is where everything starts to go awry. The Java LinkedHashMap class does not have a getAt method anywhere in it's type hierarchy, so Groovy must use dynamically associated mixin methods instead (Actually that statement is sort of reversed. Groovy uses mixin methods before using the declared methods in the class hierarchy.)

So, to make a long story short, Groovy resolves map.getAt(gString) to use the category method DefaultGroovyMethods.getAt(). Easy-peasy, right? Except that this method has a large number of different argument overloads, several of which might apply, especially when you take Groovy's default argument coercion into account.

Unfortunately, instead of choosing DefaultGroovyMethods.getAt(Map<K,V>,K), which would seem to be a perfect match, Groovy chooses DefaultGroovyMethods.getAt(Object,String), which coerces the GString key argument into a String. Since the actual key is in fact a GString, the method ultimately fails to find the value.

To me the real killer is that if the argument overload resolution is performed directly from code (instead of after the operator resolution and the category method selection), then Groovy makes the right overload choice! That is to say, if you replace this expression:

map[gString]

with this expression:

DefaultGroovyMethods.getAt(map,gString)

then the argument overloading is resolved correctly, and the correct value is found and returned.

Similitude answered 26/8, 2016 at 19:44 Comment(3)
@roger.glover would you have any insight as to why there is this similar issue with system property values? #42212868Herbal
Thank you for this in-depth explanation. It explained why a certain cache didn't work for 9 months, unbeknownst to us. 🤷‍♂️Optic
In general, it is a bad idea to use unevaluated GStrings as map keys. It's almost never what you really want anyway.Similitude
G
-2

There's nothing wrong with Groovy. A GString is not a String. It is mutable and as such should never be used as a key in a map (like any other mutable object in Java).

Learn more about this in the docs: http://docs.groovy-lang.org/latest/html/documentation/index.html#_gstring_and_string_hashcodes

Goods answered 25/8, 2016 at 9:26 Comment(2)
I've updated question with one more assert - hash code is the same and I'm not mutating this GString at allFireman
moreover, using mutable objects as map keys is perfectly fine as long as the object is not mutated after being inserted into the map.Peplum

© 2022 - 2024 — McMap. All rights reserved.