Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java generics, type erasure and type of a generic member

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?

like image 839
4-8-15-16-23-42 Avatar asked Jan 09 '23 19:01

4-8-15-16-23-42


2 Answers

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 genericFields 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.

like image 56
Jason C Avatar answered Jan 14 '23 22:01

Jason C


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!");
}
like image 27
John Kugelman Avatar answered Jan 14 '23 22:01

John Kugelman