I noticed something while I was derping around with generics. In the example below, doStuff1
compiles but doStuff2
doesn't:
public <T extends Foo> void doStuff1(T value) {
Class<? extends Foo> theClass = value.getClass();
}
public <T extends Foo> void doStuff2(T value) {
Class<? extends T> theClass = value.getClass();
}
So, I looked up the documentation for Object.getClass()
and found this:
The actual result type is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.
This made me a bit curious. Why is getClass()
designed this way? I can understand converting types to their raw classes if applicable, but I see no obvious reason why they'd necessarily have to make it also kill off T
. Is there a specific reason why it also gets rid of it, or is it just a general "let's just get rid of everything because it's easier; who would ever need it anyway" approach?
If getClass()
returns Class<? extends X>
, nothing really bad can happen; actually it'll help a lot of use cases.
The only problem is, it is not theoretically correct. if an object is an ArrayList<String>
, its class
cannot be Class<ArrayList<String>>
- there is no such class, there is only a Class<ArrayList>
.
This is actually not related to erasure. If one day Java gets full reified types, getClass()
should still return Class<? extends |X|>
; however there should be a new method, like getType()
which can return a more detailed Type<? extends X>
. (though, getType
may conflict with a lot of existing classes with their own getType
methods)
For the timing being, since Class<? extends X>
might be useful in a lot of cases, we can design our own method that does that
static <X> Class<? extends X> myGetClass(X x){ ... }
but it's understandable they wouldn't put this kind of hack in standard lib.
Consider the following program:
var a = new ArrayList<String>();
var b = new ArrayList<Integer>();
var aType = a.getClass();
var bType = b.getClass();
if (aType == bType) {
...
}
If we execute this, aType
and bType
will contain the same runtime class object (since all instances of a generic type share the same runtime class), and the body of the if statement will execute.
It will also compile just fine. In particular, the declared types of aType
and bType
are both Class<? extends ArrayList>
, and comparing two references of compatible types makes sense to the compiler.
However, if getClass
were defined to return Class<? extends T>
instead, the compile time types of aType
would be Class<? extends ArrayList<String>
, and the type of bType
would be Class<? extends ArrayList<Integer>>
. Since Java defines generics as invariant, these are inconvertible types, and the compiler would thus be required to reject the comparision aType == bType
as nonsensical, even though the runtime thinks it true.
Since fixing this would have required a major extension to the java type system, declaring getClass
to return the erasure was likely seen as the simpler option, even though it causes counter-intuitive behavior in other cases, such as the one you encountered. And of course, once defined that way, it could no longer be changed without breaking API ...
To work around the workaround, you can use an unchecked cast:
@SuppressWarnings("unchecked")
<T> Class<? extends T> getTypedClassOf(T t) {
return (Class) t.getClass();
}
Of course, this means that you might need to resort to unchecked casts to get the compiler to understand that class objects of inconvertible types might be identical after all ...
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