Récupère le type générique de java.util.List

263

J'ai;

List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();

Existe-t-il un moyen (facile) de récupérer le type générique de la liste?

Thizzer
la source
Pour pouvoir inspecter par programme un objet List et voir son type générique. Une méthode peut souhaiter insérer des objets en fonction du type générique de la collection. Ceci est possible dans les langages qui implémentent des génériques au moment de l'exécution au lieu de la compilation.
Steve Kuo
4
À droite - la seule façon d'autoriser la détection d'exécution est de sous-classer: vous POUVEZ réellement étendre le type générique, puis en utilisant la déclaration de type de recherche par réflexion du sous-type utilisé. C'est pas mal de réflexion, mais c'est possible. Malheureusement, il n'y a pas de moyen facile d'imposer que l'on doit utiliser une sous-classe générique.
StaxMan
1
Sûrement stringList contient des chaînes et des entiers integerList? Pourquoi compliquer les choses?
Ben Thurley

Réponses:

408

Si ce sont en fait des champs d'une certaine classe, vous pouvez les obtenir avec un peu d'aide à la réflexion:

package test;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;

public class Test {

    List<String> stringList = new ArrayList<String>();
    List<Integer> integerList = new ArrayList<Integer>();

    public static void main(String... args) throws Exception {
        Field stringListField = Test.class.getDeclaredField("stringList");
        ParameterizedType stringListType = (ParameterizedType) stringListField.getGenericType();
        Class<?> stringListClass = (Class<?>) stringListType.getActualTypeArguments()[0];
        System.out.println(stringListClass); // class java.lang.String.

        Field integerListField = Test.class.getDeclaredField("integerList");
        ParameterizedType integerListType = (ParameterizedType) integerListField.getGenericType();
        Class<?> integerListClass = (Class<?>) integerListType.getActualTypeArguments()[0];
        System.out.println(integerListClass); // class java.lang.Integer.
    }
}

Vous pouvez également le faire pour les types de paramètres et renvoyer le type de méthodes.

Mais s'ils se trouvent dans le même champ d'application de la classe / méthode que vous devez les connaître, il est inutile de les connaître, car vous les avez déjà déclarés vous-même.

BalusC
la source
17
Il y a certainement des situations où cela est utile ci-dessus. Dans par exemple les frameworks ORM sans configuration.
BalusC
3
..qui peut utiliser la classe # getDeclaredFields () pour obtenir tous les champs sans avoir besoin de connaître le nom du champ.
BalusC
1
BalusC: Cela ressemble à un cadre d'injection pour moi .. C'est le genre d'utilisation que je voulais dire de toute façon.
falstro
1
@loolooyyyy Plutôt que d'utiliser TypeLiteral, je recommande d'utiliser TypeToken de Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@Oleg Votre variable utilise <?>(type générique) au lieu de <Class>(type classe). Remplacez votre <?>par <Class>ou autre chose <E>.
BalusC
19

Vous pouvez également faire de même pour les paramètres de méthode:

Type[] types = method.getGenericParameterTypes();
//Now assuming that the first parameter to the method is of type List<Integer>
ParameterizedType pType = (ParameterizedType) types[0];
Class<?> clazz = (Class<?>) pType.getActualTypeArguments()[0];
System.out.println(clazz); //prints out java.lang.Integer
tsaixingwei
la source
Dans method.getGenericParameterTypes (); quelle est la méthode?
Neo Ravi
18

Réponse courte: non.

Il s'agit probablement d'un doublon, impossible d'en trouver un en ce moment.

Java utilise quelque chose appelé effacement de type, ce qui signifie qu'au moment de l'exécution, les deux objets sont équivalents. Le compilateur sait que les listes contiennent des entiers ou des chaînes et, à ce titre, peuvent maintenir un environnement sûr de type. Ces informations sont perdues (sur la base d'une instance d'objet) au moment de l'exécution, et la liste ne contient que des «objets».

