18

Question 1

Is it irrelevant whether a List (list of objects) or a List<String> (list of Strings) is used in Groovy?

In the code example below, both lists end up being an ArrayList (ArrayList of objects). Would have expected the second list to be an ArrayList<String> (ArrayList of Strings).

Does Groovy lose the type information when the class is compiled and infer it when the compiled class is executed?

Example 1

List untypedList = ["a", "b", "c"]
List<String> typedList = ["a", "b", "c"]

println "Untyped list List:       ${untypedList.getClass()}"
println "Typed list List<String>: ${typedList.getClass()}"

Output 1

Untyped list List:       class java.util.ArrayList
Typed list List<String>: class java.util.ArrayList // Would have expected ArrayList<String>

Question 2

I would have expected the line typedList << new Integer(1) in the example below to fail with an exception because I'm trying to put an int into a list of Strings. Can anyone explain why I can add an int to the String-typed List?

The output shows that it remains an Integer, i.e. it's not on-the-fly converted to a String "1".

Example 2

List untypedList = ["a", "b", "c"]
List<String> typedList = ["a", "b", "c"]

untypedList << new Integer(1)
typedList << new Integer(1) // Why does this work? Shouldn't an exception be thrown?

println "Types:"
println "Untyped list List:       ${untypedList.getClass()}"
println "Typed list List<String>: ${typedList.getClass()}"

println "List contents:"
println untypedList
println typedList

println "Untyped list:"
untypedList.each { println it.getClass() }
println "Typed list:"
typedList.each { println it.getClass() }

Output 2

Types:
Untyped list List:       class java.util.ArrayList
Typed list List<String>: class java.util.ArrayList
List contents:
[a, b, c, 1]
[a, b, c, 1]
Untyped list:
class java.lang.String
class java.lang.String
class java.lang.String
class java.lang.Integer
Typed list:
class java.lang.String
class java.lang.String
class java.lang.String
class java.lang.Integer
Lernkurve
  • 17,488
  • 24
  • 77
  • 110
  • 1
    http://stackoverflow.com/questions/15755261/groovy-map-and-java-map-on-generics – tim_yates Feb 24 '14 at 10:41
  • @tim_yates: So the answer according to the link you posted is "[...] this means that the type parameter is not checked at compile time and not available at runtime." which means I can't prevent anybody from putting an `int` into that `List` which in turn means all I can be sure of is that `List` contains objects of type `object`. Correct? – Lernkurve Feb 24 '14 at 11:49
  • 3
    Yes. Unless you're using `@CompileStatic` – tim_yates Feb 24 '14 at 11:53

3 Answers3

25

When running Groovy "normally", generics are thrown away before compilation, so only exist in the source as helpful reminders to the developer.

However, you can use @CompileStatic or @TypeChecked to make Groovy honour these Generics and check the types of things at compilation.

As an example, consider I have the following project structure:

project
 |---- src
 |      |---- main
 |             |---- groovy
 |                    |---- test
 |                           |---- ListDelegate.groovy
 |                           |---- Main.groovy
 |---- build.gradle

With the code:

build.gradle

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.2.1'
}

task( runSimple, dependsOn:'classes', type:JavaExec ) {
    main = 'test.Main'
    classpath = sourceSets.main.runtimeClasspath
}

ListDelegate.groovy

package test

class ListDelegate<T> {
    @Delegate List<T> numbers = []
}

Main.groovy

package test

class Main {
    static main( args ) {
        def del = new ListDelegate<Integer>()
        del << 1
        del << 'tim'
        println del
    }
}

Now, running gradle runSimple gives us the output:

:compileJava UP-TO-DATE
:compileGroovy
:processResources UP-TO-DATE
:classes
:runSimple
[1, tim]

BUILD SUCCESSFUL

Total time: 6.644 secs

So as you can see, the generics were thrown away, and it just worked adding Integers and Strings to out List of supposedly only Integers

Now, if we change ListDelegate.groovy to:

package test

import groovy.transform.*

@CompileStatic
class ListDelegate<T> {
    @Delegate List<T> numbers = []
}

And run again:

:compileJava UP-TO-DATE
:compileGroovy
:processResources UP-TO-DATE
:classes
:runSimple
[1, tim]

BUILD SUCCESSFUL

Total time: 6.868 secs

We get the same output!! This is because whilst ListDelegate is now statically compiled, our Main class is still dynamic, so still throws away generics before constructing the ListDelegate... So we can also change Main.groovy to:

package test

import groovy.transform.*

@CompileStatic
class Main {
    static main( args ) {
        def del = new ListDelegate<Integer>()
        del << 1
        del << 'tim'
        println del
    }
}

And now re-running gradle runSimple give us:

:compileJava UP-TO-DATE
:compileGroovy
startup failed:
/Users/tyates/Code/Groovy/generics/src/main/groovy/test/Main.groovy: 10:
    [Static type checking] - Cannot find matching method test.ListDelegate#leftShift(java.lang.String).
    Please check if the declared type is right and if the method exists.
 @ line 10, column 9.
           del << 'tim'
           ^

1 error

:compileGroovy FAILED

Which is, as you'd expect, failing to add a String to our declared List of Integer.

In fact, you only need to CompileStatic the Main.groovy class and this error will be picked up, but I always like to use it where I can, not just where I need to.

tim_yates
  • 154,107
  • 23
  • 313
  • 320
4

As @tim_yates notes, it is possible to enable compile time checks with the @TypeChecked/@CompileStatic annotations.

Another alternative is to enable runtime type checking by wrapping the collection with Collections.checkedList(). While this doesn't use the generics or the declared type, enforcing it at runtime sometimes fits in better with loosely typed dynamic code. This is a Java platform feature not specific to groovy.

Example:

// no type checking:
list1 = ["a", "b", "c"]
list1 << 1
assert list1 == ["a", "b", "c", 1]
// type checking
list2 = Collections.checkedList(["a", "b", "c"], String)
list2 << 1
// ERROR java.lang.ClassCastException:
// Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
ataylor
  • 60,501
  • 18
  • 147
  • 181
3

From Wikipedia, for Java:

Generics are checked at compile-time for type-correctness. The generic type information is then removed in a process called type erasure. For example, List will be converted to the non-generic type List, which ordinarily contains arbitrary objects. The compile-time check guarantees that the resulting code is type-correct.

This type information is for compiler and IDE. Groovy is based on Java and inherits same principles for generics.

At other hand, Groovy is more dynamic language, so probably, it's the reason why it doesn't check types on compile time. IMO for Groovy it's some kind of code comment, sometimes very useful.

PS @tim_yates suggested a link to Groovy docs about Generics, that confirms:

Groovy currently does a little further and throws away generics information "at the source level".

Igor Artamonov
  • 34,120
  • 8
  • 74
  • 106