Obter tipo de parâmetro genérico em Java com reflexão

143

É possível obter o tipo de um parâmetro genérico?

Um exemplo:

public final class Voodoo {
    public static void chill(List<?> aListWithTypeSpiderMan) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        Class typeOfTheList = ???;
    }

    public static void main(String... args) {
        chill(new ArrayList<SpiderMan>());
    }
}
cimnina
fonte

Respostas:

156

Uma construção, uma vez eu tropecei parecia

Class<T> persistentClass = (Class<T>)
   ((ParameterizedType)getClass().getGenericSuperclass())
      .getActualTypeArguments()[0];

Parece haver alguma mágica de reflexão que infelizmente não compreendo completamente ... Desculpe.

DerMike
fonte
2
Mas declare-o como abstrato então. quando você quiser usá-lo, subclasse-o inline.
13763 Snicolas
42
Embora isso seja muito antigo e aceito por algum motivo, diminuí a votação porque simplesmente não responde à pergunta. Seria não dar "Homem Aranha" no exemplo dado na questão. É indubitavelmente útil em algumas situações, mas não funciona para a pergunta que foi feita.
Jon Skeet
15
Ninguém que chegou até aqui está prestes a aceitar "Não pode ser feito" como resposta. Estamos aqui porque estamos desesperados.
Addison
14
Eu recebo esta exceção: Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType não tenho certeza qual é a restrição.
29216 Dish
4
Como @Dish I just get ajava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
133

Quero tentar detalhar a resposta do @DerMike para explicar:

Primeiro, o apagamento de tipo não significa que o JDK elimina informações de tipo em tempo de execução. É um método para permitir que a verificação do tipo em tempo de compilação e a compatibilidade do tipo em tempo de execução coexistam no mesmo idioma. Como esse bloco de código implica, o JDK retém as informações de tipo apagadas - elas não estão associadas a conversões e outras coisas verificadas.

Segundo, isso fornece informações de tipo genérico para uma classe genérica exatamente um nível acima da hierarquia do tipo concreto que está sendo verificado - ou seja, uma classe pai abstrata com parâmetros de tipo genérico pode encontrar os tipos concretos correspondentes aos seus parâmetros de tipo para uma implementação concreta de si mesma. que herda diretamente dele. Se essa classe não fosse abstrata e instanciada, ou a implementação concreta estivesse em dois níveis abaixo, isso não funcionaria (embora um pouco de brincadeira pudesse fazer com que se aplicasse a qualquer número predeterminado de níveis além de um ou até a classe mais baixa com X parâmetros de tipo genérico, etc.).

Enfim, para a explicação. Aqui está o código novamente, separado em linhas para facilitar a referência:

1 # Classe genericParameter0OfThisClass = 
2 # (classe)
3 # ((ParameterizedType)
4 # getClass ()
5 # .getGenericSuperclass ())
6 # .getActualTypeArguments () [0];

Sejam 'nós' a classe abstrata com tipos genéricos que contêm esse código. Lendo isso de dentro para fora:

  • A linha 4 obtém a instância atual da classe concreta 'Class. Isso identifica o tipo concreto de nosso descendente imediato.
  • A linha 5 obtém o supertipo dessa classe como um Tipo; estes somos nós. Como somos do tipo paramétrico, podemos converter-nos com segurança no ParameterizedType (linha 3). A chave é que, quando Java determina esse objeto Type, ele usa informações de tipo presentes no filho para associar informações de tipo aos nossos parâmetros de tipo na nova instância ParameterizedType. Portanto, agora podemos acessar tipos concretos para nossos genéricos.
  • A linha 6 obtém a matriz de tipos mapeada em nossos genéricos, na ordem declarada no código da classe. Neste exemplo, extraímos o primeiro parâmetro. Isso volta como um tipo.
  • A linha 2 lança o tipo final retornado para uma classe. Isso é seguro porque sabemos quais tipos nossos parâmetros de tipo genérico são capazes de aceitar e podemos confirmar que todos serão classes (não sei como em Java seria possível obter um parâmetro genérico que não possui uma instância de Classe associado a ele, na verdade).

