Obtenir le type d'un paramètre générique en Java avec réflexion

143

Est-il possible d'obtenir le type d'un paramètre générique?

Un exemple:

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
la source

Réponses:

156

Une construction sur laquelle je suis tombé par hasard ressemblait à

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

Il semble donc y avoir une magie de réflexion autour que je ne comprends malheureusement pas complètement ... Désolé.

DerMike
la source
2
Mais déclarez-le alors comme abstrait. lorsque vous souhaitez l'utiliser, sous-classez-le en ligne.
Snicolas
42
Même si c'est très ancien et accepté pour une raison quelconque, j'ai voté contre parce que cela ne répond tout simplement pas à la question. Il ne donnerait pas "SpiderMan" dans l'exemple donné dans la question. C'est sans aucun doute utile dans certaines situations, mais cela ne fonctionne pas pour la question qui a été posée.
Jon Skeet
15
Personne qui est venu jusqu'ici n'est sur le point d'accepter «ça ne peut pas être fait» pour une réponse. Nous sommes ici parce que nous sommes désespérés.
Addison
14
J'obtiens cette exception: je Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType ne sais pas quelle est la contrainte.
Plat du
4
Comme @Dish, je viens de recevoir unjava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
133

Je veux essayer de décomposer la réponse de @DerMike pour expliquer:

Premièrement, l'effacement de type ne signifie pas que le JDK élimine les informations de type au moment de l'exécution. C'est une méthode pour permettre à la vérification de type à la compilation et à la compatibilité des types à l'exécution de coexister dans le même langage. Comme ce bloc de code l'implique, le JDK conserve les informations de type effacées - elles ne sont tout simplement pas associées aux transtypages vérifiés et autres.

Deuxièmement, cela fournit des informations de type générique à une classe générique exactement un niveau au-dessus de la hiérarchie à partir du type concret en cours de vérification - c'est-à-dire qu'une classe parent abstraite avec des paramètres de type générique peut trouver les types concrets correspondant à ses paramètres de type pour une implémentation concrète d'elle-même qui en hérite directement . Si cette classe était non abstraite et instanciée, ou si l'implémentation concrète était de deux niveaux plus bas, cela ne fonctionnerait pas (bien qu'un peu de jimmying puisse la faire appliquer à n'importe quel nombre prédéterminé de niveaux au-delà d'un, ou jusqu'à la classe la plus basse avec X paramètres de type générique, et cetera).

Quoi qu'il en soit, à l'explication. Voici à nouveau le code, séparé en lignes pour plus de facilité:

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

Soit «nous» la classe abstraite avec des types génériques qui contient ce code. Lire ceci à peu près à l'envers:

  • La ligne 4 obtient l'instance de classe de la classe concrète actuelle. Cela identifie le type concret de notre descendant immédiat.
  • La ligne 5 obtient le supertype de cette classe comme Type; c'est nous. Puisque nous sommes un type paramétrique, nous pouvons nous convertir en toute sécurité en ParameterizedType (ligne 3). La clé est que lorsque Java détermine cet objet Type, il utilise les informations de type présentes dans l'enfant pour associer les informations de type à nos paramètres de type dans la nouvelle instance ParameterizedType. Alors maintenant, nous pouvons accéder à des types concrets pour nos génériques.
  • La ligne 6 récupère le tableau des types mappés dans nos génériques, dans l'ordre déclaré dans le code de la classe. Pour cet exemple, nous retirons le premier paramètre. Cela revient en tant que type.
  • La ligne 2 lance le type final retourné à une classe. C'est sûr parce que nous savons quels types nos paramètres de type générique sont capables de prendre et pouvons confirmer qu'ils seront tous des classes (je ne sais pas comment en Java on pourrait obtenir un paramètre générique qui n'a pas d'instance de classe associé, en fait).

...Et c'est à peu près tout. Nous renvoyons donc les informations de type de notre propre implémentation concrète en nous-mêmes, et les utilisons pour accéder à un handle de classe. nous pourrions doubler getGenericSuperclass () et passer à deux niveaux, ou éliminer getGenericSuperclass () et obtenir des valeurs pour nous-mêmes en tant que type concret (mise en garde: je n'ai pas testé ces scénarios, ils ne sont pas encore venus pour moi).

Cela devient délicat si vos enfants concrets sont à un nombre arbitraire de sauts, ou si vous êtes concret et non définitif, et surtout si vous vous attendez à ce que l'un de vos enfants (de profondeur variable) ait ses propres génériques. Mais vous pouvez généralement concevoir en fonction de ces considérations, ce qui vous permet de faire la plupart du temps.

J'espère que cela a aidé quelqu'un! Je reconnais que cet article est ancien. Je vais probablement couper cette explication et la garder pour d'autres questions.

