Obtenga el tipo de un parámetro genérico en Java con reflexión

143

¿Es posible obtener el tipo de un parámetro genérico?

Un ejemplo:

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>());
    }
}
cimnine
fuente

Respuestas:

156

Una construcción, una vez me topé parecía

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

Entonces parece haber algo de magia de reflexión alrededor que desafortunadamente no entiendo completamente ... Lo siento.

DerMike
fuente
2
Pero declararlo como abstracto entonces. cuando quieras usarlo, luego subcladéalo en línea.
Snicolas
42
Aunque esto es muy antiguo y aceptado por alguna razón, he votado negativamente porque simplemente no responde la pregunta. Sería no dar "Hombre Araña" en el ejemplo dado en la pregunta. Es indudablemente útil en algunas situaciones, pero no funciona para la pregunta que se hizo.
Jon Skeet
15
Nadie que haya llegado tan lejos está a punto de aceptar "No se puede hacer" como respuesta. Estamos aquí porque estamos desesperados.
Addison el
14
Me sale esta excepción: Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType no estoy seguro de cuál es la restricción.
Plato
44
Me gusta @Dish Acabo de recibir unjava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
133

Quiero intentar desglosar la respuesta de @DerMike para explicar:

Primero, la eliminación de tipo no significa que el JDK elimine la información de tipo en tiempo de ejecución. Es un método para permitir que la verificación de tipos en tiempo de compilación y la compatibilidad de tipos en tiempo de ejecución coexistan en el mismo idioma. Como implica este bloque de código, el JDK retiene la información del tipo borrado, simplemente no está asociado con los moldes y demás cosas verificadas.

En segundo lugar, esto proporciona información de tipo genérico a una clase genérica exactamente un nivel por encima de la jerarquía del tipo concreto que se verifica, es decir, una clase padre abstracta con parámetros de tipo genérico puede encontrar los tipos concretos correspondientes a sus parámetros de tipo para una implementación concreta de sí misma que hereda directamente de eso. Si esta clase no fuera abstracta e instanciada, o la implementación concreta se redujera dos niveles, esto no funcionaría (aunque un poco de fluctuación podría hacer que se aplique a cualquier número predeterminado de niveles más allá de uno, o hasta la clase más baja con X parámetros de tipo genérico, etcétera).

De todos modos, a la explicación. Aquí está el código nuevamente, separado en líneas para facilitar la referencia:

1 # Clase genericParameter0OfThisClass = 
2 # (clase)
3 # ((Tipo parametrizado)
4 # getClass ()
5 # .getGenericSuperclass ())
6 # .getActualTypeArguments () [0];

Permítanos 'ser' la clase abstracta con tipos genéricos que contiene este código. Leyendo esto más o menos al revés:

  • La línea 4 obtiene la instancia de clase de clase concreta actual. Esto identifica el tipo concreto de nuestro descendiente inmediato.
  • La línea 5 obtiene el supertipo de esa clase como Tipo; somos nosotros. Como somos de tipo paramétrico, podemos convertirnos en ParameterizedType (línea 3). La clave es que cuando Java determina este objeto Type, utiliza la información de tipo presente en el elemento secundario para asociar la información de tipo con nuestros parámetros de tipo en la nueva instancia de ParameterizedType. Así que ahora podemos acceder a tipos concretos para nuestros genéricos.
  • La línea 6 obtiene el conjunto de tipos mapeados en nuestros genéricos, en orden según lo declarado en el código de clase. Para este ejemplo sacamos el primer parámetro. Esto vuelve como un tipo.
  • La línea 2 arroja el tipo final devuelto a una clase. Esto es seguro porque sabemos qué tipos pueden tomar nuestros parámetros de tipo genérico y podemos confirmar que todos serán clases (no estoy seguro de cómo en Java se obtendría un parámetro genérico que no tenga una instancia de Clase asociado con él, en realidad).

... y eso es todo. Por lo tanto, enviamos información de tipo de nuestra propia implementación concreta a nosotros mismos y la usamos para acceder a un identificador de clase. podríamos doblar getGenericSuperclass () e ir a dos niveles, o eliminar getGenericSuperclass () y obtener valores para nosotros como un tipo concreto (advertencia: no he probado estos escenarios, todavía no se me han ocurrido).

Se vuelve complicado si tus hijos concretos están a una distancia arbitraria de saltos, o si eres concreto y no final, y especialmente complicado si esperas que alguno de tus hijos (variablemente profundos) tenga sus propios genéricos. Pero, por lo general, puede diseñar en torno a esas consideraciones, por lo que esto le ayuda en la mayor parte del camino.

