Java: how do I get a class literal from a generic type?

后端 未结 8 1328
北荒
北荒 2020-11-22 07:18

Typically, I\'ve seen people use the class literal like this:

Class cls = Foo.class;

But what if the type is generic, e.g. List?

相关标签:
8条回答
  • 2020-11-22 08:01

    Well as we all know that it gets erased. But it can be known under some circumstances where the type is explicitly mentioned in the class hierarchy:

    import java.lang.reflect.*;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    public abstract class CaptureType<T> {
        /**
         * {@link java.lang.reflect.Type} object of the corresponding generic type. This method is useful to obtain every kind of information (including annotations) of the generic type.
         *
         * @return Type object. null if type could not be obtained (This happens in case of generic type whose information cant be obtained using Reflection). Please refer documentation of {@link com.types.CaptureType}
         */
        public Type getTypeParam() {
            Class<?> bottom = getClass();
            Map<TypeVariable<?>, Type> reifyMap = new LinkedHashMap<>();
    
            for (; ; ) {
                Type genericSuper = bottom.getGenericSuperclass();
                if (!(genericSuper instanceof Class)) {
                    ParameterizedType generic = (ParameterizedType) genericSuper;
                    Class<?> actualClaz = (Class<?>) generic.getRawType();
                    TypeVariable<? extends Class<?>>[] typeParameters = actualClaz.getTypeParameters();
                    Type[] reified = generic.getActualTypeArguments();
                    assert (typeParameters.length != 0);
                    for (int i = 0; i < typeParameters.length; i++) {
                        reifyMap.put(typeParameters[i], reified[i]);
                    }
                }
    
                if (bottom.getSuperclass().equals(CaptureType.class)) {
                    bottom = bottom.getSuperclass();
                    break;
                }
                bottom = bottom.getSuperclass();
            }
    
            TypeVariable<?> var = bottom.getTypeParameters()[0];
            while (true) {
                Type type = reifyMap.get(var);
                if (type instanceof TypeVariable) {
                    var = (TypeVariable<?>) type;
                } else {
                    return type;
                }
            }
        }
    
        /**
         * Returns the raw type of the generic type.
         * <p>For example in case of {@code CaptureType<String>}, it would return {@code Class<String>}</p>
         * For more comprehensive examples, go through javadocs of {@link com.types.CaptureType}
         *
         * @return Class object
         * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
         * @see com.types.CaptureType
         */
        public Class<T> getRawType() {
            Type typeParam = getTypeParam();
            if (typeParam != null)
                return getClass(typeParam);
            else throw new RuntimeException("Could not obtain type information");
        }
    
    
        /**
         * Gets the {@link java.lang.Class} object of the argument type.
         * <p>If the type is an {@link java.lang.reflect.ParameterizedType}, then it returns its {@link java.lang.reflect.ParameterizedType#getRawType()}</p>
         *
         * @param type The type
         * @param <A>  type of class object expected
         * @return The Class<A> object of the type
         * @throws java.lang.RuntimeException If the type is a {@link java.lang.reflect.TypeVariable}. In such cases, it is impossible to obtain the Class object
         */
        public static <A> Class<A> getClass(Type type) {
            if (type instanceof GenericArrayType) {
                Type componentType = ((GenericArrayType) type).getGenericComponentType();
                Class<?> componentClass = getClass(componentType);
                if (componentClass != null) {
                    return (Class<A>) Array.newInstance(componentClass, 0).getClass();
                } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
            } else if (type instanceof Class) {
                Class claz = (Class) type;
                return claz;
            } else if (type instanceof ParameterizedType) {
                return getClass(((ParameterizedType) type).getRawType());
            } else if (type instanceof TypeVariable) {
                throw new RuntimeException("The type signature is erased. The type class cant be known by using reflection");
            } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
        }
    
        /**
         * This method is the preferred method of usage in case of complex generic types.
         * <p>It returns {@link com.types.TypeADT} object which contains nested information of the type parameters</p>
         *
         * @return TypeADT object
         * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
         */
        public TypeADT getParamADT() {
            return recursiveADT(getTypeParam());
        }
    
        private TypeADT recursiveADT(Type type) {
            if (type instanceof Class) {
                return new TypeADT((Class<?>) type, null);
            } else if (type instanceof ParameterizedType) {
                ArrayList<TypeADT> generic = new ArrayList<>();
                ParameterizedType type1 = (ParameterizedType) type;
                return new TypeADT((Class<?>) type1.getRawType(),
                        Arrays.stream(type1.getActualTypeArguments()).map(x -> recursiveADT(x)).collect(Collectors.toList()));
            } else throw new UnsupportedOperationException();
        }
    
    }
    
    public class TypeADT {
        private final Class<?> reify;
        private final List<TypeADT> parametrized;
    
        TypeADT(Class<?> reify, List<TypeADT> parametrized) {
            this.reify = reify;
            this.parametrized = parametrized;
        }
    
        public Class<?> getRawType() {
            return reify;
        }
    
        public List<TypeADT> getParameters() {
            return parametrized;
        }
    }
    

    And now you can do things like:

    static void test1() {
            CaptureType<String> t1 = new CaptureType<String>() {
            };
            equals(t1.getRawType(), String.class);
        }
    
        static void test2() {
            CaptureType<List<String>> t1 = new CaptureType<List<String>>() {
            };
            equals(t1.getRawType(), List.class);
            equals(t1.getParamADT().getParameters().get(0).getRawType(), String.class);
        }
    
    
        private static void test3() {
                CaptureType<List<List<String>>> t1 = new CaptureType<List<List<String>>>() {
                };
                equals(t1.getParamADT().getRawType(), List.class);
            equals(t1.getParamADT().getParameters().get(0).getRawType(), List.class);
        }
    
        static class Test4 extends CaptureType<List<String>> {
        }
    
        static void test4() {
            Test4 test4 = new Test4();
            equals(test4.getParamADT().getRawType(), List.class);
        }
    
        static class PreTest5<S> extends CaptureType<Integer> {
        }
    
        static class Test5 extends PreTest5<Integer> {
        }
    
        static void test5() {
            Test5 test5 = new Test5();
            equals(test5.getTypeParam(), Integer.class);
        }
    
        static class PreTest6<S> extends CaptureType<S> {
        }
    
        static class Test6 extends PreTest6<Integer> {
        }
    
        static void test6() {
            Test6 test6 = new Test6();
            equals(test6.getTypeParam(), Integer.class);
        }
    
    
    
        class X<T> extends CaptureType<T> {
        }
    
        class Y<A, B> extends X<B> {
        }
    
        class Z<Q> extends Y<Q, Map<Integer, List<List<List<Integer>>>>> {
        }
    
        void test7(){
            Z<String> z = new Z<>();
            TypeADT param = z.getParamADT();
            equals(param.getRawType(), Map.class);
            List<TypeADT> parameters = param.getParameters();
            equals(parameters.get(0).getRawType(), Integer.class);
            equals(parameters.get(1).getRawType(), List.class);
            equals(parameters.get(1).getParameters().get(0).getRawType(), List.class);
            equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getRawType(), List.class);
            equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getParameters().get(0).getRawType(), Integer.class);
        }
    
    
    
    
        static void test8() throws IllegalAccessException, InstantiationException {
            CaptureType<int[]> type = new CaptureType<int[]>() {
            };
            equals(type.getRawType(), int[].class);
        }
    
        static void test9(){
            CaptureType<String[]> type = new CaptureType<String[]>() {
            };
            equals(type.getRawType(), String[].class);
        }
    
        static class SomeClass<T> extends CaptureType<T>{}
        static void test10(){
            SomeClass<String> claz = new SomeClass<>();
            try{
                claz.getRawType();
                throw new RuntimeException("Shouldnt come here");
            }catch (RuntimeException ex){
    
            }
        }
    
        static void equals(Object a, Object b) {
            if (!a.equals(b)) {
                throw new RuntimeException("Test failed. " + a + " != " + b);
            }
        }
    

    More info here. But again, it is almost impossible to retrieve for:

    class SomeClass<T> extends CaptureType<T>{}
    SomeClass<String> claz = new SomeClass<>();
    

    where it gets erased.

    0 讨论(0)
  • 2020-11-22 08:11

    You can manage it with a double cast :

    @SuppressWarnings("unchecked") Class<List<Foo>> cls = (Class<List<Foo>>)(Object)List.class

    0 讨论(0)
  • 2020-11-22 08:15

    To expound on cletus' answer, at runtime all record of the generic types is removed. Generics are processed only in the compiler and are used to provide additional type safety. They are really just shorthand that allows the compiler to insert typecasts at the appropriate places. For example, previously you'd have to do the following:

    List x = new ArrayList();
    x.add(new SomeClass());
    Iterator i = x.iterator();
    SomeClass z = (SomeClass) i.next();
    

    becomes

    List<SomeClass> x = new ArrayList<SomeClass>();
    x.add(new SomeClass());
    Iterator<SomeClass> i = x.iterator();
    SomeClass z = i.next();
    

    This allows the compiler to check your code at compile-time, but at runtime it still looks like the first example.

    0 讨论(0)
  • 2020-11-22 08:16

    You could use a helper method to get rid of @SuppressWarnings("unchecked") all over a class.

    @SuppressWarnings("unchecked")
    private static <T> Class<T> generify(Class<?> cls) {
        return (Class<T>)cls;
    }
    

    Then you could write

    Class<List<Foo>> cls = generify(List.class);
    

    Other usage examples are

      Class<Map<String, Integer>> cls;
    
      cls = generify(Map.class);
    
      cls = TheClass.<Map<String, Integer>>generify(Map.class);
    
      funWithTypeParam(generify(Map.class));
    
    public void funWithTypeParam(Class<Map<String, Integer>> cls) {
    }
    

    However, since it is rarely really useful, and the usage of the method defeats the compiler's type checking, I would not recommend to implement it in a place where it is publicly accessible.

    0 讨论(0)
  • 2020-11-22 08:16

    The Java Generics FAQ and therefore also cletus' answer sound like there is no point in having Class<List<T>>, however the real problem is that this is extremely dangerous:

    @SuppressWarnings("unchecked")
    Class<List<String>> stringListClass = (Class<List<String>>) (Class<?>) List.class;
    
    List<Integer> intList = new ArrayList<>();
    intList.add(1);
    List<String> stringList = stringListClass.cast(intList);
    // Surprise!
    String firstElement = stringList.get(0);
    

    The cast() makes it look as if it is safe, but in reality it is not safe at all.


    Though I don't get where there can't be List<?>.class = Class<List<?>> since this would be pretty helpful when you have a method which determines the type based on the generic type of a Class argument.

    For getClass() there is JDK-6184881 requesting to switch to using wildcards, however it does not look like this change will be performed (very soon) since it is not compatible with previous code (see this comment).

    0 讨论(0)
  • 2020-11-22 08:18

    There are no Class literals for parameterized types, however there are Type objects that correctly define these types.

    See java.lang.reflect.ParameterizedType - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/ParameterizedType.html

    Google's Gson library defines a TypeToken class that allows to simply generate parameterized types and uses it to spec json objects with complex parameterized types in a generic friendly way. In your example you would use:

    Type typeOfListOfFoo = new TypeToken<List<Foo>>(){}.getType()
    

    I intended to post links to the TypeToken and Gson classes javadoc but Stack Overflow won't let me post more than one link since I'm a new user, you can easily find them using Google search

    0 讨论(0)
提交回复
热议问题