The method field.getGenericType()
returns a reference to the Type
interface.
The real type could be an instance of TypeVariable
or GenericArrayType
or ParameterizedType
or Class
or something else I don't know about at this moment.
Different approaches are needed to retrieve the actual type of field.
Here is my solution for getting information about public fields in the form of a tree of TypeFieldTreeNode objects.
public class TypeFieldTreeNode {
public String fieldName;
public String typeSimpleName;
public String typeCanonicalName;
public String typeGenericName;
public List<TypeFieldTreeNode> children;
public TypeFieldTreeNode(String fieldName, String typeSimpleName, String typeCanonicalName, String genericTypeName) {
this.fieldName = fieldName;
this.typeSimpleName = typeSimpleName;
this.typeCanonicalName = typeCanonicalName;
this.typeGenericName = genericTypeName;
this.children = new ArrayList<>();
}
}
Main method:
private List<TypeFieldTreeNode> getTypeFields(Class<?> clazz, Type genericType,
Map<TypeVariable<?>, Type> actualClassArguments) throws Exception {
if(clazz == null) {
return Collections.emptyList();
}
List<Field> fields = Arrays.stream(clazz.getDeclaredFields())
.filter(f -> Modifier.isPublic(f.getModifiers()) && !Modifier.isFinal(f.getModifiers()))
.collect(Collectors.toList());
List<TypeFieldTreeNode> result = new ArrayList<>();
Map<TypeVariable<?>, Type> classArgumentsMap = mapTypeActualClassArguments(
clazz, genericType, actualClassArguments);
for(Field field : fields) {
result.add(getClassFieldData(field, classArgumentsMap));
}
if(clazz.getSuperclass() != null) {
List<TypeFieldTreeNode> superClassFields =
getTypeFields(clazz.getSuperclass(), clazz.getGenericSuperclass(), classArgumentsMap);
result.addAll(superClassFields);
}
return result;
}
Next is listing of a core method that binds metadata of type TypeVariable of generic parameters with actual types of generic parameters.
Method uses the mapping obtained earlier to restore the actual type of the generic parameter when that type is an instance of TypeVariable:
private Map<TypeVariable<?>, Type> mapTypeActualClassArguments(Class<?> clazz, Type genericType,
Map<TypeVariable<?>, Type> actualClassArguments) throws Exception {
if(!(genericType instanceof ParameterizedType)) {
return Collections.emptyMap();
}
Map<TypeVariable<?>, Type> result = new HashMap<>();
Type[] actualTypeParametersTypes = ((ParameterizedType) genericType).getActualTypeArguments();
TypeVariable<?>[] classTypeParameters = clazz.getTypeParameters();
for (int i = 0; i < classTypeParameters.length; i++) {
if(actualTypeParametersTypes[i] instanceof TypeVariable<?>) {
TypeVariable<?> fieldTypeVariable = (TypeVariable<?>) actualTypeParametersTypes[i];
if(actualClassArguments.containsKey(fieldTypeVariable))
actualTypeParametersTypes[i] = actualClassArguments.get(fieldTypeVariable);
else
throw new Exception(String.format("For generic parameter %s of type %s, the corresponding actual type of generic parameter was not found",
classTypeParameters[i].getName(), genericType.getTypeName()));
}
result.put(classTypeParameters[i], actualTypeParametersTypes[i]);
}
return result;
}
Get data about a field and all available fields of the class that is the type of this field:
private TypeFieldTreeNode getClassFieldData(Field field,
Map<TypeVariable<?>, Type> actualClassArguments) throws Exception {
Class<?> fieldClass = field.getType();
Type fieldGenericType = field.getGenericType();
TypeFieldTreeNode result = null;
// if type of the field is a generic parameter of the class containing the field
if(fieldGenericType instanceof TypeVariable<?>) {
Type actualFieldType = null;
Class<?> actualFieldClass = null;
Map<TypeVariable<?>, Type> fieldTypeActualClassArguments = new HashMap<>();
TypeVariable<?> fieldTypeVariable = (TypeVariable<?>) fieldGenericType;
if(actualClassArguments.containsKey(fieldTypeVariable))
actualFieldType = actualClassArguments.get(fieldTypeVariable);
else
throw new Exception(String.format("For a field %s of type %s from class %s, the corresponding actual type of generic parameter was not found",
field.getName(), fieldGenericType.getTypeName(), field.getDeclaringClass().getCanonicalName()));
// for example, field "myField2" of class MyClass2<MyClass<Integer>> where:
// public class MyClass2<T> { public T myField2; }
// public class MyClass<T> { public T myField; }
if(actualFieldType instanceof ParameterizedType) {
actualFieldClass = (Class<?>)((ParameterizedType) actualFieldType).getRawType();
result = new TypeFieldTreeNode(field.getName(), actualFieldClass.getSimpleName(),
actualFieldClass.getCanonicalName(), actualFieldType.getTypeName());
fieldTypeActualClassArguments = mapTypeActualClassArguments(actualFieldClass, actualFieldType, actualClassArguments);
}
// for example, field "myField" of class MyClass<Integer> where:
// public class MyClass<T> { public T myField; }
else {
actualFieldClass = (Class<?>) actualFieldType;
result = new TypeFieldTreeNode(field.getName(), actualFieldClass.getSimpleName(),
actualFieldClass.getCanonicalName(), "");
}
List<Field> childFields = Arrays.stream(actualFieldClass.getFields())
.filter(f -> !Modifier.isFinal(f.getModifiers()))
.collect(Collectors.toList());
for (Field childField : childFields) {
result.children.add(getClassFieldData(childField, fieldTypeActualClassArguments));
}
}
// if the field is an array and the type of the elements of the array is a generic parameter of the class containing the field
// for example, field "myField" of class MyClass<Integer> where:
// public class MyClass<T> { public T[] myField; }
else if(fieldGenericType instanceof GenericArrayType) {
Type genericComponentType = ((GenericArrayType) fieldGenericType).getGenericComponentType();
if(genericComponentType instanceof TypeVariable<?>) {
if(actualClassArguments.containsKey(genericComponentType)) {
Type actualArrayComponentType = actualClassArguments.get(genericComponentType);
assert !(actualArrayComponentType instanceof ParameterizedType);
Class<?> actualArrayClass = (Class<?>) actualArrayComponentType;
result = new TypeFieldTreeNode(field.getName(), actualArrayClass.getSimpleName() + "[]",
actualArrayClass.getCanonicalName() + "[]", "");
}
else
throw new Exception(String.format("For a field %s of type %s from class %s, the corresponding actual type of generic parameter was not found",
field.getName(), fieldGenericType.getTypeName(), field.getDeclaringClass().getCanonicalName()));
}
else
throw new Exception(String.format("Unknown array genericComponentType: %s", genericComponentType.getClass().getCanonicalName()));
}
else {
result = new TypeFieldTreeNode(field.getName(), fieldClass.getSimpleName(), fieldClass.getCanonicalName(), "");
Map<TypeVariable<?>, Type> fieldTypeActualClassArguments = new HashMap<>();
// for example, field "myField2" of class MyClass2<Integer> where:
// public class MyClass2<T> { public MyClass<T> myField2; }
// public class MyClass<T> { public T myField; }
if(fieldGenericType instanceof ParameterizedType) {
// custom generic type name creator for situations when actual type arguments can be of type TypeVariable
result.typeGenericName = getGenericTypeName((ParameterizedType)fieldGenericType, actualClassArguments);
fieldTypeActualClassArguments = mapTypeActualClassArguments(fieldClass, fieldGenericType, actualClassArguments);
}
List<Field> childFields = Arrays.stream(fieldClass.getFields()).filter(f -> !Modifier.isFinal(f.getModifiers()))
.collect(Collectors.toList());
for (Field childField : childFields) {
result.children.add(getClassFieldData(childField, fieldTypeActualClassArguments));
}
}
return result;
}
private String getGenericTypeName(ParameterizedType parameterizedType,
Map<TypeVariable<?>, Type> actualClassArguments) throws Exception {
List<String> genericParamJavaTypes = new ArrayList<>();
for(Type typeArgument : parameterizedType.getActualTypeArguments()) {
if (typeArgument instanceof TypeVariable<?>) {
TypeVariable<?> typeVariable = (TypeVariable<?>) typeArgument;
if(actualClassArguments.containsKey(typeVariable)) {
typeArgument = actualClassArguments.get(typeVariable);
} else
throw new Exception(String.format("For generic parameter %s of type %s, the corresponding actual type of generic parameter was not found",
typeArgument.getTypeName(), parameterizedType.getTypeName()));
}
if(typeArgument instanceof ParameterizedType) {
ParameterizedType parameterizedTypeArgument = (ParameterizedType) typeArgument;
Map<TypeVariable<?>, Type> typeActualClassArguments = mapTypeActualClassArguments(
(Class<?>)parameterizedTypeArgument.getRawType(),
typeArgument, actualClassArguments);
genericParamJavaTypes.add(getGenericTypeName((ParameterizedType) typeArgument, typeActualClassArguments));
}
else if (typeArgument instanceof Class<?>)
genericParamJavaTypes.add(((Class<?>) typeArgument).getCanonicalName());
else
throw new Exception(String.format("For generic parameter %s of type %s, the corresponding actual type of generic parameter was not found", typeArgument.getTypeName()));
}
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
return rawType.getCanonicalName() + "<" + String.join(", ", genericParamJavaTypes) + ">";
}
Usage:
public List<TypeFieldTreeNode> getReturnTypeFields(Method method) throws Exception {
return getTypeFields(method.getReturnType(),
method.getGenericReturnType(), Collections.emptyMap());
}
The solution works as expected for the following test types:
MyClass2<MyClass<Integer>, MyClass<Boolean>, Double>
MyClass3<MyClass<Integer>, MyClass<Double>>
Where:
public class MyClass<T> {
public T value;
public List<String> list;
}
public class MyClass2<T, V, E> {
public T value;
public List<String> strList;
public List<V> genericList;
public int[] intArray;
public E[] genericArray;
public MyClass<E> genericClass;
}
public class MyClass3<T, V> extends MyClass2<T, V, Boolean> {
public T value3;
public List<V> genericList3;
}