Java: como obtenho uma literal de classe de um tipo genérico?

193

Normalmente, eu já vi pessoas usarem a classe literal assim:

Class<Foo> cls = Foo.class;

Mas e se o tipo for genérico, por exemplo, Lista? Isso funciona bem, mas possui um aviso, pois a Lista deve ser parametrizada:

Class<List> cls = List.class

Então, por que não adicionar um <?>? Bem, isso causa um erro de incompatibilidade de tipo:

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

Imaginei que algo assim funcionaria, mas este é apenas um simples erro de sintaxe:

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

Como posso obter Class<List<Foo>>estaticamente, por exemplo, usando a classe literal?

Eu poderia usar @SuppressWarnings("unchecked")para me livrar dos avisos causados ​​pelo uso não parametrizado de List no primeiro exemplo Class<List> cls = List.class, mas prefiro não.

Alguma sugestão?

Tom
fonte

Respostas:

160

Você não pode devido ao tipo de apagamento .

Os genéricos Java são pouco mais que o açúcar sintático para lançamentos de objetos. Para demonstrar:

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

A única instância em que as informações genéricas de tipo são retidas no tempo de execução é Field.getGenericType()se estiver interrogando os membros de uma classe por meio de reflexão.

Tudo isso é por que Object.getClass()tem esta assinatura:

public final native Class<?> getClass();

A parte importante é Class<?>.

Em outras palavras, nas Perguntas frequentes sobre o Java Generics :

Por que não há literal de classe para tipos parametrizados concretos?

Como o tipo parametrizado não tem representação exata do tipo de tempo de execução.

Um literal de classe denota um Class objeto que representa um determinado tipo. Por exemplo, a classe literal String.classdenota o Class objeto que representa o tipo Stringe é idêntico ao Classobjeto retornado quando o método getClassé chamado em um Stringobjeto. Um literal de classe pode ser usado para verificações de tipo de tempo de execução e para reflexão.

Tipos parametrizados perdem seus argumentos de tipo quando são convertidos em código de bytes durante a compilação em um processo chamado apagamento de tipo. Como efeito colateral da eliminação de tipo, todas as instanciações de um tipo genérico compartilham a mesma representação de tempo de execução, ou seja, a do tipo bruto correspondente. Em outras palavras, tipos parametrizados não possuem representação de tipo própria. Consequentemente, não faz sentido formar literais de classe como List<String>.class, List<Long>.classe List<?>.class , uma vez que esses Classobjetos não existem. Somente o tipo bruto Listpossui um Class objeto que representa seu tipo de tempo de execução. É referido como List.class.

cleto
fonte
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Você não pode fazer isso em Java, obtém um erro de compilação de incompatibilidade de tipo!
DhafirNz
4
então ... o que eu faço se precisar de um?
Christopher Francisco
2
Você sempre pode enganar o compilador porList<String> list2 = (List<String>) (Object) list1;
kftse
17
Ainda outro "Ele simplesmente funciona em C #, mas não em Java" para mim. Estou desserializando um objeto JSON e typeof (List <MyClass>) funciona perfeitamente em C #, mas List <MyClass> .class é um erro de sintaxe em Java. Sim, há uma explicação lógica para isso, como sempre, como Cletus escreveu, mas sempre me pergunto por que todas essas coisas funcionam em C #.
Damn Vegetables
2
como assim perfeitamente legal? Essa parte do código não compila?
Eduardo Dennis
63

Não existem literais de classe para tipos parametrizados, no entanto, existem objetos Type que definem corretamente esses tipos.

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

A biblioteca Gson do Google define uma classe TypeToken que permite simplesmente gerar tipos parametrizados e a utiliza para especificar objetos json com tipos parametrizados complexos de uma maneira amigável genérica. No seu exemplo, você usaria:

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

Pretendia postar links para as classes TypeToken e Gson javadoc, mas o Stack Overflow não me permite publicar mais de um link, já que sou um novo usuário. Você pode encontrá-los facilmente usando a Pesquisa do Google.

Santi P.
fonte
1
Com isso, eu era capaz de criar uma classe com um E genérico e depois usá-lo clzz = new TypeToken<E>(){}.getRawType();para iterar posteriormente sobre enums com estrutura semelhante clzz.getEnumConstants()e, finalmente, usar a refection para chamar os métodos de membros a uma Method method = clzz.getDeclaredMethod("getSomeFoo");vitória tão grande! Obrigado!
Naruto Sempai
57

