Groovy: Generics? What are those?

2013-11-10 in groovy java
A fractal of bad implementation

Java 1.5 introduced into the language a concept called generics. In environments that do generics correctly, a generic type parameter specifies the type of elements contained within an abstract data type. C++’s templates have this effect — it is fundamentally impossible for a list<string> to ever contain anything other than strings.

For the sake of backwards compatibility, Java instead handles generics with type erasure and implicit casting. Type erasure means that the Java compiler determines the least general type that the ADT can possibly have (eg, Object for List, Comparable for TreeMap, etc) according to its generic declaration, and then internally uses that type, and provides extra information in the generated .class files so that the compiler knows what generics were originally there. Whenever a value with a more specific generic type is used, the compiler implicitly casts it back down to the type that “should” be there.

In other words, the following code

1   List<String> list = new ArrayList<String>();
2   populate(list);
3 
4   for (int i = 0; i < list.size(); ++i)
5     System.out.println(list.get(i));

is actually compiled to

1   // Without generics, is essentially List<Object> and ArrayList<Object>
2   List list = new ArrayList();
3   populate(list);
4 
5   for (int i = 0; i < list.size(); ++i)
6     // Note implicit cast
7     System.out.println((String)list.get(i));

What this means is that Groovy doesn’t really need to know about generics; it could just use bare types and let its horrible dynamic type system silently remove the programmer inconvenience of not having the implicit downcasts.

However, for backwards compatibility with Java, Groovy does understand the Java syntax for generics usage. One would expect that it would enforce them, or at least provide a warning that it doesn’t. (Of course, the Java culture seems to have a thing against the concept of compiler warnings, one that I’ve never understood.) But no, it just parses them and silently discards them. Try throwing the following two files into the base project and watching a seemingly well-typed Java program crash and burn.

Main.java

 1 package gl.lin;
 2 
 3 import java.util.ArrayList;
 4 
 5 public class Main {
 6   public static void main(String args[]) {
 7     ArrayList<String> list = new ArrayList<String>();
 8     Generics.populate(list);
 9     for (String s: list)
10       System.out.println(s);
11   }
12 }

Generics.groovy

1 package gl.lin;
2 
3 class Generics {
4   static populate(List<String> list) {
5     list.add("string");
6     list.add(new Void()); // !
7     list.add(1);          // !
8   }
9 }

Notice how even the Groovy code explicitly states its generics, but the Groovy compiler allows us to completely disregard them and add whatever the heck we want into the list. Trying to run the program, we get

1 $ ./gradlew run
2 
3 ...
4 
5 string
6 Exception in thread "main"
7 java.lang.ClassCastException: java.lang.Void cannot be cast to java.lang.String
8 	at gl.lin.Main.main(Main.java:10)

The issue in particular we’ve hit is that on line 11 of Main.java, the compiler implicitly tries to downcast an element to String, but Groovy inserted a value of type Void.

Another interesting point is that the Groovy compiler actually does preserve the generics information in the “Java stubs” it creates, which allows Java code to call back into Groovy. Amusingly, it doesn’t actually verify whether the generics make any sense unless they all come from within the same file, so it is possible to cause the Java compiler to fail to compile code produced by the Groovy front-end.

Stuff.java

 1 package gl.lin;
 2 
 3 import java.util.*;
 4 
 5 public class Stuff {
 6   public static class A {}
 7   public static class B {}
 8   public static class AList<T extends A> extends ArrayList<T> {}
 9 
10   /* Need to reference Weirdness to produce Java stubs */
11   private static final Class c = Weirdness.class;
12 }

Weirdness.groovy

1 package gl.lin;
2 
3 class Weirdness extends Stuff.AList<Stuff.B> { }
1 build/tmp/groovy-java-stubs/gl/lin/Weirdness.java:11: type parameter gl.lin.Stuf
2 f.B is not within its bound
3   extends Stuff.AList<Stuff.B>  implements
4                            ^
5 1 error