Mark McKenna
la source
3
Répondez simplement à votre "Je ne sais pas comment en Java on pourrait obtenir un paramètre générique auquel aucune instance de classe n'est associée, en fait)": Ceci est possible si le paramètre de la classe générique est soit un " expression générique "(par exemple:? étend SomeClass) ou une" variable de type "(par exemple: <T> où T provient d'une sous-classe générique). dans les deux cas, les instances "Type" renvoyées ne peuvent pas être converties en "Class" car chaque VM peut avoir une implémentation différente pour les deux qui implémentent l'interface Type mais ne dérivent pas directement de java.lang.Class.
Roberto Andrade
19

En fait, je l'ai fait fonctionner. Considérez l'extrait de code suivant:

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

     }
 }

J'utilise jdk 1.6

Andrey Rodionov
la source
12
-1: cela ne résout pas le problème de la question; il ne vous donne que le type déclaré dans la signature de la méthode, pas le type d'exécution réel.
Michael Borgwardt
5
Peut-être ne pas répondre à la question du PO, mais c'est une technique utile.
dnault le
3
C'est la réponse à une question entièrement différente.
Dawood ibn Kareem
Vous pouvez appeler m.getParameterTypes () pour obtenir tous les types réels. Il renverrait "Liste" pour l'exemple.
Lee Meador
Si genericParameterTypes [i] n'est pas une instance de ParameterizedType, il peut s'agir d'une classe. Cela signifie qu'un argument particulier de la méthode n'est pas du tout paramétré.
Lee Meador
18

Il existe une solution en fait, en appliquant l' astuce de la "classe anonyme" et les idées des 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 {
}

Les mensonges trick dans la création d'une classe anonyme , new ArrayList<SpiderMan>() {}dans le lieu de l'original (simple) new ArrayList<SpiderMan>(). L'utilisation d'une classe anoymous (si possible) garantit que le compilateur conserve les informations sur l'argument de type SpiderMandonné au paramètre de type List<?>. Voilà!

Yann-Gaël Guéhéneuc
la source
Cela répond indirectement à la question: le problème d'origine n'a pas de solution. Pour que le type conserve ses paramètres génériques, il doit être incorporé dans un autre type, tel qu'une sous-classe. Cet exemple montre comment vous devez modifier le code d'appel pour permettre de récupérer le paramètre de type générique dans chill ().
Florian F
Pour info, le premier lien est mort
Ashvin Sharma
@AshvinSharma Je crois que le même matériel est disponible ici: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Yann-Gaël Guéhéneuc
7

En raison de l'effacement de type, le seul moyen de connaître le type de la liste serait de passer le type en tant que paramètre à la méthode:

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
la source
7

Annexe à la réponse de @ DerMike pour obtenir le paramètre générique d'une interface paramétrée (en utilisant la méthode #getGenericInterfaces () dans une méthode par défaut Java-8 pour éviter la duplication):

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
la source
Ce n'est pas ce que le message original demandait. Ici, vous avez créé une sous-classe de Awesomeable<Beer>. Dans ce cas, les informations de type sont conservées. Si vous passez new Awesomable<Beer> ()à la méthode, cela ne fonctionnera pas.
Florian F
@FlorianF Si vous passez sur un Awesomable<Beer>à la volée sans la définition explicite d'une sous-classe concrète comme EstrellaGaliciadans ce cas, il sort toujours le type paramétré: Je l'ai exécuté maintenant: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> Le type est: ParameterizedStuff $ Beer
Campa
6

