Java has type erasure and people say that one can't determine the type of a generic object at runtime without hacks. Consider the code below
public class TestClass<T> {
private T genericField;
public TestClass(T genericField) {
this.genericField = genericField;
}
public void printTypeInfo() {
System.out.println("Hi I'm a " + genericField.getClass());
System.out.println("Am I a string? " + (genericField instanceof String));
System.out.println("Am I a long? " + (genericField instanceof Long));
}
public static void main(String [] args) {
TestClass<String> genericString = new TestClass<>("Hello");
TestClass<Long> genericLong = new TestClass<>(111111L);
genericString.printTypeInfo();
System.out.println("------------------");
genericLong.printTypeInfo();
}
}
It gives me the following result:
Hi I'm a class java.lang.String
Am I a string? true
Am I a long? false
------------------
Hi I'm a class java.lang.Long
Am I a string? false
Am I a long? true
Seems like type information is readily available at runtime. What am I missing here?
You can determine the type of any given object in genericField
at runtime, but you cannot determine the difference between a TestClass<X>
and a TestClass<Y>
at runtime without examining some members that you know happen to be constrained by the generic type. That is you cannot determine the type parameter of a TestClass<...>
given an instance of a TestClass
alone.
Your code displays the type of genericField
's value, not the parameterized type of the TestClass
instance. Try printing this.getClass()
and you'll see its identical in both cases.
What you are "missing" is this: You are (understandably) making an incorrect connection between the fact that genericField
itself holds an object (with a type) and the fact that TestClass
has a generic type parameter. You are confusing the ability to determine the type of genericField
s value with the ability to determine the type parameter specified to a TestClass
. That is, while you can deduce what the type parameter was based on your knowledge that genericField
is T
, this is not the same as directly being able to determine what T
was, which is impossible.
Another way to look at the previous paragraph is to consider these points:
If TestClass
had no members of type T
then there's no other way you could extract T
. Your code only "determines" what T
was based on your own personal knowledge that genericField
was declared as holding that same type (and therefore the object in it must be of that type, and therefore you can conclude that the generic parameter was likely that same type or some supertype of it).
If you did not use generics, and genericField
was just an Object
, you'd still be able to determine the type of the object in genericField
. That is, its type is "independent" of the generic type, except when you use generics the compiler places a constraint on the type. It's still just an arbitrary object after compilation, regardless of whether or not you used generics (which are really just a convenience, as you could do all of this without generics and just use Object
and lots of casts instead).
Consider also the possibility of a TestClass<Base>
, where genericField
was assigned a Derived
. Your code would correctly show that genericField
was a Derived
, but you have no way of knowing that the type parameter was a Base
vs. a Derived
, because the information was erased.
Also, to ram the above points home even further:
TestClass<String> genericString = new TestClass<String>("Hello");
TestClass<?> kludge = genericString;
TestClass<Long> genericLongButNotReally = (TestClass<Long>)kludge;
genericLongButNotReally.printTypeInfo();
Outputs the info for a String
(this is why those "unchecked conversion" warnings are given, to prevent strange things like this), not caring about the fact that genericLongButNotReally
was specified with a Long
type parameter. The kludge is necessary to circumvent the good protection that the compiler offers when you use generic types; but at runtime it does not care.
TestClass<Number> genericNumber = new TestClass<>(42L);
genericNumber.printTypeInfo();
This will print Hi I'm a Long
instead of Hi I'm a Number
. You can see that genericField
is a Long
but you can't see that T
was instantiated as Number
.
Here's an example of something you can't do because of type erasure.
TestClass<?> generic = new TestClass<String>("Hello");
if (generic instanceof TestClass<String>) {
System.out.println("It holds a string!");
}
else if (generic instanceof TestClass<Long>) {
System.out.println("It holds a long!");
}
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