I ran into some strange behaviour of Java generics today. The following code compiles fine and works as you would expect:
import java.util.*;
public class TestGeneric
{
public static void main(String[] args)
{
GenericClass<Integer> generic = new GenericClass<Integer>(7);
String stringFromList = generic.getStringList().get(0);
}
static class GenericClass<A>
{
private A objA;
private List<String> stringList;
GenericClass(A objA)
{
this.objA = objA;
stringList = new ArrayList<String>();
stringList.add("A string");
stringList.add("Another string");
}
A getObjA()
{
return objA;
}
List<String> getStringList()
{
return stringList;
}
}
}
but if you change the type of the variable generic to GenericClass (note no type parameters) compilation fails with the message "incompatible types: java.lang.Object cannot be converted to java.lang.String".
This issue seems to happen with any generic class that contains a generic object with a concrete type parameter. A few Google searches haven't turned up anything and the JLS mentions nothing about this scenario? Am I doing something wrong or is this a bug in javac?
The raw type GenericClass
is considered like erased, including generic types that are not declared by the class. So getStringList
returns a raw List
instead of a parameterized List<String>
.
I've found it difficult to find one thing in the Java specification to point to for this but it is normal behavior.
Here is another example.
public class Test {
public static void main(String[] args) {
String s;
// this compiles
s = new Generic<Object>().get();
// so does this
s = new Generic<Object>().<String>get();
// this doesn't compile
s = new Generic().get();
// neither does this
s = new Generic().<String>get();
}
}
class Generic<A> {
<B> B get() { return null; }
}
Both A
and B
are erased, yet B
is declared by a method. It is curious.
This kind of surprise nuance is why it is so obligatory to warn against using raw types.
Following up on @Radiodef's answer, I may have found the JLS language behind this:
From §15.12.2.6 (SE8):
The invocation type of a most specific accessible and applicable method is a method type (§8.2) expressing the target types of the invocation arguments, the result (return type or void) of the invocation, and the exception types of the invocation. It is determined as follows:
What this means is that once the compiler figures out what method it's calling, it then determines several properties collectively called the method type. One of those properties is the return type, which becomes the type of the expression (the method invocation); that's the only one we're interested in here.
Using the original example, in which the method (getStringList
) is not generic:
If the chosen method is not generic, then:
If unchecked conversion was necessary for the method to be applicable, the parameter types of the invocation type are the parameter types of the method's type, and the return type and thrown types are given by the erasures of the return type and thrown types of the method's type. [emphasis mine]
"Unchecked conversion" means casting a raw type to a parameterized type (§5.1.9). I couldn't find a specific definition of when "unchecked conversion [is] necessary for [a] method to be applicable". But in the form object.method
, the first step in determining applicability is to determine the class or interface to search (§15.12.1), which is the class of the object
, and what's probably going on is that since generic
(in generic.getStringList()
) has a raw type GenericClass
, and the class we need to search is GenericClass<A>
, this amounts to an unchecked conversion that is necessary for the method to be applicable. I'm not 100% sure I'm right, though.
But assuming that I got this right, that means that the above paragraph applies, and thus the return type of the invocation is the erasure of the return type of the method's type. Thus the type of generic.getStringList()
is List
, not List<String>
, as @Radiodef pointed out. The way the rule is written, it doesn't matter that the type parameter being erased from List<String>
has nothing to do with the type parameters that were missing from the raw type; tightening up the rule to erase types when they need to be erased, but not in cases like this, would probably be very difficult.
So once the return type is seen as List
and not List<String>
, then unless it's cast back to a List<String>
or assigned to something declared as List<String>
, it will be treated as a List
and the return type of a get
call will be an Object
.
The rule I quoted is for a non-generic method. For a generic method, as in @Radiodef's example, the actual rule that applies is a couple paragraphs above the one I quoted, but what it says about the return type is the same as for non-generic methods.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With