Java: ¿cómo obtengo un literal de clase de un tipo genérico?

193

Por lo general, he visto a personas usar la clase literal así:

Class<Foo> cls = Foo.class;

Pero, ¿qué pasa si el tipo es genérico, por ejemplo, Lista? Esto funciona bien, pero tiene una advertencia ya que la lista debe ser parametrizada:

Class<List> cls = List.class

Entonces, ¿por qué no agregar un <?>? Bueno, esto causa un error de desajuste de tipo:

Class<List<?>> cls = List.class

Pensé que algo como esto funcionaría, pero esto es solo un simple error de sintaxis:

Class<List<Foo>> cls = List<Foo>.class

¿Cómo puedo obtener una Class<List<Foo>>estadística, por ejemplo, usando el literal de la clase?

Yo podría utilizar @SuppressWarnings("unchecked")para deshacerse de las advertencias causadas por el uso no parametrizada de lista en el primer ejemplo, Class<List> cls = List.classpero preferiría no hacerlo.

¿Alguna sugerencia?

Tom
fuente

Respuestas:

160

No se puede debido al borrado de tipo .

Los genéricos de Java son poco más que azúcar sintáctica para lanzamientos de objetos. Demostrar:

List<Integer> list1 = new ArrayList<Integer>();
List<String> list2 = (List<String>)list1;
list2.add("foo"); // perfectly legal

El único caso en el que la información de tipo genérico se retiene en el tiempo de ejecución es Field.getGenericType()si se interroga a los miembros de una clase mediante reflexión.

Todo esto es por qué Object.getClass()tiene esta firma:

public final native Class<?> getClass();

La parte importante es Class<?>.

Para decirlo de otra manera, desde las preguntas frecuentes de Java Generics :

¿Por qué no hay una clase literal para tipos parametrizados concretos?

Porque el tipo parametrizado no tiene una representación exacta del tipo de tiempo de ejecución.

Un literal de clase denota un Class objeto que representa un tipo dado. Por ejemplo, la clase literal String.classdenota el Class objeto que representa el tipo Stringy es idéntico al Classobjeto que se devuelve cuando getClassse invoca el método en un Stringobjeto. Se puede usar un literal de clase para las comprobaciones de tipo de tiempo de ejecución y para la reflexión.

Los tipos parametrizados pierden sus argumentos de tipo cuando se traducen a código de byte durante la compilación en un proceso llamado borrado de tipo. Como efecto secundario de la eliminación de tipos, todas las instancias de un tipo genérico comparten la misma representación de tiempo de ejecución, es decir, la del tipo sin procesar correspondiente. En otras palabras, los tipos parametrizados no tienen representación de tipo propia. En consecuencia, no tiene sentido formar literales de clase como List<String>.class, List<Long>.classy List<?>.class , dado que no Classexisten tales objetos. Solo el tipo sin formato Listtiene un Class objeto que representa su tipo de tiempo de ejecución. Se le conoce como List.class.

cletus
fuente
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal ¡No puedes hacer eso en Java, obtienes un error de compilación de tipo no coincidente!
DhafirNz
44
entonces ... ¿qué hago si necesito uno?
Christopher Francisco
2
Siempre puedes engañar al compilador porList<String> list2 = (List<String>) (Object) list1;
kftse
17
Otro "Simplemente funciona en C #, pero no en Java" para mí. Estoy deserializando un objeto JSON, y typeof (List <MyClass>) funciona perfectamente bien en C #, pero List <MyClass> .class es un error de sintaxis en Java. Sí, hay una explicación lógica para eso, como siempre escribió Cletus, pero siempre me pregunto por qué todas esas cosas simplemente funcionan en C #.
Malditas verduras
2
¿Qué quieres decir con perfectamente legal? ¿Esa parte del código no se compila?
Eduardo Dennis
63

No hay literales de clase para tipos parametrizados, sin embargo, hay objetos de tipo que definen correctamente estos tipos.

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

La biblioteca Gson de Google define una clase TypeToken que permite generar simplemente tipos parametrizados y la utiliza para especificar objetos json con tipos parametrizados complejos de una manera amigable genérica. En su ejemplo, usaría:

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

Tenía la intención de publicar enlaces a las clases TypeToken y Gson javadoc, pero Stack Overflow no me permite publicar más de un enlace, ya que soy un usuario nuevo, puede encontrarlos fácilmente mediante la búsqueda de Google

Santi P.
fuente
1
¡Con esto pude crear una clase con una E genérica y luego usar clzz = new TypeToken<E>(){}.getRawType();para iterar sobre enumeraciones estructuradas de manera similar clzz.getEnumConstants()y finalmente usar refection para llamar a los métodos de miembro a la Method method = clzz.getDeclaredMethod("getSomeFoo");victoria! ¡Gracias!
Naruto Sempai
57

