Minor background for readers new to Groovy:
Groovy provides language support to make the routine tasks on collection types easier than Java programmers are used to. For example, creating a map is as easy as:
def map = [key1:valueX, key2:valueY]which creates a map with two entries: key1 maps to valueX and key2 maps to valueY. The fun starts with the many ways of accessing the maps data. The following are all valid ways of retrieving valueX from the map:
def value = map.get('key1') def value = map['key1'] def value = map.key1
The first example looks just like Java. The second and third examples are provided by the Groovy language. Now, on to the mistake.
Can you guess what happens?
I have created a very small subset of the code to show the bug. The actual code resides in a Grails controller method that gets invoked on a form submission. In the submitted form, I use DOM scripting to dynamically create multiple line items for a transaction.
Take a look at the excerpt below and see if you can tell what happens when it executes.
def params = [itemCount:'3'] // this was really form data submitted for (int i=0; i<params.itemCount; i++) def lineItem = new LineItem() bindData(lineItem, params["lineItem["+i+"]"]) transactionInstance.lineItem[i] = lineItem }
Anyone see the error?
The for loop looped about 10-12 times and then stop even though there were only 3 lineItems submitted on the form. Other times, the loop ran forever. The problem: the value of the itemCount entry is a String and not a Integer, which resulted in a strange comparison (more on that later). The fix was easy, convert the String to an Integer.
def count = params.itemCount.toInteger() for (int i=0; i<count; i++)
But why did the loop behave the way it did?
Operator overload for < (less than)
The next step is to figure out what Groovy's doing. The operator < is basically doing a comparison, so what happens when you try either of the following:
i.compareTo(params.itemCount) params.itemCount.compareTo(i)The answer is, both throw a ClassCastException indicating that an Integer can not be cast to a String and vice versa. So how is Groovy coercing this comparison? Time to decompile the code to get to the bottom of this.
Decompile the code!
Below is a snippet of the decompiled code where you can see it calls the ScriptBytecodeAdapter compareLessThan method with both of the arguments as Objects, so we get an identity comparison!
for(Integer i = $const$0; ScriptBytecodeAdapter.compareLessThan(i, acallsite.callGetProperty(params));)