Você pode gerenciá-lo com um elenco duplo:

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

escravo
fonte
2
Ao alterar a segunda conversão de Objectpara, Classvocê provavelmente pode economizar a sobrecarga de uma conversão de tempo de execução verificada (sem sentido).
Clashsoft
2
@Clashsoft Usando Class, em vez de Object, como você sugere, parece mais significativa mas não remove a necessidade da @SuppressWarnings("unchecked")anotação, ele ainda adicionar um novo aviso:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Você pode usar Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@ Devstr Vejo que você está correto quando tento isso ... Quais são os argumentos para usar (Object) ou (Class <?>)?
Cellepo #
2
Esta resposta é totalmente inútil. A razão pela qual o OP queria parametrizar o caminho da classe foi porque ele recebeu um uncheckedaviso. Esta resposta não altera / melhora nada disso. OP afirma mesmo na sua pergunta que não quer usar SuppressWarnings...
Spenhouet
6

Para expor a resposta do cletus, em tempo de execução, todos os registros dos tipos genéricos são removidos. Os genéricos são processados ​​apenas no compilador e são usados ​​para fornecer segurança de tipo adicional. Eles são realmente apenas uma abreviação que permite ao compilador inserir previsões de tipos nos locais apropriados. Por exemplo, anteriormente você teria que fazer o seguinte:

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

torna-se

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

Isso permite que o compilador verifique seu código em tempo de compilação, mas em tempo de execução ele ainda se parece com o primeiro exemplo.

Jim Garrison
fonte
Obrigado pela explicação adicional - minha compreensão dos genéricos é muito mais clara agora que percebo que eles não são um mecanismo de tempo de execução. :)
Tom
2
Na minha opinião, isso significa que os genéricos foram implementados de maneira medíocre pela Sun, e esperamos que a Oracle conserte isso algum dia. Implementação C # 's de genéricos é muito muito muito melhor (Anders é divino)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, em Java, eles implementaram dessa maneira porque queriam que o código antigo (pré-1.5) funcionasse em novas JVMs sem problemas. Parece que é uma decisão de design muito inteligente que se preocupa com a compatibilidade. Eu não acho que haja algo medíocre nisso.
Peter.petrov
3

As perguntas frequentes sobre o Java Generics e, portanto, também a resposta do cletus parecem não ter sentido Class<List<T>>, no entanto, o verdadeiro problema é que isso é extremamente perigoso:

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

O cast()faz parecer que é seguro, mas na realidade não é seguro.


Embora eu não chegue onde não pode haver List<?>.class=, Class<List<?>>pois isso seria bastante útil quando você tem um método que determina o tipo com base no tipo genérico de um Classargumento.

Como getClass()JDK-6184881 solicitando a troca de caracteres curinga, no entanto, parece que essa alteração não será executada (muito em breve), pois não é compatível com o código anterior (consulte este comentário ).

Marcono1234
fonte
2

Bem, como todos sabemos que é apagado. Mas pode ser conhecido em algumas circunstâncias em que o tipo é mencionado explicitamente na hierarquia de classes:

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

E agora você pode fazer coisas 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);
        }
    }

Mais informações aqui . Mas, novamente, é quase impossível recuperar para:

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

onde é apagado.

Jatin
fonte
Essa também é a solução alternativa usada pelo JAX-RS, cf. GenericEntitye GenericType.
Hein Blöd
1

Devido ao fato exposto de que literais de classe não têm informações de tipo genérico, acho que você deve assumir que será impossível se livrar de todos os avisos. De certa forma, usar Class<Something>é o mesmo que usar uma coleção sem especificar o tipo genérico. O melhor que pude apresentar foi:

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
fonte
1

Você pode usar um método auxiliar para se livrar de @SuppressWarnings("unchecked")toda a classe.

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

Então você poderia escrever

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

Outros exemplos de uso são

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

No entanto, como raramente é realmente útil e o uso do método anula a verificação de tipo do compilador, eu não recomendaria implementá-lo em um local em que seja acessível ao público.

aventurina
fonte