Non, ce n'est pas possible. En raison de problèmes de compatibilité descendante, les génériques de Java sont basés sur l' effacement de type , par exemple au moment de l'exécution, tout ce que vous avez est un non génériqueList objet . Il y a des informations sur les paramètres de type au moment de l'exécution, mais elles résident dans les définitions de classe (c'est-à-dire que vous pouvez demander " quel type générique la définition de ce champ utilise-t-elle? "), Pas dans les instances d'objet.

Michael Borgwardt
la source
Bien que java puisse stocker cela sous forme de métadonnées pendant l'exécution, n'est-ce pas? J'espérais que ce serait le cas. Malchance.
cimnine
1
@ user99475: Non. J'ai raison et Andrey a tort. Il fait référence aux informations de type que je mentionne dans la deuxième partie de ma réponse, mais ce n'est pas ce que demande la question.
Michael Borgwardt
5

Comme l'a souligné @bertolami, il n'est pas possible de nous un type de variable et d'obtenir sa valeur future (le contenu de la variable typeOfList).

Néanmoins, vous pouvez lui passer la classe en paramètre comme ceci:

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

C'est plus ou moins ce que fait Google lorsque vous devez passer une variable de classe au constructeur d' ActivityInstrumentationTestCase2 .

Snicolas
la source
3

Non, ce n'est pas possible.

Vous pouvez obtenir un type générique d'un champ étant donné qu'une classe est la seule exception à cette règle et même c'est un peu un hack.

Voir Connaître le type de générique en Java pour un exemple de cela.

cletus
la source
1

Vous pouvez obtenir le type d'un paramètre générique avec une réflexion comme dans cet exemple que j'ai trouvé ici :

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
la source
comment feriez-vous la même chose pour obtenir V de Home <K, V>?
Rafael Sanches
1

La réponse rapide à la question est non, vous ne pouvez pas, en raison de l'effacement de type générique Java.

La réponse la plus longue serait que si vous avez créé votre liste comme ceci:

new ArrayList<SpideMan>(){}

Ensuite, dans ce cas, le type générique est conservé dans la superclasse générique de la nouvelle classe anonyme ci-dessus.

Ce n'est pas que je recommande de faire cela avec des listes, mais c'est une implémentation d'écouteur:

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

Et comme l'extrapolation des types génériques de super classes et de super interfaces change entre les JVM, la solution générique n'est pas aussi simple que certaines réponses pourraient le suggérer.

Voici maintenant que je l'ai fait.

TacB0sS
la source
0

Ceci est impossible car les génériques en Java ne sont pris en compte qu'au moment de la compilation. Ainsi, les génériques Java ne sont qu'une sorte de pré-processeur. Cependant, vous pouvez obtenir la classe réelle des membres de la liste.

Bertolami
la source
Ouais c'est ce que je fais maintenant. Mais maintenant je veux connaître le type même si la liste est vide. Mais quatre gars ne peuvent pas se tromper. Merci à tous)!
cimnine
19
Ce n'est pas vrai. Bien que ce soit délicat, vous pouvez voir dans le prochain article que ParameterizedType permet de le faire.
Edmondo1984
1
D'accord, cela peut être fait. L'exemple de DerMike le décrit (a fonctionné pour moi).
mac
LOL à "quatre gars ne peuvent pas se tromper". Cette réponse est correcte.
Dawood ibn Kareem
0

J'ai codé cela pour les méthodes qui s'attendent à accepter ou à retourner Iterable<?...>. Voici le code:

/**
 * 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
la source
0

Vous ne pouvez pas obtenir de paramètre générique à partir d'une variable. Mais vous pouvez à partir d'une déclaration de méthode ou de champ:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Anton Pogonets
la source
Cela me donne une exception dans le fil "main" java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
Lluis Martinez
«params» sont des «types» qui ont plusieurs sous-interfaces. TypeVariableImpl est celui qui est utilisé pour les arguments de méthode et il indique le nom du générique dans les <>, pas la classe qu'il représente.
Lee Meador
0

Rien que pour moi, lire cet extrait de code était difficile, je l'ai juste divisé en 2 lignes lisibles:

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

Je voulais créer une instance du paramètre Type sans avoir de paramètres pour ma méthode:

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
la source
0

Voici une autre astuce. Utiliser un tableau vararg générique

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<> ();
    }
}
Paul
la source
0

J'ai remarqué que beaucoup de gens se tournent vers la getGenericSuperclass()solution:

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

Cependant, cette solution est sujette aux erreurs. Cela ne fonctionnera pas correctement s'il y a des génériques dans les descendants. Considère ceci:

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

class Bar extends Foo<Double> {}

Quel type aura Bar.persistentClass? Class<Integer>? Non, ça le sera Class<Double>. Cela se produira car getClass()renvoie toujours la classe la plus élevée, ce qui est Bardans ce cas, et sa super classe générique est Foo<Double>. Par conséquent, le type d'argument seraDouble .

Si vous avez besoin d'une solution fiable qui n'échoue pas, je peux en suggérer deux.

  1. Utilisez Guava. Il a une classe qui a été fait exactement à cet effet: com.google.common.reflect.TypeToken. Il gère très bien tous les cas d'angle et offre des fonctionnalités plus intéressantes. L'inconvénient est une dépendance supplémentaire. Étant donné que vous avez utilisé cette classe, votre code semble simple et clair, comme ceci:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Utilisez la méthode personnalisée ci-dessous. Il implémente une logique considérablement simplifiée similaire à la classe Guava, mentionnée ci-dessus. Cependant, je ne garantis pas qu'il soit sujet aux erreurs. Cela résout cependant le problème avec les descendants génériques.
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
la source
-1

Utilisation:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
user4845560
la source
C'est faux. toArray()revient Object[]. Vous ne pouvez pas et ne devriez pas dépendre du fait qu'il s'agisse d'un sous-type particulier.
Radiodef