Java: Wie erhalte ich ein Klassenliteral von einem generischen Typ?

193

Normalerweise habe ich gesehen, dass Leute das Klassenliteral wie folgt verwenden:

Class<Foo> cls = Foo.class;

Aber was ist, wenn der Typ generisch ist, zB List? Dies funktioniert gut, hat aber eine Warnung, da List parametrisiert werden sollte:

Class<List> cls = List.class

Warum also nicht ein hinzufügen <?>? Nun, dies verursacht einen Typinkongruenzfehler:

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

Ich dachte, so etwas würde funktionieren, aber das ist nur ein einfacher alter Syntaxfehler:

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

Wie kann ich Class<List<Foo>>statisch eine erhalten, zB mit dem Klassenliteral?

Ich konnte verwenden , @SuppressWarnings("unchecked")um die Warnungen durch die nicht-parametrisierte Verwendung von Liste im ersten Beispiel verursacht loszuwerden, Class<List> cls = List.classaber ich möchte lieber nicht.

Irgendwelche Vorschläge?

Tom
quelle

Antworten:

160

Sie können nicht aufgrund der Löschung eingeben .

Java-Generika sind kaum mehr als syntaktischer Zucker für Objekt-Casts. Demonstrieren:

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

Die einzige Instanz, in der generische Typinformationen zur Laufzeit beibehalten werden, ist die Field.getGenericType()Abfrage der Mitglieder einer Klasse durch Reflektion.

All dies ist der Grund, warum Object.getClass()diese Unterschrift hat:

public final native Class<?> getClass();

Der wichtige Teil ist Class<?>.

Anders ausgedrückt, aus den Java Generics-FAQ :

Warum gibt es kein Klassenliteral für konkrete parametrisierte Typen?

Weil der parametrisierte Typ keine genaue Laufzeittypdarstellung hat.

Ein Klassenliteral bezeichnet ein Class Objekt, das einen bestimmten Typ darstellt. Beispielsweise bezeichnet das Klassenliteral String.classdas Class Objekt, das den Typ darstellt, Stringund ist identisch mit dem ClassObjekt, das zurückgegeben wird, wenn die Methode getClassfür ein StringObjekt aufgerufen wird. Ein Klassenliteral kann zur Laufzeitprüfung und zur Reflexion verwendet werden.

Parametrisierte Typen verlieren ihre Typargumente, wenn sie während der Kompilierung in einem Prozess namens Typlöschung in Bytecode übersetzt werden. Als Nebeneffekt der Typlöschung haben alle Instanziierungen eines generischen Typs dieselbe Laufzeitdarstellung, nämlich die des entsprechenden Rohtyps. Mit anderen Worten, parametrisierte Typen haben keine eigene Typendarstellung. Folglich gibt es keinen Punkt Klasse Literale bei der Bildung wie List<String>.class, List<Long>.classund List<?>.class , da solche ClassObjekte vorhanden sind . Nur der Rohtyp Listhat ein Class Objekt, das seinen Laufzeittyp darstellt. Es wird als bezeichnet List.class.

Cletus
quelle
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal In Java ist dies nicht möglich. Es wird ein Kompilierungsfehler beim Typ-Mismatch angezeigt!
DhafirNz
4
Also ... was mache ich, wenn ich einen brauche?
Christopher Francisco
2
Sie können den Compiler immer täuschen, indem SieList<String> list2 = (List<String>) (Object) list1;
kftse
17
Noch ein "Es funktioniert nur in C #, aber nicht in Java" für mich. Ich deserialisiere ein JSON-Objekt und typeof (List <MyClass>) funktioniert in C # einwandfrei, aber List <MyClass> .class ist ein Syntaxfehler in Java. Ja, es gibt eine logische Erklärung dafür, wie Cletus es geschrieben hat, aber ich frage mich immer, warum all diese Dinge nur in C # funktionieren.
Verdammtes Gemüse
2
Was meinst du damit vollkommen legal? Dieser Teil des Codes wird nicht kompiliert?
Eduardo Dennis
63

Es gibt keine Klassenliterale für parametrisierte Typen, es gibt jedoch Typobjekte, die diese Typen korrekt definieren.

Siehe java.lang.reflect.ParameterizedType - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/ParameterizedType.html

Die Gson-Bibliothek von Google definiert eine TypeToken-Klasse, mit der einfach parametrisierte Typen generiert und json-Objekte mit komplexen parametrisierten Typen generisch benutzerfreundlich spezifiziert werden können. In Ihrem Beispiel würden Sie verwenden:

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

Ich wollte Links zu den Javadoc-Klassen TypeToken und Gson veröffentlichen, aber mit Stack Overflow kann ich nicht mehr als einen Link veröffentlichen, da ich ein neuer Benutzer bin. Sie können sie mithilfe der Google-Suche leicht finden

Santi P.
quelle
1
Damit konnte ich eine Klasse mit einem generischen E erstellen und clzz = new TypeToken<E>(){}.getRawType();später über ähnlich strukturierte Aufzählungen iterieren clzz.getEnumConstants()und schließlich die Refektion verwenden, um Mitgliedsmethoden a la Method method = clzz.getDeclaredMethod("getSomeFoo");so viel Gewinn aufzurufen ! Danke!
Naruto Sempai
57