Vous POUVEZ en savoir un peu plus sur la classe, par quels types elle pourrait être paramétrée, mais normalement c'est juste tout ce qui étend "Object", c'est-à-dire n'importe quoi. Si vous définissez un type comme

class <A extends MyClass> AClass {....}

AClass.class contiendra uniquement le fait que le paramètre A est délimité par MyClass, mais plus que cela, il n'y a aucun moyen de le savoir.

falstro
la source
1
Cela sera vrai si la classe concrète du générique n'est pas spécifiée; dans son exemple, il déclare explicitement les listes comme List<Integer>et List<String>; dans ces cas, l'effacement de type ne s'applique pas.
Haroldo_OK
13

Le type générique d'une collection ne devrait avoir d'importance que si elle contient réellement des objets, non? N'est-il pas plus facile de faire simplement:

Collection<?> myCollection = getUnknownCollectionFromSomewhere();
Class genericClass = null;
Iterator it = myCollection.iterator();
if (it.hasNext()){
    genericClass = it.next().getClass();
}
if (genericClass != null) { //do whatever we needed to know the type for

Il n'y a pas de type générique dans l'exécution, mais les objets à l'intérieur lors de l'exécution sont garantis comme étant du même type que le générique déclaré, il est donc assez facile de tester la classe de l'élément avant de le traiter.

Une autre chose que vous pouvez faire est simplement de traiter la liste pour obtenir des membres du bon type, en ignorant les autres (ou en les traitant différemment).

Map<Class<?>, List<Object>> classObjectMap = myCollection.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.groupingBy(Object::getClass));

// Process the list of the correct class, and/or handle objects of incorrect
// class (throw exceptions, etc). You may need to group subclasses by
// filtering the keys. For instance:

List<Number> numbers = classObjectMap.entrySet().stream()
        .filter(e->Number.class.isAssignableFrom(e.getKey()))
        .flatMap(e->e.getValue().stream())
        .map(Number.class::cast)
        .collect(Collectors.toList());

Cela vous donnera une liste de tous les éléments dont les classes étaient des sous-classes Numberdont vous pouvez ensuite traiter selon vos besoins. Les autres éléments ont été filtrés dans d'autres listes. Parce qu'ils sont sur la carte, vous pouvez les traiter comme vous le souhaitez ou les ignorer.

Si vous voulez ignorer complètement les éléments d'autres classes, cela devient beaucoup plus simple:

List<Number> numbers = myCollection.stream()
    .filter(Number.class::isInstance)
    .map(Number.class::cast)
    .collect(Collectors.toList());

Vous pouvez même créer une méthode utilitaire pour vous assurer qu'une liste contient UNIQUEMENT les éléments correspondant à une classe spécifique:

public <V> List<V> getTypeSafeItemList(Collection<Object> input, Class<V> cls) {
    return input.stream()
            .filter(cls::isInstance)
            .map(cls::cast)
            .collect(Collectors.toList());
}
Steve K
la source
5
Et si vous avez une collection vide? Ou si vous avez une collection <Object> avec un entier et une chaîne?
Falci
Le contrat de la classe peut nécessiter que seules des listes d'objets finaux soient transmises et que l'appel de méthode renvoie null si la liste est nulle, vide ou si le type de liste n'est pas valide.
ggb667
1
Dans le cas d'une collection vide, il est sûr de l'attribuer à n'importe quel type de liste au moment de l'exécution, car il n'y a rien dedans, et après l'effacement du type, une liste est une liste est une liste. Pour cette raison, je ne peux penser à aucun cas où le type d'une collection vide importera.
Steve K
Eh bien, cela "pourrait avoir de l'importance" si vous conserviez la référence dans un fil et la traitiez une fois que les objets ont commencé à apparaître dans un scénario de consommation de producteur. Si la liste avait le mauvais type d'objet, de mauvaises choses pourraient se produire. Je suppose que pour éviter que vous deviez arrêter le traitement en dormant jusqu'à ce qu'il y ait au moins un élément dans la liste avant de continuer.
ggb667
Si vous avez ce genre de scénario producteur / consommateur, planifier la liste pour avoir un certain type générique sans être sûr de ce qui pourrait être mis dans la liste est de toute façon une mauvaise conception. Au lieu de cela, vous le déclareriez comme une collection de Objects et lorsque vous les retirez de la liste, vous les donniez à un gestionnaire approprié en fonction de leur type.
Steve K
11

