Wednesday, July 1, 2009

Stringy Lists and Maps

We all like to keep lists and maps of our objects. From shopping lists to translation maps, these useful little utilities are the staple diet of any Java programmer.

You need to be careful with lists and maps when using GStrings. Consider the following test:

public class GroovyTest extends GroovyTestCase{

void testAddName(){
def name = "tom"
def someNames = []

someNames.add("${name} 1")

assertTrue someNames.contains("tom 1")
}
}


This test will fail. But why? Well, the GString "${name}" and the GString "tom 1" are different object types. "tom 1" is a String, and "${name} 1" is a GString.

Something similar happens with maps, when using a GString as a key. This time it is the hashcode of the GString which is different each time.

As Jochen puts it on JIRA :

"there where several discussion on the mailing list for the usage of GStrings as map keys. Map keys things that should keep their hashcode the same and their euqals(sic) method should also return always the same if feed with the same object. This implies that hash key objects should either not be changed or even better, they should not be mutable. GStrings are mutable. Making a map immutable, does not change that fact. And while the GString "${p}" or "$p" uses the value of p at the time the GString was created and thus could be seen as something that does not change, the GString "${>p}" uses a closure and evaluates the value of p every time anew. Not only that, but the hashcode of "${>p}" differs from "$p", because the hashcode of the closure is used, instead of the hashcode of p to create the GString's hashcode.

So I suggest you to just follow the rule to nether to use a GString as key and there is no issue at all. Even the GString "$p" and "33" do not have the same hashcode for p=33. That is no bug, that is a design decision often discussed on the list. But there are good reasons for it. "

So, how do we work around these problems? Well, there is no elegant solution to this but calling toString on the GString should at least solve some of it:


public class GroovyTest extends GroovyTestCase{

void testAddName(){
def name = "tom"
def someNames = []

someNames.add("${name} 1".toString())

assertTrue someNames.contains('tom 1') // note we can also switch to single quotes?
}
}


... at least our test now passes. The same solution would work for keys on maps.


Has anyone got a more elegant solution to this?

2 comments:

Burt said...

"tom 1" and 'tom 1' are not GStrings, they're Strings. They only become GStrings if they have interpolated ${} values. So the problem isn't the mismatched hashCode values - it's the mismatched classes. You'd have the same problem mixing Integer and Long:

someNames.add(5L)
assertTrue someNames.contains(5)

also fails since 5 is an int/Integer.

The solution is to make sure all the values are of the same type like you did converting the GString to a String with toString()

matt said...

thanks for pointing that out! I've updated the article to reflect what you said.

Post a Comment