Puedes gestionarlo con un doble elenco:

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

inclinado
fuente
2
Al cambiar el segundo lanzamiento de Objecta Classprobablemente pueda guardar la sobrecarga de un lanzamiento de tiempo de ejecución verificado (sin sentido).
Clashsoft
2
@Clashsoft Usar en Classlugar de Object, como sugiere, parece más significativo, pero no elimina la necesidad de la @SuppressWarnings("unchecked")anotación, incluso agrega una nueva advertencia:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Se puede utilizar Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@Devstr Veo que tienes razón cuando intento eso ... ¿Cuáles son los argumentos para usar (Objeto) o (Clase <?>)?
cellepo
2
Esta respuesta es totalmente inútil. La razón por la cual OP quería parametrizar la ruta de clase fue porque recibió una uncheckedadvertencia. Esta respuesta no cambia / mejora nada de eso. OP incluso afirma en su pregunta que no quiere usar SuppressWarnings...
Spenhouet
6

Para exponer la respuesta de cletus, en tiempo de ejecución se eliminan todos los registros de los tipos genéricos. Los genéricos se procesan solo en el compilador y se utilizan para proporcionar seguridad de tipo adicional. Realmente son solo una taquigrafía que permite al compilador insertar los tipos de letra en los lugares apropiados. Por ejemplo, anteriormente tendría que hacer lo siguiente:

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

se convierte

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

Esto permite que el compilador verifique su código en tiempo de compilación, pero en tiempo de ejecución todavía se ve como el primer ejemplo.

Jim Garrison
fuente
Gracias por la explicación adicional: mi comprensión de los genéricos es mucho más clara ahora que me doy cuenta de que no son un mecanismo de tiempo de ejecución. :)
Tom
2
En mi opinión, esto solo significa que Sun implementó genéricos de manera mediocre, ojalá Oracle solucione esto algún día. La implementación de C # de genéricos es mucho, mucho, mucho mejor (Anders es divino)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, en Java lo implementaron de esa manera porque querían que el código antiguo (anterior a 1.5) funcionara en nuevas JVM sin ningún problema. Parece que es una decisión de diseño muy inteligente que se preocupa por la compatibilidad. No creo que haya nada mediocre en eso.
peter.petrov
3

Las preguntas frecuentes de Java Generics y, por lo tanto, también la respuesta de cletus suena como si no tuviera sentido Class<List<T>>, sin embargo, el verdadero problema es que esto es extremadamente peligroso:

@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);

Esto cast()hace que parezca seguro, pero en realidad no lo es en absoluto.


Aunque no entiendo donde no puede haber List<?>.class= Class<List<?>>ya que esto sería bastante útil cuando tienes un método que determina el tipo basado en el tipo genérico de un Classargumento.

Porque getClass()hay JDK-6184881 solicitando cambiar a usar comodines, sin embargo, no parece que este cambio se realizará (muy pronto) ya que no es compatible con el código anterior (vea este comentario ).

Marcono1234
fuente
2

Bueno, como todos sabemos, se borra. Pero se puede conocer en algunas circunstancias donde el tipo se menciona explícitamente en la jerarquía de clases:

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;
    }
}

Y ahora puedes hacer cosas como:

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);
        }
    }

Más información aquí . Pero, de nuevo, es casi imposible recuperarlo para:

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

donde se borra

Jatin
fuente
Esta es también la solución utilizada por JAX-RS, cf. GenericEntityy GenericType.
Hein Blöd
1

Debido al hecho expuesto de que los literales de clase no tienen información de tipo genérico, creo que debe suponer que será imposible deshacerse de todas las advertencias. En cierto modo, usar Class<Something>es lo mismo que usar una colección sin especificar el tipo genérico. Lo mejor que pude obtener fue:

private <C extends A<C>> List<C> getList(Class<C> cls) {
    List<C> res = new ArrayList<C>();
    // "snip"... some stuff happening in here, using cls
    return res;
}

public <C extends A<C>> List<A<C>> getList() {
    return getList(A.class);
}
Thiago Chaves
fuente
1

Podrías usar un método auxiliar para deshacerte de @SuppressWarnings("unchecked")toda una clase.

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

Entonces podrías escribir

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

Otros ejemplos de uso son

  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) {
}

Sin embargo, dado que rara vez es realmente útil, y el uso del método anula la verificación de tipo del compilador, no recomendaría implementarlo en un lugar donde sea de acceso público.

aventurina
fuente