... e é isso mesmo. Por isso, inserimos informações de tipo de nossa própria implementação concreta em nós mesmos e as usamos para acessar um identificador de classe. poderíamos duplicar o getGenericSuperclass () e subir dois níveis, ou eliminar o getGenericSuperclass () e obter valores para nós mesmos como um tipo concreto (ressalva: não testei esses cenários, eles ainda não vieram para mim).

Fica complicado se seus filhos concretos estão a um número arbitrário de saltos de distância, ou se você é concreto e não final, e especialmente complicado se você espera que algum de seus filhos (com profundidade variada) tenha seus próprios genéricos. Mas você geralmente pode projetar em torno dessas considerações, e isso o leva a maior parte do caminho.

Espero que isso tenha ajudado alguém! Eu reconheço que este post é antigo. Provavelmente cortarei esta explicação e a guardarei para outras perguntas.

Mark McKenna
fonte
3
Apenas respondendo sua pergunta "Não tenho certeza de como, em Java, seria possível obter um parâmetro genérico que não tem uma instância de classe associada a ele, na verdade)": isso é possível se o parâmetro da classe genérica for um " expressão curinga "(diga:? estende SomeClass) ou uma" variável de tipo "(diga: <T> onde T vem de uma subclasse genérica). nos dois casos, as instâncias "Type" retornadas não podem ser convertidas em "Class", pois cada VM pode ter uma implementação diferente para ambas as quais implementam a interface Type, mas não deriva diretamente de java.lang.Class.
Roberto Andrade
19

Na verdade, consegui que isso funcionasse. Considere o seguinte trecho:

Method m;
Type[] genericParameterTypes = m.getGenericParameterTypes();
for (int i = 0; i < genericParameterTypes.length; i++) {
     if( genericParameterTypes[i] instanceof ParameterizedType ) {
                Type[] parameters = ((ParameterizedType)genericParameterTypes[i]).getActualTypeArguments();
//parameters[0] contains java.lang.String for method like "method(List<String> value)"

     }
 }

Estou usando o jdk 1.6

Andrey Rodionov
fonte
12
-1: isso não resolve o problema na pergunta; fornece apenas o tipo declarado na assinatura do método, não o tipo de tempo de execução real.
Michael Borgwardt
5
Pode não responder à pergunta do OP, mas é uma técnica útil.
dnault 12/09
3
Esta é a resposta para uma pergunta totalmente diferente.
Dawood ibn Kareem
Você pode chamar m.getParameterTypes () para obter todos os tipos reais. Ele retornaria "Lista" para o exemplo.
Lee Meador 4/16
Se genericParameterTypes [i] não for uma instância de ParameterizedType, pode ser uma Classe. Isso significa que um argumento específico para o método não é parametrizado.
Lee Meador 4/16
18

Na verdade, existe uma solução, aplicando o truque "classe anônima" e as idéias dos Super Type Tokens :

public final class Voodoo {
    public static void chill(final List<?> aListWithSomeType) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        System.out.println(aListWithSomeType.getClass().getGenericSuperclass());
        System.out.println(((ParameterizedType) aListWithSomeType
            .getClass()
            .getGenericSuperclass()).getActualTypeArguments()[0]);
    }
    public static void main(String... args) {
        chill(new ArrayList<SpiderMan>() {});
    }
}
class SpiderMan {
}

O truque consiste na criação de uma classe anônima , new ArrayList<SpiderMan>() {}, no lugar do original (simples) new ArrayList<SpiderMan>(). O uso de uma classe anômala (se possível) garante que o compilador retenha informações sobre o argumento de tipo SpiderManfornecido ao parâmetro de tipo List<?>. Voilà!

Yann-Gaël Guéhéneuc
fonte
Isso responde indiretamente à pergunta: O problema original não tem solução. Para que o tipo retenha seus parâmetros genéricos, ele precisa ser incorporado em outro tipo, como uma subclasse. Este exemplo mostra como você precisa alterar o código de chamada para tornar possível recuperar o parâmetro de tipo genérico em chill ().
Florian F
FYI, primeiro link está morto
Ashvin Sharma
@AshvinSharma Acredito que o mesmo material esteja disponível aqui: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Yann-Gaël Guéhéneuc
7