Pour rechercher le type générique d'un champ:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()
Mohsen Kashi
la source
10

Si vous avez besoin d'obtenir le type générique d'un type renvoyé, j'ai utilisé cette approche lorsque j'avais besoin de trouver des méthodes dans une classe qui renvoyait un Collection, puis d'accéder à leurs types génériques:

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;

public class Test {

    public List<String> test() {
        return null;
    }

    public static void main(String[] args) throws Exception {

        for (Method method : Test.class.getMethods()) {
            Class returnClass = method.getReturnType();
            if (Collection.class.isAssignableFrom(returnClass)) {
                Type returnType = method.getGenericReturnType();
                if (returnType instanceof ParameterizedType) {
                    ParameterizedType paramType = (ParameterizedType) returnType;
                    Type[] argTypes = paramType.getActualTypeArguments();
                    if (argTypes.length > 0) {
                        System.out.println("Generic type is " + argTypes[0]);
                    }
                }
            }
        }

    }

}

Cela produit:

Le type générique est la classe java.lang.String

Aram Kocharyan
la source
6

Développant la réponse de Steve K:

/** 
* Performs a forced cast.  
* Returns null if the collection type does not match the items in the list.
* @param data The list to cast.
* @param listType The type of list to cast to.
*/
static <T> List<? super T> castListSafe(List<?> data, Class<T> listType){
    List<T> retval = null;
    //This test could be skipped if you trust the callers, but it wouldn't be safe then.
    if(data!=null && !data.isEmpty() && listType.isInstance(data.iterator().next().getClass())) {
        @SuppressWarnings("unchecked")//It's OK, we know List<T> contains the expected type.
        List<T> foo = (List<T>)data;
        return retval;
    }
    return retval;
}
Usage:

protected WhateverClass add(List<?> data) {//For fluant useage
    if(data==null) || data.isEmpty(){
       throw new IllegalArgumentException("add() " + data==null?"null":"empty" 
       + " collection");
    }
    Class<?> colType = data.iterator().next().getClass();//Something
    aMethod(castListSafe(data, colType));
}

aMethod(List<Foo> foo){
   for(Foo foo: List){
      System.out.println(Foo);
   }
}

aMethod(List<Bar> bar){
   for(Bar bar: List){
      System.out.println(Bar);
   }
}
ggb667
la source
Mêmes problèmes que dans la réponse de Steve K: que faire si la liste est vide? Que faire si la liste est de type <Objet> qui contient une chaîne et un entier? Votre code signifie que vous ne pouvez ajouter plus de chaînes que si le premier élément de la liste est une chaîne, et aucun entier du tout ...
subrunner
Si la liste est vide, vous n'avez pas de chance. Si la liste est Objet et qu'elle contient en fait un Objet, vous êtes OK, mais si elle contient un mélange de choses, vous n'avez pas de chance. L'effacement signifie que c'est aussi bon que possible. L'effacement fait défaut à mon avis. Les ramifications de celui-ci sont à la fois mal comprises et insuffisantes pour répondre aux cas d'utilisation intéressants et souhaités de préoccupation typique. Rien n'empêche de créer une classe à laquelle on peut demander quel type elle prend, mais ce n'est tout simplement pas ainsi que fonctionnent les classes intégrées. Les collections devraient savoir ce qu'elles contiennent, mais hélas ...
ggb667
4

Au moment de l'exécution, non, vous ne pouvez pas.

Cependant, par réflexion, les paramètres de type sont accessibles. Essayer

for(Field field : this.getDeclaredFields()) {
    System.out.println(field.getGenericType())
}

