I'm just going to assume that anyone reading this answer already knows about type erasure. If you don't, you should follow the Type Erasure tutorial, and I'd also recommend reading through the Q&A Java generics type erasure: when and what happens.
An explanation of type erasure provides some helpful background, but it doesn't actually explain what getActualTypeArguments()
does and why the code in the question doesn't work.
For a Class c
, c.getGenericSuperclass()
effectively gets the type in the extends clause of c
.
So, for example, if we had this (using a real class):
public class ArrayList<E> extends AbstractList<E> {...}
And we said ArrayList.class.getGenericSuperclass()
, what we get is a parameterized type representing AbstractList<E>
. Again, this is the type in the extends clause.
If we had this:
ArrayList<Long> al = new ArrayList<Long>();
al.getClass()
returns ArrayList.class
, so al.getClass().getGenericSuperclass()
again returns AbstractList<E>
.
The "actual" type argument to AbstractList<E>
is the type variable E
, so if we said:
Type actualTypeArgument =
((ParameterizedType)
al.getClass()
.getGenericSuperclass())
.getActualTypeArguments() [0];
Then actualTypeArgument
refers to the type variable E
, declared by ArrayList
. (And in fact, it's the case that actualTypeArgument == ArrayList.class.getTypeParameters()[0]
is true.)
What's usually done while using this idiom is to create an anonymous subclass any time you create an instance of the type whose type argument you want to capture:
ArrayList<Long> al = new ArrayList<Long>() {};
// Note ^^
That implicitly declares a class like this:
class AnonymousArrayList1 extends ArrayList<Long> {}
(Except that the anonymous class has no name. Also, we could declare a subclass like that explicitly, instead of using an anonymous class.)
Now when we call al.getClass()
we get AnonymousArrayList1.class
whose generic superclass is ArrayList<Long>
. Now you can get the actual type argument to ArrayList<Long>
, which is Long
.
(Here's a short program which demonstrates all of the above.)
As for allowing a level of indirection, it's possible, but the code is more complicated. You're also still bound by the rule above that an actual class has to be provided as a type argument in the extends clause.
Here's an example of this which allows any number of interim superclasses:
package mcve;
import java.lang.reflect.*;
public abstract class KnowMyType<T> {
public KnowMyType() {
System.out.println(getMyType());
}
@SuppressWarnings("unchecked")
private Class<T> getMyType() {
try {
return (Class<T>) findMyType(getClass(), null);
} catch (RuntimeException x) {
throw new IllegalArgumentException("illegal type argument", x);
}
}
private Type findMyType(Class<?> plainClass, Type genericClass) {
Class<?> plainSuper = plainClass.getSuperclass();
Type genericSuper = plainClass.getGenericSuperclass();
Type t;
if (plainSuper == KnowMyType.class) {
t = ((ParameterizedType) genericSuper).getActualTypeArguments()[0];
} else {
t = findMyType(plainSuper, genericSuper);
}
if (t instanceof TypeVariable<?>) {
TypeVariable<?>[] vars = plainClass.getTypeParameters();
for (int i = 0; i < vars.length; ++i) {
if (t == vars[i]) {
t = ((ParameterizedType) genericClass).getActualTypeArguments()[i];
break;
}
}
}
return t;
}
}
That will let you do this:
class KnowMyTypeSub<T> extends KnowMyType<T> {}
new KnowMyTypeSub<Long>() {}; // constructor prints 'class java.lang.Long'
Guava's TypeToken
also does stuff like this automatically.