How can I determine the type of a generic field in Java?

前端 未结 7 852
无人共我
无人共我 2020-11-27 13:25

I have been trying to determine the type of a field in a class. I\'ve seen all the introspection methods but haven\'t quite figured out how to do it. This is going to be use

相关标签:
7条回答
  • 2020-11-27 13:45

    Have a look at Obtaining Field Types from the Java Tutorial Trail: The Reflection API.

    Basically, what you need to do is to get all java.lang.reflect.Field of your class and call Field#getType() on each of them (check edit below). To get all object fields including public, protected, package and private access fields, simply use Class.getDeclaredFields(). Something like this:

    for (Field field : Person.class.getDeclaredFields()) {
        System.out.format("Type: %s%n", field.getType());
        System.out.format("GenericType: %s%n", field.getGenericType());
    }
    

    EDIT: As pointed out by wowest in a comment, you actually need to call Field#getGenericType(), check if the returned Type is a ParameterizedType and then grab the parameters accordingly. Use ParameterizedType#getRawType() and ParameterizedType#getActualTypeArgument() to get the raw type and an array of the types argument of a ParameterizedType respectively. The following code demonstrates this:

    for (Field field : Person.class.getDeclaredFields()) {
        System.out.print("Field: " + field.getName() + " - ");
        Type type = field.getGenericType();
        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType)type;
            System.out.print("Raw type: " + pType.getRawType() + " - ");
            System.out.println("Type args: " + pType.getActualTypeArguments()[0]);
        } else {
            System.out.println("Type: " + field.getType());
        }
    }
    

    And would output:

    Field: name - Type: class java.lang.String
    Field: children - Raw type: interface java.util.List - Type args: class foo.Person
    
    0 讨论(0)
  • 2020-11-27 13:49

    take this snippet:

     for (Field field : Person.class.getFields()) {
            System.out.println(field.getType());
     }
    

    the key class is Field

    0 讨论(0)
  • 2020-11-27 13:52

    Here's my take. It cannot handle every possible case (and surely has some bugs), but it does handle every case that occurs in my code so far. That includes these declarations, which should be a good start for many use cases:

      private int                                                primitiveField1;
    
      private Object                                             field1;
      private List<Integer>                                      field2;
      private Map<Integer, String>                               field3;
      private Map<? extends String, List<Map<Class<?>, Object>>> field4;
    
      private char[]                                             array1;
      private Character[]                                        array2;
      private Class<? extends Integer>[]                         array3;
      private List<Integer>[]                                    array4;
    
      private InnerClass<String>                                 innerClass;
    

    Implementation:

      public static String getDeclaration(Field field) {
        return getDeclaration(field.getGenericType());
      }
    
      private static String getDeclaration(Type genericType) {
        if(genericType instanceof ParameterizedType) {
          // types with parameters
          ParameterizedType parameterizedType = (ParameterizedType) genericType;
          String declaration = parameterizedType.getRawType().getTypeName();
          declaration += "<";
    
          Type[] typeArgs = parameterizedType.getActualTypeArguments();
    
          for(int i = 0; i < typeArgs.length; i++) {
            Type typeArg = typeArgs[i];
    
            if(i > 0) {
              declaration += ", ";
            }
    
            // note: recursive call
            declaration += getDeclaration(typeArg);
          }
    
          declaration += ">";
          declaration = declaration.replace('$', '.');
          return declaration;
        }
        else if(genericType instanceof Class<?>) {
          Class<?> clazz = (Class<?>) genericType;
    
          if(clazz.isArray()) {
            // arrays
            return clazz.getComponentType().getCanonicalName() + "[]";
          }
          else {
            // primitive and types without parameters (normal/standard types)
            return clazz.getCanonicalName();
          }
        }
        else {
          // e.g. WildcardTypeImpl (Class<? extends Integer>)
          return genericType.getTypeName();
        }
      }
    
    0 讨论(0)
  • 2020-11-27 13:57

    Here's an example that answers my question

    class Person {
      public final String name;
      public final List<Person> children;  
    }
    
    //in main
    Field[] fields = Person.class.getDeclaredFields();
    for (Field field : fields) {
      Type type = field.getGenericType();
      System.out.println("field name: " + field.getName());
      if (type instanceof ParameterizedType) {
        ParameterizedType ptype = (ParameterizedType) type;
        ptype.getRawType();
        System.out.println("-raw type:" + ptype.getRawType());
        System.out.println("-type arg: " + ptype.getActualTypeArguments()[0]);
      } else {
        System.out.println("-field type: " + field.getType());
      }
    }
    

    This outputs

    field name: name
    -field type: class java.lang.String
    field name: children
    -raw type:interface java.util.List
    -type arg: class com.blah.Person
    
    0 讨论(0)
  • 2020-11-27 13:58

    As dfa points out, you can get the erased type with java.lang.reflect.Field.getType. You can get the generic type with Field.getGenericType (which may have wildcards and bound generic parameters and all sorts of craziness). You can get the fields through Class.getDeclaredFields (Class.getFields will give you public fields (including those of the supertpye) - pointless). To get the base type fields, go through Class.getSuperclass. Note to check modifiers from Field.getModifiers - static fields probably will not be interesting to you.

    0 讨论(0)
  • 2020-11-27 13:58

    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;
    }
    
    0 讨论(0)
提交回复
热议问题