La méthode getGenericType()renvoie un objet Type. Dans ce cas, ce sera une instance de ParametrizedType, qui à son tour a des méthodes getRawType()(qui contiendront List.class, dans ce cas) et getActualTypeArguments(), qui renverra un tableau (dans ce cas, de longueur un, contenant soit String.classou Integer.class).

Scott Morrison
la source
2
et ça marche pour les paramètres reçus par une méthode au lieu des champs d'une classe ???
opensas
@opensas Vous pouvez utiliser method.getGenericParameterTypes()pour obtenir les types déclarés des paramètres d'une méthode.
Radiodef
4

J'ai eu le même problème, mais j'ai plutôt utilisé instanceof. Est-ce ainsi:

List<Object> listCheck = (List<Object>)(Object) stringList;
    if (!listCheck.isEmpty()) {
       if (listCheck.get(0) instanceof String) {
           System.out.println("List type is String");
       }
       if (listCheck.get(0) instanceof Integer) {
           System.out.println("List type is Integer");
       }
    }
}

Cela implique l'utilisation de transtypages non contrôlés, donc ne faites cela que lorsque vous savez qu'il s'agit d'une liste et de quel type elle peut être.

Andy
la source
3

Généralement impossible, car List<String>et List<Integer>partager la même classe d'exécution.

Vous pourrez peut-être réfléchir au type déclaré du champ contenant la liste (si le type déclaré ne fait pas lui-même référence à un paramètre de type dont vous ne connaissez pas la valeur).

meriton
la source
2

Comme d'autres l'ont dit, la seule bonne réponse est non, le type a été effacé.

Si la liste contient un nombre d'éléments différent de zéro, vous pouvez rechercher le type du premier élément (en utilisant sa méthode getClass, par exemple). Cela ne vous dira pas le type générique de la liste, mais il serait raisonnable de supposer que le type générique était une superclasse des types de la liste.

Je ne préconiserais pas l'approche, mais dans une impasse, cela pourrait être utile.

Chris Arguin
la source
Cela sera vrai si la classe concrète du générique n'est pas spécifiée; dans son exemple, il déclare explicitement les listes comme List<Integer>et List<String>; dans ces cas, l'effacement de type ne s'applique pas.
Haroldo_OK
2
import org.junit.Assert;
import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class GenericTypeOfCollectionTest {
    public class FormBean {
    }

    public class MyClazz {
        private List<FormBean> list = new ArrayList<FormBean>();
    }

    @Test
    public void testName() throws Exception {
        Field[] fields = MyClazz.class.getFields();
        for (Field field : fields) {
            //1. Check if field is of Collection Type
            if (Collection.class.isAssignableFrom(field.getType())) {
                //2. Get Generic type of your field
                Class fieldGenericType = getFieldGenericType(field);
                //3. Compare with <FromBean>
                Assert.assertTrue("List<FormBean>",
                  FormBean.class.isAssignableFrom(fieldGenericType));
            }
        }
    }

    //Returns generic type of any field
    public Class getFieldGenericType(Field field) {
        if (ParameterizedType.class.isAssignableFrom(field.getGenericType().getClass())) {
            ParameterizedType genericType =
             (ParameterizedType) field.getGenericType();
            return ((Class)
              (genericType.getActualTypeArguments()[0])).getSuperclass();
        }
        //Returns dummy Boolean Class to compare with ValueObject & FormBean
        return new Boolean(false).getClass();
    }
}
Ashish
la source
1
qu'est-ce que getFieldGenericTypefieldGenericTypecela ne peut pas être trouvé
shareef
0

Utilisez Reflection pour obtenir le Fieldpour ceux-ci, alors vous pouvez simplement faire: field.genericTypepour obtenir le type qui contient également les informations sur les génériques.

Siddharth Shishulkar
la source
Pensez à ajouter un exemple de code, sinon cela convient mieux à un commentaire et non à une réponse
Alexey Kamenskiy