¡Espero que esto haya ayudado a alguien! Reconozco que esta publicación es antigua. Probablemente recortaré esta explicación y la guardaré para otras preguntas.

Mark McKenna
fuente
3
Simplemente respondiendo su "No estoy seguro de cómo en Java se podría obtener un parámetro genérico que no tenga una instancia de Clase asociada, en realidad)": Esto es posible si el parámetro de la clase genérica es un " expresión comodín "(por ejemplo:? extiende SomeClass) o una" variable de tipo "(por ejemplo: <T> donde T proviene de una subclase genérica). en ambos casos, las instancias de "Tipo" devueltas no se pueden convertir a "Clase", ya que cada VM puede tener una implementación diferente para ambas, que implementa la interfaz de Tipo pero no deriva directamente de java.lang.Class.
Roberto Andrade
19

En realidad, tengo esto para trabajar. Considere el siguiente fragmento:

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)"

     }
 }

Estoy usando jdk 1.6

Andrey Rodionov
fuente
12
-1: esto no resuelve el problema en la pregunta; solo le da el tipo declarado en la firma del método, no el tipo de tiempo de ejecución real.
Michael Borgwardt
55
Es posible que no responda la pregunta del OP, pero es una técnica útil.
dnault
3
Esta es la respuesta a una pregunta completamente diferente.
Dawood ibn Kareem
Puede llamar a m.getParameterTypes () para obtener todos los tipos reales. Devolvería "Lista" para el ejemplo.
Lee Meador
Si genericParameterTypes [i] no es una instancia de ParameterizedType, podría ser una Clase. Eso significa que un argumento particular del método no está parametrizado en absoluto.
Lee Meador
18

Hay una solución en realidad, aplicando el truco de "clase anónima" y las ideas de los 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 {
}

El truco radica en la creación de una clase anónima , new ArrayList<SpiderMan>() {}en el lugar del original (simple) new ArrayList<SpiderMan>(). El uso de una clase anónima (si es posible) asegura que el compilador retiene información sobre el argumento de tipo SpiderMandado al parámetro de tipo List<?>. Voilà!

Yann-Gaël Guéhéneuc
fuente
Esto indirectamente responde a la pregunta: el problema original no tiene solución. Para que el tipo conserve sus parámetros genéricos, debe incrustarse en otro tipo, como una subclase. Este ejemplo muestra cómo debe cambiar el código de llamada para que sea posible recuperar el parámetro de tipo genérico en chill ().
Florian F
FYI, primer enlace está muerto
Ashvin Sharma
@AshvinSharma Creo que el mismo material está disponible aquí: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Yann-Gaël Guéhéneuc
7

Debido a la eliminación de tipos, la única forma de conocer el tipo de la lista sería pasar el tipo como parámetro al 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
fuente
7

