web-dev-qa-db-fra.com

Java: comment obtenir un littéral de classe à partir d'un type générique?

En général, j'ai vu des gens utiliser le littéral de classe comme ceci:

Class<Foo> cls = Foo.class;

Mais que se passe-t-il si le type est générique, par exemple Liste? Cela fonctionne bien, mais il y a un avertissement puisque la liste doit être paramétrée:

Class<List> cls = List.class

Alors pourquoi ne pas ajouter un <?>? Cela provoque une erreur d'incompatibilité de type:

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

J'ai pensé que quelque chose comme ceci fonctionnerait, mais ceci est juste une simple erreur de syntaxe:

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

Comment obtenir un Class<List<Foo>> de manière statique, par exemple en utilisant le littéral de classe?

I pourrait utiliser @SuppressWarnings("unchecked") pour supprimer les avertissements causés par l'utilisation non paramétrée de List dans le premier exemple, Class<List> cls = List.class, mais je préfère ne pas le faire.

Aucune suggestion?

175
Tom

Vous ne pouvez pas à cause de effacement du type .

Les génériques Java ne sont guère plus qu'un sucre syntaxique pour les conversions d'objets. Démontrer:

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

Le seul cas où les informations de type génériques sont conservées au moment de l'exécution est avec Field.getGenericType() si vous interrogez les membres d'une classe par réflexion.

C’est pourquoi Object.getClass() a cette signature:

public final native Class<?> getClass();

La partie importante étant Class<?>.

En d'autres termes, à partir du Java Generics FAQ :

Pourquoi n'y a-t-il pas de littéral de classe pour les types paramétrés concrets?

Parce que le type paramétré n'a pas de représentation exacte du type à l'exécution.

Un littéral de classe désigne un objet Class qui représente un type donné. Par exemple, le littéral de classe String.class désigne l'objet Class qui représente le type String et est identique à l'objet Class renvoyé lorsque la méthode getClass est invoqué sur un objet String. Un littéral de classe peut être utilisé pour les contrôles de type à l'exécution et pour la réflexion.

Les types paramétrés perdent leurs arguments lorsqu'ils sont traduits en code d'octet lors de la compilation dans un processus appelé type effacement. En tant qu'effet secondaire d'effacement de type, toutes les instanciations d'un type générique partagent la même représentation d'exécution, à savoir celle du type brut correspondant. En d'autres termes, les types paramétrés n'ont pas de représentation de type propre. Par conséquent, il est inutile de former des littéraux de classe tels que List<String>.class, List<Long>.class et List<?>.class, car aucun de ces objets Class n'existe. Seul le type brut List a un objet Class qui représente son type d'exécution. Il est appelé List.class.

149
cletus

Il n'y a pas de littéraux de classe pour les types paramétrés, mais certains objets Type définissent correctement ces types.

Voir Java.lang.reflect.ParameterizedType - http://Java.Sun.com/j2se/1.5.0/docs/api/Java/lang/reflect/ParameterizedType.html

La bibliothèque Gson de Google définit une classe TypeToken permettant de générer simplement des types paramétrés et de l'utiliser pour spécifier des objets json avec des types paramétrés complexes de manière générique. Dans votre exemple, vous utiliseriez:

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

J'avais l'intention de publier des liens vers le javadoc des classes TypeToken et Gson, mais Stack Overflow ne me permet pas de publier plus d'un lien, car je suis un nouvel utilisateur. Vous pouvez facilement les trouver à l'aide de la recherche Google.

54
Santi P.

Vous pouvez le gérer avec un double casting:

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

47
slaurent

Pour expliquer la réponse de Cletus, tous les enregistrements des types génériques sont supprimés à l'exécution. Les génériques sont traités uniquement dans le compilateur et sont utilisés pour fournir une sécurité de type supplémentaire. Ce sont en réalité des raccourcis qui permettent au compilateur d'insérer des copies de type aux endroits appropriés. Par exemple, auparavant, vous deviez procéder comme suit:

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

devient

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

Cela permet au compilateur de vérifier votre code au moment de la compilation, mais au moment de l'exécution, cela ressemble toujours au premier exemple.

6
Jim Garrison

Comme nous le savons tous, cela s’efface. Mais il peut être connu dans certaines circonstances où le type est explicitement mentionné dans la hiérarchie 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;
    }
}

Et maintenant, vous pouvez faire des choses comme:

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

Plus d'infos ici . Mais encore une fois, il est presque impossible de récupérer pour:

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

où il est effacé.

2
Jatin

Etant donné que les littéraux de classe ne contiennent pas d'informations de type générique, je pense que vous devriez supposer qu'il sera impossible de supprimer tous les avertissements. D'une certaine manière, utiliser Class<Something> revient à utiliser une collection sans spécifier le type générique. Le meilleur que je pouvais sortir était:

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);
}
1
Thiago Chaves

Vous pouvez utiliser une méthode d'assistance pour vous débarrasser de @SuppressWarnings("unchecked") dans l'ensemble d'une classe.

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

Ensuite, vous pourriez écrire

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

D'autres exemples d'utilisation sont

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

Cependant, comme il est rarement très utile et que l'utilisation de la méthode annule la vérification de type du compilateur, je ne recommanderais pas de l'implémenter dans un endroit accessible au public.

0
aventurin

Le Java Generics FAQ et donc aussi le cletus réponse semble inutile, il est inutile d'avoir Class<List<T>>, mais le vrai problème est que c'est extrêmement dangereux:

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

La cast() donne l'impression que c'est sûr, mais en réalité, ce n'est pas du tout le cas.


Bien que j'arrive pas là où il ne peut pas y avoir List<?>.class = Class<List<?>> car ce serait très utile si vous avez une méthode qui détermine le type en fonction du type générique d'un Class argument.

Pour getClass() il y a JDK-6184881 demande à passer à l'utilisation de caractères génériques, mais il ne semble pas que cette modification sera effectuée (très bientôt) car elle n'est pas compatible avec le code précédent (voir ce commentaire ).

0
Marcono1234