Two problems with generics in Java
Since generics were out and I started using them, there were always a few cases in which I couldn't make them do what I wanted. I always thought it was my problem, and that I didn't understand what was going on... Turns out: it's not. There are at least two things that are implemented in a way that break what I thought were very safe expectations.
Before Generics, you would write something like:
List list = Collections.EMPTY_LIST;
Adding generics you get a warning:
List<String> list1 = Collections.EMPTY_LIST;
GenericsGotchas.java:[19,40] [unchecked] unchecked conversion
found : java.util.List
The problem is that we have a raw type on the right, and a generic type on the other. We could suppress the warning every single time, but there is a better option: using a generic method and let the compiler infer and match the types
List<String> list2 = Collections.emptyList();
This compiles without a problem. But this does not:
List<String> list3 = new ArrayList<String>(Collections.emptyList());
GenericsGotchas.java:[21,29] cannot find symbol
symbol : constructor ArrayList(java.util.List<java.lang.Object>)
location: class java.util.ArrayList<java.lang.String
You would expect that it would, right? But it doesn't and you get an ERROR, not a warning. If you really want to put in one line, you can use (as suggest by the comments) the following
List<String> list3 = new ArrayList<String>(Collections.<String>emptyList());
The problem here is that the language is behaving in a way I was not expecting. My expectation is that whetever you have on the right side can be used directly as a parameter to a function. Generics break this expectation. That the follwoing refactoring:
a = expression;
is always valid. Unfortunately, it's not true in this corner case. So, when I am refactoring my code, or decorating an expression, I have now one more thing to remember and to pay attention to.
Casting generics type
Casting of generic types is inherently unsafe because of erasure. We all know that. But sometimes we have to do it, we expect the compiler to give us a warning and we explicitly ignore it together with a comment explaining why. There is one case in which this does not quite work. And it comes up if you are using class tokens, so I'll use those as example. A simple class object:
Class<List> class1 = List.class;
For a List of String you get:
Class<List<String>> class2 = List.class;
GenericsGotchas.java:[24,41] incompatible types
found : java.lang.Class<java.util.List>
And this is an error, rightly so. So you may expect to simply add a cast, as you normally do in these cases for non generic types:
Class<List<String>> class3 = (Class<List<String>>) List.class;
GenericsGotchas.java:[25,63] inconvertible types
found : java.lang.Class<java.util.List>
It does not work either. I understand that the compiler can't make the test, but the problem here is that I can't tell it that it's ok to make the conversion: it's an error, not a warning! So, how do you do it?
Class<List<String>> class4 = (Class<List<String>>) (Class) List.class;
You cast it first to an Class, "forgetting" all of the parameter type, then you are free to cast it to whatever you want. You get the warning, which you can suppress. Yes, this is extermely poor style, but at least it's concise. The expectation is that I can cast one type into the other, and I get a warning if the cast can in theory be done but can't guaranteed at runtime. If your type parameter is generic (a List of a Map of something), this expectation is not met.
I am surprised that I haven't been able to find them discussed anywhere, so I felt compelled to write about it. At least you know how to work around them... I think that all the problems I have been having with generics are one way or another caused by some combination of these two. The rest (erasure, bounds, ...) they all make sense to me. These do not, they feel like bugs... Maybe there is something I am missing, but I don't see why these behaviours are needed.