Por causa do apagamento do tipo, a única maneira de saber o tipo da lista seria passar o tipo como parâmetro para o método:

public class Main {

    public static void main(String[] args) {
        doStuff(new LinkedList<String>(), String.class);

    }

    public static <E> void doStuff(List<E> list, Class<E> clazz) {

    }

}
Jared Russell
fonte
7

Apêndice à resposta do @ DerMike para obter o parâmetro genérico de uma interface parametrizada (usando o método #getGenericInterfaces () dentro de um método padrão Java-8 para evitar duplicação):

import java.lang.reflect.ParameterizedType; 

public class ParametrizedStuff {

@SuppressWarnings("unchecked")
interface Awesomable<T> {
    default Class<T> parameterizedType() {
        return (Class<T>) ((ParameterizedType)
        this.getClass().getGenericInterfaces()[0])
            .getActualTypeArguments()[0];
    }
}

static class Beer {};
static class EstrellaGalicia implements Awesomable<Beer> {};

public static void main(String[] args) {
    System.out.println("Type is: " + new EstrellaGalicia().parameterizedType());
    // --> Type is: ParameterizedStuff$Beer
}
Campa
fonte
Não é isso que a postagem original estava pedindo. Aqui, você criou uma subclasse de Awesomeable<Beer>. Nesse caso, as informações do tipo são preservadas. Se você passar new Awesomable<Beer> ()para o método, não funcionará.
Florian F
@FlorianF Se você transmitir on Awesomable<Beer>-the-fly sem a definição explícita de uma subclasse concreta, como EstrellaGalicianeste caso, ainda está obtendo o tipo parametrizado: eu executei agora: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> O tipo é: ParameterizedStuff $ Beer
Campa
6

Não, isso não é possível. Devido a problemas de compatibilidade com versões anteriores, os genéricos do Java são baseados no apagamento de tipo , ou seja, no tempo de execução, tudo o que você tem é um Listobjeto não genérico . Há algumas informações sobre parâmetros de tipo em tempo de execução, mas ele reside nas definições de classe (ou seja, você pode perguntar " que tipo genérico a definição desse campo usa? "), Não nas instâncias de objetos.

Michael Borgwardt
fonte
Embora o java possa armazenar isso como metadados durante o tempo de execução, não é? Eu esperava que sim. Má sorte.
Cimnine
1
@ user99475: Não. Estou certo e Andrey está errado. Ele está se referindo às informações de tipo mencionadas na segunda parte da minha resposta, mas não é isso que a pergunta pede.
Michael Borgwardt
5

Conforme apontado por @bertolami, não é possível usar um tipo de variável e obter seu valor futuro (o conteúdo da variável typeOfList).

No entanto, você pode passar a classe como parâmetro desta forma:

public final class voodoo {
    public static void chill(List<T> aListWithTypeSpiderMan, Class<T> clazz) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        Class typeOfTheList = clazz;
    }

    public static void main(String... args) {
        chill(new List<SpiderMan>(), Spiderman.class );
    }
}

Isso é mais ou menos o que o Google faz quando você precisa passar uma variável de classe para o construtor de ActivityInstrumentationTestCase2 .

Snicolas
fonte
3

Não, não é possível.

Você pode obter um tipo genérico de um campo, pois uma classe é a única exceção a essa regra e até isso é um pouco complicado.

Consulte Conhecendo o tipo de genérico em Java para obter um exemplo disso.

cleto
fonte
1

Você pode obter o tipo de um parâmetro genérico com reflexão, como neste exemplo que encontrei aqui :

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Home<E> {
    @SuppressWarnings ("unchecked")
    public Class<E> getTypeParameterClass(){
        Type type = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) type;
        return (Class<E>) paramType.getActualTypeArguments()[0];
    }

    private static class StringHome extends Home<String>{}
    private static class StringBuilderHome extends Home<StringBuilder>{}
    private static class StringBufferHome extends Home<StringBuffer>{}   

    /**
     * This prints "String", "StringBuilder" and "StringBuffer"
     */
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Object object0 = new StringHome().getTypeParameterClass().newInstance();
        Object object1 = new StringBuilderHome().getTypeParameterClass().newInstance();
        Object object2 = new StringBufferHome().getTypeParameterClass().newInstance();
        System.out.println(object0.getClass().getSimpleName());
        System.out.println(object1.getClass().getSimpleName());
        System.out.println(object2.getClass().getSimpleName());
    }
}
madx
fonte
como você faria a mesma coisa para obter o V of Home <K, V>?
Rafael Sanches
1

