Generics and Reflection in Java
Type erasure removes information about parameter types of generic classes at compile time. If you are given an object of a generic class at runtime, normally it is not possible to examine which parameter is actually used. However, if you have access to an object's reference, the reflection feature may provide information about the parameters of the object.
Reflection involves a set of Java classes describing the internal workings of a class Class
, Method
and Field
. Given a reference for an object, you can access these classes to retrieve information about generic type parameters.
void testArgument(SomeClass object) throws Exception {
Class clazz = object.getClass();
Field field = clazz.getDeclaredField("fieldName");
Method method = clazz.getDeclaredMethod("methodName");
}
Print parameters
The simplest way to obtain full information about parameters is through the toGenericString
method, common to Field
, Method
and Class
. Consider the following class:
class SomeClass<T> {
public Map<String, Integer> map;
public List<? extends Number> getList(T obj){
return List.of();
}
}
The following code snippet shows how to print the details of a class at runtime:
System.out.println(SomeClass.class.toGenericString()); // class SomeClass<T>
Field mapField = SomeClass.class.getDeclaredField("map");
System.out.println(mapField.toGenericString()); // public java.util.Map<java.lang.String, java.lang.Integer> SomeClass.map
Method method = SomeClass.class.getDeclaredMethod("getList", Object.class);
System.out.println(method.toGenericString()); // public java.util.List<? extends java.lang.Number> SomeClass.getList(T)
Parameterized type
The toGenericString
method is quite useful, but sometimes you need to fetch parameters one by one instead of getting a summary string. For such cases, reflection classes have the ParameterizedType
interface. It describes parameterized types and provides an array of parameter types through the method getActualTypeArguments
.
To illustrate how to use the ParameterizedType
interface, let's consider the following class:
public class DataHolder {
public Map<String, Integer> data;
public void setData(Map<String, Integer> data) {
this.data = data;
}
public Map<String, Integer> getData() {
return data;
}
}
Suppose you want to discover which parameter is used by the data field. The getGenericType
method of the Field
class produces a ParameterizedType
object that contains such information.
Field field = DataHolder.class.getDeclaredField("data");
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type rawType = parameterizedType.getRawType(); // interface java.util.Map
Type[] argumentTypes = parameterizedType.getActualTypeArguments(); // class java.lang.String, class java.lang.Integer
// or you can get type name as a String
String arg1TypeName = argumentTypes[0].getTypeName(); // java.lang.String
String arg2TypeName = argumentTypes[1].getTypeName(); // java.lang.Integer
A method may have a parameterized argument and return type. For both cases, the Method
class also provides ParameterizedType
. For instance, it is possible to examine the return type of the getData
method
Method method = DataHolder.class.getMethod("getData");
ParameterizedType parameterizedType = (ParameterizedType) method.getGenericReturnType();
or arguments of the setData
method.
Method method = DataHolder.class.getMethod("setData", Map.class);
Type[] parameterTypes = method.getGenericParameterTypes();
ParameterizedType parameterizedType = (ParameterizedType) parameterTypes[0]; // method has a single parameter
Wildcard type
ParameterizedType
allows us to extract parameters of types like List<String>
or Map<K, V>
, but it has no methods for extracting the bounds of wildcard parameters.
Take the following example of a field containing a wildcard type: List<? extends Number> wildcardField
. Using ParameterizedType
, we can obtain the ? extends Number
parameter, but not its upper or lower bounds:
Field field = ...
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type type = parameterizedType.getActualTypeArguments()[0]; // ? extends Number
To examine the bounds of wildcard types, reflection provides the WildcardType interface:
WildcardType wildcardType = (WildcardType) parameterizedType.getActualTypeArguments()[0]; // There is a single parameter
System.out.println(wildcardType.getLowerBounds()); // empty
System.out.println(wildcardType.getUpperBounds()); // Number
Type variable
We have covered how to analyze the parameterized class DataHolder
. Let's take a look at its generic version:
class GenericDataHolder<K extends String, V extends Number> {
public Map<K, V> data;
public void setData(Map<K, V> data) {
this.data = data;
}
public Map<K, V> getData() {
return data;
}
}
For classes that contain type variables (e.g. <K extends String, V extends Number>
), the TypeVariable
class allows us to retrieve detailed information about types. Similarly to the WildcardType
interface, this class provides a method for retrieving information on type bounds. We can initialize a list of TypeVariable
objects by calling the getTypeParameters
method from a Class
or Method
object.
TypeVariable<Class<GenericDataHolder>>[] typeVariables = GenericDataHolder.class.getTypeParameters();
System.out.println("Type variables count " + typeVariables.length);
System.out.println(typeVariables[0]); // K
System.out.println("First type var upper bound " + typeVariables[0].getBounds()[0]); // java.lang.String
System.out.println(typeVariables[1]); // V
System.out.println("Second type var upper bound " + typeVariables[1].getBounds()[0]); // java.lang.Number
Generic Array type
The GenericArrayType
interface provides a way to analyze generic arrays. Let's apply it to retrieve the type of a T[] genericArrayField
field:
Field field = DataHolder.class.getDeclaredField("genericArrayField");
GenericArrayType arrayType = (GenericArrayType) field.getGenericType();
System.out.println(arrayType); // T[]
System.out.println(arrayType.getGenericComponentType()); // T
Generic ancestor and interfaces
Reflection also allows us to obtain types of generic interfaces. Let's consider the example:
// Generic interface
interface GenericInterface<T> {}
// Class that implements generic interface with some type argument
class SomeClass implements GenericInterface<Boolean> {}
SomeClass.class.getGenericInterfaces(); // GenericInterface<java.lang.Boolean>
The same approach works for fetching the parameters of a parent generic class. The only difference is that we would use the getGenericSuperclass
method, rather than the getGenericInterfaces()
method.
Bridge method
Let's recall the generic class:
class Data<T> {
private T data;
public T get() {
return data;
}
public void set(T data) {
this.data = data;
}
}
and it's successor:
public class NumberData extends Data<Number> {
public void set(Number number) {
System.out.println("NumberData set");
super.set(number);
}
}
As explained in Type Erasure, the compiler generates bridge methods for get
and set
to preserve type safety. We can check whether a runtime method is a bridge method via the isBridge
method in the Method
class:
for (Method method : NumberData.class.getMethods()) {
if (method.isBridge()) {
System.out.println(method.getName());
}
}
The code snippet prints the names of bridge methods only.
Conclusion
During compilation, type erasure removes information about parameters. A generic object does not contain information on what parameters it uses. A reference to such an object, however, will still hold this information. We can retrieve type information through a feature known as reflection. Reflection consists of a set of classes and interfaces that provide details about parameters. Reflection can also reveal methods generated by the compiler such as bridge methods.