Learn Java

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 ClassMethod 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 FieldMethod 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.

Create a free account to access the full topic

“It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.”
Andrei Maftei
Hyperskill Graduate