A resposta rápida à pergunta é não, você não pode, devido ao apagamento do tipo genérico Java.

A resposta mais longa seria que, se você criou sua lista assim:

new ArrayList<SpideMan>(){}

Nesse caso, o tipo genérico é preservado na superclasse genérica da nova classe anônima acima.

Não que eu recomendo fazer isso com listas, mas é uma implementação de ouvinte:

new Listener<Type>() { public void doSomething(Type t){...}}

E como a extrapolação dos tipos genéricos de superclasses e super interfaces muda entre as JVMs, a solução genérica não é tão direta quanto algumas respostas podem sugerir.

Aqui está agora eu fiz isso.

TacB0sS
fonte
0

Isso é impossível porque os genéricos em Java são considerados apenas em tempo de compilação. Assim, os genéricos Java são apenas algum tipo de pré-processador. No entanto, você pode obter a classe real dos membros da lista.

bertolami
fonte
Sim, é isso que estou fazendo agora. Mas agora eu quero saber o tipo, mesmo que a lista esteja vazia. Mas quatro caras não podem estar errados. Obrigado a todos!
Cimnine
19
Isso não é verdade. Embora seja complicado, você pode ver no próximo post que o ParameterizedType permite fazer isso.
Edmondo1984
1
Concordo, isso pode ser feito. O exemplo do DerMike o descreve (funcionou para mim).
mac
LOL em "quatro caras não podem estar errados". Esta resposta está correta.
Dawood ibn Kareem
0

Eu codifiquei isso para métodos que esperam aceitar ou retornar Iterable<?...>. Aqui está o código:

/**
 * Assuming the given method returns or takes an Iterable<T>, this determines the type T.
 * T may or may not extend WindupVertexFrame.
 */
private static Class typeOfIterable(Method method, boolean setter)
{
    Type type;
    if (setter) {
        Type[] types = method.getGenericParameterTypes();
        // The first parameter to the method expected to be Iterable<...> .
        if (types.length == 0)
            throw new IllegalArgumentException("Given method has 0 params: " + method);
        type = types[0];
    }
    else {
        type = method.getGenericReturnType();
    }

    // Now get the parametrized type of the generic.
    if (!(type instanceof ParameterizedType))
        throw new IllegalArgumentException("Given method's 1st param type is not parametrized generic: " + method);
    ParameterizedType pType = (ParameterizedType) type;
    final Type[] actualArgs = pType.getActualTypeArguments();
    if (actualArgs.length == 0)
        throw new IllegalArgumentException("Given method's 1st param type is not parametrized generic: " + method);

    Type t = actualArgs[0];
    if (t instanceof Class)
        return (Class<?>) t;

    if (t instanceof TypeVariable){
        TypeVariable tv =  (TypeVariable) actualArgs[0];
        AnnotatedType[] annotatedBounds = tv.getAnnotatedBounds();///
        GenericDeclaration genericDeclaration = tv.getGenericDeclaration();///
        return (Class) tv.getAnnotatedBounds()[0].getType();
    }

    throw new IllegalArgumentException("Unknown kind of type: " + t.getTypeName());
}
Ondra Žižka
fonte
0