Apéndice a la respuesta de @ DerMike para obtener el parámetro genérico de una interfaz parametrizada (usando el método #getGenericInterfaces () dentro de un método predeterminado Java-8 para evitar la duplicación):

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
fuente
Esto no es lo que pedía la publicación original. Aquí, debe crear una subclase de Awesomeable<Beer>. En ese caso, se conserva la información de tipo. Si pasa new Awesomable<Beer> ()al método wt no funcionará.
Florian F
@FlorianF Si pasa un Awesomable<Beer>sobre la marcha sin la definición explícita de una subclase concreta como EstrellaGaliciaen este caso, todavía está obteniendo el tipo parametrizado: lo ejecuté ahora: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> El tipo es: ParameterizedStuff $ Beer
Campa
6

No, eso no es posible. Debido a problemas de compatibilidad con versiones anteriores, los genéricos de Java se basan en el borrado de tipos , es decir, en tiempo de ejecución, todo lo que tiene es un Listobjeto no genérico . Hay información sobre los parámetros de tipo en tiempo de ejecución, pero reside en las definiciones de clase (es decir, puede preguntar " ¿qué tipo genérico utiliza la definición de este campo? "), No en instancias de objetos.

Michael Borgwardt
fuente
Aunque Java podría almacenar esto como metadatos durante el tiempo de ejecución, ¿no? Esperaba que así fuera. Mala suerte.
cimnine
1
@ user99475: No. Tengo razón y Andrey está equivocado. Se está refiriendo a la información de tipo que menciono en la segunda parte de mi respuesta, pero eso no es lo que pide la pregunta.
Michael Borgwardt
5

Como señaló @bertolami, no es posible para nosotros un tipo de variable y obtener su valor futuro (el contenido de la variable typeOfList).

Sin embargo, puede pasar la clase como parámetro de esta manera:

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

Eso es más o menos lo que hace Google cuando tiene que pasar una variable de clase al constructor de ActivityInstrumentationTestCase2 .

Snicolas
fuente
3

No, no es posible.

Puede obtener un tipo genérico de un campo dado que una clase es la única excepción a esa regla e incluso eso es un truco.

Consulte Conocer el tipo de genérico en Java para ver un ejemplo de eso.

cletus
fuente
1

Puede obtener el tipo de un parámetro genérico con reflexión como en este ejemplo que encontré aquí :

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
fuente
¿cómo harías lo mismo para obtener V of Home <K, V>?
Rafael Sanches
1

La respuesta rápida a la pregunta es no, no se puede, debido a la eliminación de tipo genérico de Java.

La respuesta más larga sería que si ha creado su lista de esta manera:

new ArrayList<SpideMan>(){}

Entonces, en este caso, el tipo genérico se conserva en la superclase genérica de la nueva clase anónima anterior.

No es que recomiendo hacer esto con listas, pero es una implementación de escucha:

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

Y dado que extrapolar los tipos genéricos de superclases y super interfaces cambian entre JVM, la solución genérica no es tan sencilla como algunas respuestas podrían sugerir.

Aquí está ahora lo hice.

TacB0sS
fuente
0

Esto es imposible porque los genéricos en Java solo se consideran en tiempo de compilación. Por lo tanto, los genéricos de Java son solo una especie de preprocesador. Sin embargo, puede obtener la clase real de los miembros de la lista.

bertolami
fuente
Sí, eso es lo que estoy haciendo ahora. Pero ahora quiero saber el tipo, incluso si la lista está vacía. Pero cuatro chicos no pueden estar equivocados. Gracias a todos)!
cimnine
19
Esto no es verdad. Si bien es complicado, puedes ver en la próxima publicación que ParameterizedType permite hacer eso.
Edmondo1984
1
De acuerdo, se puede hacer. El ejemplo de DerMike lo describe (funcionó para mí).
mac
LOL en "cuatro chicos no pueden estar equivocados". Esta respuesta es correcta.
Dawood ibn Kareem
0

Lo he codificado para métodos que esperan aceptar o devolver Iterable<?...>. Aquí está el 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
fuente
0

No puede obtener un parámetro genérico de una variable. Pero puede desde un método o declaración de campo:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Anton Pogonets
fuente
Esto me da una excepción en el hilo "main" java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
Lluis Martinez
'params' son 'Type's que tiene varias subinterfaces. TypeVariableImpl es uno que se usa para argumentos de métodos y le dice el nombre del genérico dentro de <> s, no la clase que representa.
Lee Meador
0

Solo para mí, leer este fragmento de código fue difícil, simplemente lo dividí en 2 líneas legibles:

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

Quería crear una instancia del parámetro Type sin tener ningún parámetro para mi 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
fuente
0

Aquí hay otro truco. Use una matriz genérica de vararg

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<> ();
    }
}
Pablo
fuente
0

Noté que muchas personas se inclinan hacia la getGenericSuperclass()solución:

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

Sin embargo, esta solución es propensa a errores. Será no funciona correctamente si hay genéricos en los descendientes. Considera esto:

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

class Bar extends Foo<Double> {}

¿Qué tipo tendrá Bar.persistentClass? Class<Integer>? No, lo será Class<Double>. Esto sucederá debido a que getClass()siempre devuelve la clase más alta, que es Baren este caso, y su superclase genérica es Foo<Double>. Por lo tanto, el tipo de argumento seráDouble .

Si necesita una solución confiable que no falle, puedo sugerir dos.

  1. Uso Guava. Tiene una clase que se hizo precisamente para este propósito: com.google.common.reflect.TypeToken. Maneja todos los casos de esquina muy bien y ofrece una funcionalidad más agradable. La desventaja es una dependencia extra. Dado que ha usado esta clase, su código se vería simple y claro, así:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Use el método personalizado a continuación. Implementa una lógica significativamente simplificada similar a la clase Guava, mencionada anteriormente. Sin embargo, no garantizo que sea propenso a errores. Sin embargo, resuelve el problema con los descendientes 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
fuente
-1

Utilizar:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
usuario4845560
fuente
Esto está mal. toArray()vuelve Object[]. No puede y no debe depender de que sea un subtipo particular.
Radiodef