Sie können es mit einer Doppelbesetzung verwalten:

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

slaurent
quelle
2
Durch Ändern der zweiten Umwandlung von Objectin können ClassSie wahrscheinlich den Overhead einer (sinnlosen) überprüften Laufzeitbesetzung sparen.
Clashsoft
2
@Clashsoft Verwenden Classanstelle von Object, wie Sie vorschlagen, scheint sinnvoller, aber es entfernt nicht die Notwendigkeit der @SuppressWarnings("unchecked")Anmerkung, es fügt sogar eine neue Warnung hinzu:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Sie können verwenden Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@ Devstr Ich sehe, dass Sie richtig sind, wenn ich das versuche ... Was sind die Argumente für die Verwendung von (Object) oder (Class <?>)?
Cellepo
2
Diese Antwort ist völlig sinnlos. Der Grund, warum OP den Klassenpfad parametrisieren wollte, war, dass er eine uncheckedWarnung erhielt. Diese Antwort ändert / verbessert nichts davon. OP sagt sogar in seiner Frage, dass er nicht verwenden will SuppressWarnings...
Spenhouet
6

Um die Antwort von cletus zu erläutern, werden zur Laufzeit alle Datensätze der generischen Typen entfernt. Generika werden nur im Compiler verarbeitet und dienen der zusätzlichen Typensicherheit. Sie sind wirklich nur eine Abkürzung, mit der der Compiler Typecasts an den entsprechenden Stellen einfügen kann. Zuvor mussten Sie beispielsweise Folgendes tun:

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

wird

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

Auf diese Weise kann der Compiler Ihren Code zur Kompilierungszeit überprüfen, zur Laufzeit sieht er jedoch immer noch wie das erste Beispiel aus.

Jim Garrison
quelle
Vielen Dank für die zusätzliche Erklärung - mein Verständnis von Generika ist jetzt so viel klarer, dass ich merke, dass sie kein Laufzeitmechanismus sind. :)
Tom
2
Meiner Meinung nach bedeutet dies nur, dass Generika von Sun mittelmäßig implementiert wurden, hoffentlich behebt Oracle dies eines Tages. C # 's Implementierung von Generika ist viel, viel besser (Anders ist gottähnlich)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, in Java haben sie es so implementiert, weil sie wollten, dass alter Code (vor 1.5) ohne Probleme auf neuen JVMs funktioniert. Es scheint eine sehr kluge Designentscheidung zu sein, bei der es um Kompatibilität geht. Ich denke nicht, dass daran etwas Mittelmäßiges ist.
Peter.petrov
3

Die Java Generics-FAQ und damit auch die Antwort von cletus klingen so, als hätte es keinen Sinn, sie zu Class<List<T>>haben. Das eigentliche Problem ist jedoch, dass dies äußerst gefährlich ist:

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

Das cast()lässt es so aussehen, als ob es sicher ist, aber in Wirklichkeit ist es überhaupt nicht sicher.


Obwohl ich nicht verstehe, wo es nicht geben kann List<?>.class=, Class<List<?>>da dies ziemlich hilfreich wäre, wenn Sie eine Methode haben, die den Typ basierend auf dem generischen Typ eines ClassArguments bestimmt.

Es getClass()gibt JDK-6184881 , das die Umstellung auf die Verwendung von Platzhaltern anfordert. Es sieht jedoch nicht so aus, als würde diese Änderung (sehr bald) durchgeführt, da sie nicht mit dem vorherigen Code kompatibel ist (siehe diesen Kommentar ).

Marcono1234
quelle
2

Wie wir alle wissen, wird es gelöscht. Unter bestimmten Umständen kann jedoch bekannt sein, dass der Typ in der Klassenhierarchie explizit erwähnt wird:

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

Und jetzt können Sie Dinge tun wie:

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

Mehr Infos hier . Aber auch hier ist es fast unmöglich, Folgendes abzurufen:

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

wo es gelöscht wird.

Jatin
quelle
Dies ist auch die von JAX-RS verwendete Problemumgehung, vgl. GenericEntityund GenericType.
Hein Blöd
1

Aufgrund der Tatsache, dass Klassenliterale keine allgemeinen Typinformationen enthalten, sollten Sie davon ausgehen, dass es unmöglich sein wird, alle Warnungen zu entfernen. In gewisser Weise entspricht die Verwendung Class<Something>der Verwendung einer Sammlung ohne Angabe des generischen Typs. Das Beste, was ich herausbringen konnte, war:

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
quelle
1

Sie können eine Hilfsmethode verwenden, um die @SuppressWarnings("unchecked")gesamte Klasse zu entfernen .

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

Dann könntest du schreiben

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

Andere Anwendungsbeispiele sind

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

Da es jedoch selten wirklich nützlich ist und die Verwendung der Methode die Typprüfung des Compilers zunichte macht, würde ich nicht empfehlen, es an einem Ort zu implementieren, an dem es öffentlich zugänglich ist.

Aventurin
quelle