Você não pode obter um parâmetro genérico de uma variável. Mas você pode, a partir de uma declaração de método ou de campo:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Anton Pogonets
fonte
Isso me dá uma exceção no encadeamento "main" java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl #
Lluis Martinez
'params' são 'Type's que possui várias subinterfaces. TypeVariableImpl é aquele usado para argumentos do método e informa o nome do genérico dentro de <> s, não a Classe que ele representa.
Lee Meador 4/16
0

Só para mim, foi difícil ler esse trecho de código, apenas o dividi em duas linhas legíveis:

// assuming that the Generic Type parameter is of type "T"
ParameterizedType p = (ParameterizedType) getClass().getGenericSuperclass();
Class<T> c =(Class<T>)p.getActualTypeArguments()[0];

Eu queria criar uma instância do parâmetro Type sem ter nenhum parâmetro para o meu método:

publc T getNewTypeInstance(){
    ParameterizedType p = (ParameterizedType) getClass().getGenericSuperclass();
    Class<T> c =(Class<T>)p.getActualTypeArguments()[0];

    // for me i wanted to get the type to create an instance
    // from the no-args default constructor
    T t = null;
    try{
        t = c.newInstance();
    }catch(Exception e){
        // no default constructor available
    }
    return t;
}
Ahmed Adel Ismail
fonte
0

Aqui está outro truque. Use uma matriz vararg genérica

import java.util.ArrayList;

class TypedArrayList<E> extends ArrayList<E>
{
    @SafeVarargs
    public TypedArrayList (E... typeInfo)
    {
        // Get generic type at runtime ...
        System.out.println (typeInfo.getClass().getComponentType().getTypeName());
    }
}

public class GenericTest
{
    public static void main (String[] args)
    {
        // No need to supply the dummy argument
        ArrayList<Integer> ar1 = new TypedArrayList<> ();
        ArrayList<String> ar2 = new TypedArrayList<> ();
        ArrayList<?> ar3 = new TypedArrayList<> ();
    }
}
Paulo
fonte
0

Percebi que muitas pessoas se inclinam para a getGenericSuperclass()solução:

class RootGeneric<T> {
  public Class<T> persistentClass = (Class<T>)
    ((ParameterizedType)getClass().getGenericSuperclass())
      .getActualTypeArguments()[0];
}

No entanto, esta solução é propensa a erros. Ele vai não funcionar corretamente se existem genéricos nos descendentes. Considere isto:

class Foo<S> extends RootGeneric<Integer> {}

class Bar extends Foo<Double> {}

Que tipo terá Bar.persistentClass? Class<Integer>? Não, será Class<Double>. Isso acontecerá porque getClass()sempre retorna a classe mais alta, Barneste caso, e sua super classe genérica é Foo<Double>. Portanto, o tipo de argumento será Double.

Se você precisar de uma solução confiável que não falhe, posso sugerir duas.

  1. Use Guava. Tem uma classe que foi feito exatamente para esta finalidade: com.google.common.reflect.TypeToken. Ele lida com todos os casos de canto muito bem e oferece uma funcionalidade mais agradável. A desvantagem é uma dependência extra. Como você usou essa classe, seu código ficaria simples e claro, assim:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Use o método personalizado abaixo. Ele implementa uma lógica significativamente simplificada semelhante à classe Guava, mencionada acima. No entanto, eu não garantiria que seja propenso a erros. Porém, ele resolve o problema com os descendentes genéricos.
abstract class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  private Class<T> getTypeOfT() {
    Class<T> type = null;
    Class<?> iter = getClass();
    while (iter.getSuperclass() != null) {
      Class<?> next = iter.getSuperclass();
      if (next != null && next.isAssignableFrom(RootGeneric.class)) {
        type =
            (Class<T>)
                ((ParameterizedType) iter.getGenericSuperclass()).getActualTypeArguments()[0];
        break;
      }
      iter = next;
    }
    if (type == null) {
      throw new ClassCastException("Cannot determine type of T");
    }
    return type;
  }
}
real4x
fonte
-1

Usar:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
user4845560
fonte
Isto está errado. toArray()retorna Object[]. Você não pode e não deve depender de ser um subtipo específico.
Radiodef 15/0818