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

193

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

Class<Foo> cls = Foo.class;

Mais que faire si le type est générique, par exemple List? Cela fonctionne bien, mais comporte un avertissement car List doit être paramétré:

Class<List> cls = List.class

Alors pourquoi ne pas en ajouter un <?>? Eh bien, cela provoque une erreur de non-correspondance de type:

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

J'ai pensé que quelque chose comme ça fonctionnerait, mais ce n'est qu'une simple erreur de syntaxe:

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

Comment puis-je obtenir un Class<List<Foo>>statique, par exemple en utilisant le littéral de classe?

Je pourrais utiliser @SuppressWarnings("unchecked")pour me débarrasser des 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.

Aucune suggestion?

À M
la source

Réponses:

160

Vous ne pouvez pas en raison de l' effacement du type .

Les génériques Java ne sont guère plus que du sucre syntaxique pour les transformations 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érique sont conservées lors de l'exécution est avec Field.getGenericType()si vous interrogez les membres d'une classe via la réflexion.

Tout cela explique pourquoi Object.getClass()cette signature:

public final native Class<?> getClass();

La partie importante étant Class<?>.

Pour le dire autrement, à partir de la FAQ Java Generics :

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 d'exécution.

Un littéral de classe désigne un Class objet qui représente un type donné. Par exemple, le littéral de classe String.classdésigne l' Class objet qui représente le type Stringet est identique à l' Classobjet renvoyé lorsque la méthode getClassest invoquée sur un Stringobjet. Un littéral de classe peut être utilisé pour les vérifications de type d'exécution et pour la réflexion.

Les types paramétrés perdent leurs arguments de type lorsqu'ils sont traduits en code octet pendant la compilation dans un processus appelé effacement de type. En tant qu'effet secondaire de l'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 leur propre représentation de type. Par conséquent, il est inutile de former des littéraux de classe tels que List<String>.class, List<Long>.classet List<?>.class , puisque de tels Classobjets n'existent pas. Seul le type brut Lista un Class objet qui représente son type d'exécution. Il est appelé List.class.

cletus
la source
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Vous ne pouvez pas le faire en Java, vous obtenez une erreur de compilation de non-concordance de type!
DhafirNz
4
alors ... que dois-je faire si j'en ai besoin?
Christopher Francisco
2
Vous pouvez toujours tromper le compilateur parList<String> list2 = (List<String>) (Object) list1;
kftse
17
Encore un autre "ça marche juste en C #, mais pas en Java" pour moi. Je désérialise un objet JSON, et typeof (List <MyClass>) fonctionne parfaitement bien en C #, mais List <MyClass> .class est une erreur de syntaxe en Java. Oui, il y a une explication logique à cela, comme d'habitude comme l'a écrit Cletus, mais je me demande toujours pourquoi toutes ces choses fonctionnent simplement en C #.
Damn Vegetables
2
que voulez-vous dire par parfaitement légal? Cette partie du code ne se compile pas?
Eduardo Dennis
63

Il n'y a pas de littéraux de classe pour les types paramétrés, mais il existe des objets Type qui 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 qui permet de générer simplement des types paramétrés et l'utilise 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 les classes TypeToken et Gson javadoc mais Stack Overflow ne me permettra pas de publier plus d'un lien car je suis un nouvel utilisateur, vous pouvez facilement les trouver en utilisant la recherche Google

Santi P.
la source
1
Avec cela, j'ai pu créer une classe avec un E générique, puis l'utiliser clzz = new TypeToken<E>(){}.getRawType();pour itérer plus tard sur des énumérations structurées de manière similaire clzz.getEnumConstants()et enfin utiliser la réflexion pour appeler les méthodes membres à la Method method = clzz.getDeclaredMethod("getSomeFoo");victoire! Merci!
Naruto Sempai
57

Vous pouvez le gérer avec une double distribution:

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

slaurent
la source
2
En changeant la deuxième distribution de Objecten Classvous pouvez probablement économiser les frais généraux d'une distribution d'exécution vérifiée (inutile).
Clashsoft
2
@Clashsoft Utiliser Classau lieu de Object, comme vous le suggérez, semble plus significatif mais cela ne supprime pas la nécessité de l' @SuppressWarnings("unchecked")annotation, il ajoute même un nouvel avertissement:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Vous pouvez utiliser Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@Devstr Je vois que vous avez raison quand j'essaye ... Quels sont les arguments pour utiliser (Object) ou (Class <?>)?
cellepo
2
Cette réponse est totalement inutile. La raison pour laquelle OP souhaitait paramétrer le chemin de classe était qu'il avait reçu un uncheckedavertissement. Cette réponse ne change / améliore rien de tout cela. OP déclare même dans sa question qu'il ne veut pas utiliser SuppressWarnings...
Spenhouet
6

Pour expliquer la réponse de Cletus, au moment de l'exécution, tous les enregistrements des types génériques sont supprimés. 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 vraiment des raccourcis qui permettent au compilateur d'insérer des typecasts aux endroits appropriés. Par exemple, auparavant, vous devez effectuer les opérations suivantes:

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 à l'exécution, il ressemble toujours au premier exemple.

Jim Garrison
la source
Merci pour l'explication supplémentaire - ma compréhension des génériques est tellement plus claire maintenant que je me rends compte qu'ils ne sont pas un mécanisme d'exécution. :)
Tom
2
À mon avis, cela signifie simplement que les génériques ont été implémentés de manière médiocre par Sun, avec un peu de chance Oracle corrige cela un jour. L'implémentation générique de C # est beaucoup mieux (Anders est divin)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, en Java, ils l'ont implémenté de cette façon parce qu'ils voulaient que l'ancien code (antérieur à 1.5) fonctionne sur de nouvelles machines virtuelles Java sans aucun problème. Semble que c'est une décision de conception très intelligente qui se soucie de la compatibilité. Je ne pense pas qu'il y ait quoi que ce soit de médiocre là-dedans.
peter.petrov
3

Le Java Generics FAQ et donc Cletus' réponse du son comme il est inutile d'avoir Class<List<T>>, mais le vrai problème est que cela 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);

Les cast()fait regarder comme s'il est sûr, mais en réalité il n'est pas sûr du tout.


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

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

Marcono1234
la source
2

Eh bien, comme nous le savons tous, il est effacé. Mais il peut être connu dans certaines circonstances où le type est explicitement mentionné dans la hiérarchie des 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é.

Jatin
la source
C'est également la solution de contournement utilisée par JAX-RS, cf. GenericEntityet GenericType.
Hein Blöd
1

En raison du fait exposé que les littéraux de classe n'ont pas d'informations de type générique, je pense que vous devriez supposer qu'il sera impossible de se débarrasser de tous les avertissements. D'une certaine manière, l'utilisation Class<Something>est identique à l'utilisation d'une collection sans spécifier le type générique. Le mieux que j'ai pu en tirer é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);
}
Thiago Chaves
la source
1

Vous pouvez utiliser une méthode d'aide pour vous débarrasser de @SuppressWarnings("unchecked")tout dans 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 elle est rarement vraiment utile et que l'utilisation de la méthode défait la vérification de type du compilateur, je ne recommanderais pas de l'implémenter dans un endroit où elle est accessible au public.

aventurine
la source