Ruft den Typ eines generischen Parameters in Java mit Reflektion ab

143

Ist es möglich, den Typ eines generischen Parameters abzurufen?

Ein Beispiel:

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>());
    }
}
Cimnin
quelle

Antworten:

156

Ein Konstrukt, über das ich einmal gestolpert bin, sah aus wie

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

Es scheint also eine Reflexionsmagie zu geben, die ich leider nicht ganz verstehe ... Entschuldigung.

DerMike
quelle
2
Aber deklarieren Sie es dann als abstrakt. Wenn Sie es verwenden möchten, unterklassifizieren Sie es inline.
Snicolas
42
Obwohl dies sehr alt ist und aus irgendeinem Grund akzeptiert wird , habe ich abgelehnt, weil es die Frage einfach nicht beantwortet. In dem in der Frage angegebenen Beispiel würde "SpiderMan" nicht angezeigt. Es ist zweifellos in einigen Situationen nützlich, aber es funktioniert nicht für die gestellte Frage.
Jon Skeet
15
Niemand, der so weit gekommen ist, wird "Es kann nicht getan werden" als Antwort akzeptieren. Wir sind hier, weil wir verzweifelt sind.
Addison
14
Ich bekomme diese Ausnahme: Ich bin Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType mir nicht sicher, was die Einschränkung ist.
Dish
4
Wie @Dish bekomme ich gerade einenjava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
133

Ich möchte versuchen, die Antwort von @DerMike aufzuschlüsseln, um Folgendes zu erklären:

Erstens bedeutet das Löschen des Typs nicht, dass das JDK eliminiert Typinformationen zur Laufzeit. Es ist eine Methode, mit der die Typprüfung zur Kompilierungszeit und die Laufzeitkompatibilität in derselben Sprache koexistieren können. Wie dieser Codeblock impliziert, behält das JDK die gelöschten Typinformationen bei - es ist nur nicht mit überprüften Casts und Sachen verbunden.

Zweitens liefert dies generische Typinformationen für eine generische Klasse genau eine Ebene höher als die Hierarchie des zu überprüfenden konkreten Typs - dh eine abstrakte übergeordnete Klasse mit generischen Typparametern kann die konkreten Typen finden, die ihren Typparametern für eine konkrete Implementierung von sich selbst entsprechen das erbt direkt davon. Wenn diese Klasse nicht abstrakt und instanziiert wäre oder die konkrete Implementierung zwei Ebenen tiefer wäre, würde dies nicht funktionieren (obwohl ein wenig Jimmying dazu führen könnte, dass sie auf eine vorgegebene Anzahl von Ebenen über eine oder bis zur niedrigsten Klasse angewendet wird mit X generischen Typparametern usw.).

Wie auch immer, weiter zur Erklärung. Hier ist noch einmal der Code, der zur leichteren Bezugnahme in Zeilen unterteilt ist:

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

Sei 'wir' die abstrakte Klasse mit generischen Typen, die diesen Code enthält. Lesen Sie dies grob von innen nach außen:

  • Zeile 4 ruft die aktuelle Klasseninstanz der konkreten Klasse ab. Dies identifiziert den konkreten Typ unseres unmittelbaren Nachkommen.
  • Zeile 5 erhält den Supertyp dieser Klasse als Typ. Das sind wir. Da wir ein parametrischer Typ sind, können wir uns sicher in ParameterizedType (Zeile 3) umwandeln. Der Schlüssel ist, dass Java, wenn es dieses Type-Objekt bestimmt, die im Kind vorhandenen Typinformationen verwendet, um Typinformationen unseren Typparametern in der neuen ParameterizedType-Instanz zuzuordnen. Jetzt können wir auf konkrete Typen für unsere Generika zugreifen.
  • Zeile 6 ruft das Array von Typen ab, die unseren Generika zugeordnet sind, in der Reihenfolge, wie sie im Klassencode deklariert ist. In diesem Beispiel ziehen wir den ersten Parameter heraus. Dies kommt als Typ zurück.
  • Zeile 2 wirft den endgültigen Typ, der an eine Klasse zurückgegeben wurde. Dies ist sicher, da wir wissen, welche Typen unsere generischen Typparameter annehmen können, und bestätigen können, dass es sich bei allen um Klassen handelt (ich bin mir nicht sicher, wie man in Java einen generischen Parameter ohne Klasseninstanz abrufen würde damit verbunden).

... und das war's auch schon. Wir übertragen also Typinformationen aus unserer eigenen konkreten Implementierung zurück in uns selbst und verwenden sie, um auf ein Klassenhandle zuzugreifen. Wir könnten getGenericSuperclass () verdoppeln und zwei Ebenen durchlaufen oder getGenericSuperclass () eliminieren und Werte für uns als konkreten Typ erhalten (Einschränkung: Ich habe diese Szenarien nicht getestet, sie sind noch nicht für mich aufgetaucht).

Es wird schwierig, wenn Ihre konkreten Kinder eine beliebige Anzahl von Sprüngen entfernt sind oder wenn Sie konkret und nicht endgültig sind, und besonders schwierig, wenn Sie erwarten, dass eines Ihrer (unterschiedlich tiefen) Kinder seine eigenen Generika hat. Normalerweise können Sie diese Überlegungen jedoch berücksichtigen, sodass Sie den größten Teil des Weges zurücklegen können.

Hoffe das hat jemandem geholfen! Ich erkenne, dass dieser Beitrag uralt ist. Ich werde diese Erklärung wahrscheinlich abschneiden und für andere Fragen aufbewahren.

Mark McKenna
quelle
3
Beantworten Sie einfach Ihre Frage "Ich bin nicht sicher, wie man in Java einen generischen Parameter abrufen würde, dem eigentlich keine Klasseninstanz zugeordnet ist)": Dies ist möglich, wenn der Parameter für die generische Klasse entweder a ist. Platzhalterausdruck "(sagen wir :? erweitert SomeClass) oder eine" Typvariable "(sagen wir: <T> wobei T aus einer generischen Unterklasse stammt). In beiden Fällen können die zurückgegebenen "Type" -Instanzen nicht in "Class" umgewandelt werden, da jede VM möglicherweise für beide eine andere Implementierung hat, die die Type-Schnittstelle implementiert, jedoch nicht direkt von java.lang.Class abgeleitet ist.
Roberto Andrade
19

Eigentlich habe ich das zum Laufen gebracht. Betrachten Sie das folgende Snippet:

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

     }
 }

Ich benutze JDK 1.6

Andrey Rodionov
quelle
12
-1: Dies löst das Problem in der Frage nicht. Sie erhalten nur den in der Methodensignatur deklarierten Typ, nicht den tatsächlichen Laufzeittyp.
Michael Borgwardt
5
Könnte die Frage des OP nicht beantworten, aber es ist eine nützliche Technik.
dnault
3
Dies ist die Antwort auf eine ganz andere Frage.
Dawood ibn Kareem
Sie können m.getParameterTypes () aufrufen, um alle tatsächlichen Typen abzurufen. Für das Beispiel würde "Liste" zurückgegeben.
Lee Meador
Wenn genericParameterTypes [i] keine Instanz von ParameterizedType ist, kann es sich um eine Klasse handeln. Das bedeutet, dass ein bestimmtes Argument für die Methode überhaupt nicht parametrisiert ist.
Lee Meador
18

Es gibt tatsächlich eine Lösung, indem der Trick "anonyme Klasse" und die Ideen aus den Super Type Tokens angewendet werden :

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

Der Trick liegt in der Schaffung einer anonymen Klasse , new ArrayList<SpiderMan>() {}an der Stelle des ursprünglichen (einfach) new ArrayList<SpiderMan>(). Die Verwendung einer anoymischen Klasse (falls möglich) stellt sicher, dass der Compiler Informationen über das Typargument behält SpiderMan, das dem Typparameter gegeben wurde List<?>. Voilà!

Yann-Gaël Guéhéneuc
quelle
Dies beantwortet indirekt die Frage: Das ursprüngliche Problem hat keine Lösung. Damit der Typ seine generischen Parameter beibehält, muss er in einen anderen Typ eingebettet werden, z. B. eine Unterklasse. Dieses Beispiel zeigt, wie Sie den aufrufenden Code ändern müssen, damit der generische Typparameter in chill () wiederhergestellt werden kann.
Florian F
Zu Ihrer Information, der erste Link ist tot
Ashvin Sharma
@AshvinSharma Ich glaube, dass das gleiche Material hier verfügbar ist: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Yann-Gaël Guéhéneuc
7

Aufgrund der Typlöschung besteht die einzige Möglichkeit, den Typ der Liste zu ermitteln, darin, den Typ als Parameter an die Methode zu übergeben:

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

Anhang zur Antwort von @ DerMike zum Abrufen des generischen Parameters einer parametrisierten Schnittstelle (mithilfe der Methode #getGenericInterfaces () in einer Java-8-Standardmethode, um Doppelarbeit zu vermeiden):

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
quelle
Dies ist nicht das, wonach der ursprüngliche Beitrag gefragt hat. Hier haben Sie eine Unterklasse von erstellt Awesomeable<Beer>. In diesem Fall bleiben die Typinformationen erhalten. Wenn Sie new Awesomable<Beer> ()an die Methode übergeben, funktioniert wt nicht.
Florian F
@FlorianF Wenn Sie eine Awesomable<Beer>on the fly ohne die explizite Definition einer konkreten Unterklasse wie EstrellaGaliciain diesem Fall weitergeben, wird immer noch der parametrisierte Typ ausgegeben : Ich habe ihn gerade ausgeführt: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> Typ ist: ParameterizedStuff $ Beer
Campa
6

Nein, das ist nicht möglich. Aufgrund von Abwärtskompatibilitätsproblemen basieren die Generika von Java auf der Löschung von Typen , dh zur Laufzeit. Sie haben lediglich ein nicht generisches ListObjekt. Es gibt einige Informationen zu Typparametern zur Laufzeit, diese befinden sich jedoch in Klassendefinitionen (dh Sie können fragen, welchen generischen Typ die Definition dieses Felds verwendet? ), Nicht in Objektinstanzen.

Michael Borgwardt
quelle
Obwohl Java dies zur Laufzeit als Metadaten speichern könnte, nicht wahr? Ich hoffte es würde. Pech.
Cimnine
1
@ user99475: Nein. Ich habe recht und Andrey liegt falsch. Er bezieht sich auf die Typinformationen, die ich im zweiten Teil meiner Antwort erwähne, aber das ist nicht das, wonach die Frage fragt.
Michael Borgwardt
5

Wie von @bertolami hervorgehoben, ist es uns nicht möglich, einen Variablentyp zu erhalten und seinen zukünftigen Wert (den Inhalt der Variablen typeOfList) abzurufen.

Trotzdem können Sie die Klasse wie folgt als Parameter übergeben:

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

Dies ist mehr oder weniger das, was Google tut, wenn Sie eine Klassenvariable an den Konstruktor von ActivityInstrumentationTestCase2 übergeben müssen .

Snicolas
quelle
3

Nein, das ist nicht möglich.

Sie können einen generischen Feldtyp erhalten, wenn eine Klasse die einzige Ausnahme von dieser Regel darstellt, und selbst das ist ein kleiner Hack.

Ein Beispiel hierfür finden Sie unter Kennen des generischen Typs in Java .

Cletus
quelle
1

Sie können den Typ eines generischen Parameters mit Reflexion wie in diesem Beispiel erhalten, das ich hier gefunden habe :

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
quelle
Wie würden Sie dasselbe tun, um V of Home <K, V> zu erhalten?
Rafael Sanches
1

Die schnelle Antwort auf die Frage lautet: Nein, das können Sie nicht, da der generische Java-Typ gelöscht wird.

Die längere Antwort wäre, wenn Sie Ihre Liste wie folgt erstellt haben:

new ArrayList<SpideMan>(){}

In diesem Fall bleibt der generische Typ in der generischen Oberklasse der neuen anonymen Klasse oben erhalten.

Nicht, dass ich dies mit Listen empfehlen würde, aber es ist eine Listener-Implementierung:

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

Und da sich die Extrapolation der generischen Typen von Superklassen und Superschnittstellen zwischen JVMs ändert, ist die generische Lösung nicht so einfach, wie einige Antworten vermuten lassen.

Hier ist jetzt habe ich es getan.

TacB0sS
quelle
0

Dies ist unmöglich, da Generika in Java nur zur Kompilierungszeit berücksichtigt werden. Somit sind die Java-Generika nur eine Art Vorprozessor. Sie können jedoch die tatsächliche Klasse der Mitglieder der Liste abrufen.

Bertolami
quelle
Ja, das mache ich jetzt. Aber jetzt möchte ich den Typ wissen, auch wenn die Liste leer ist. Aber vier Jungs können sich nicht irren. Danke euch allen)!
Cimnine
19
Das ist nicht wahr. Obwohl es schwierig ist, können Sie im nächsten Beitrag sehen, dass ParameterizedType dies ermöglicht.
Edmondo1984
1
Stimmen Sie zu, es kann getan werden. Das Beispiel von DerMike skizziert es (hat bei mir funktioniert).
Mac
LOL bei "vier Jungs können sich nicht irren". Diese Antwort ist richtig.
Dawood ibn Kareem
0

Ich habe dies für Methoden codiert, die erwarten, dass sie akzeptiert oder zurückgegeben werden Iterable<?...>. Hier ist der 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
quelle
0

Sie können keinen generischen Parameter aus einer Variablen abrufen. Sie können jedoch aus einer Methode oder Felddeklaration:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Anton Pogonets
quelle
Dies gibt mir eine Ausnahme im Thread "main" java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
Lluis Martinez
'params' sind 'Type' mit mehreren Subschnittstellen. TypeVariableImpl wird für Methodenargumente verwendet und gibt den Namen des Generikums in den <> s an, nicht die Klasse, die es darstellt.
Lee Meador
0

Nur für mich war das Lesen dieses Codeausschnitts schwierig, ich habe es nur in zwei lesbare Zeilen unterteilt:

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

Ich wollte eine Instanz des Type-Parameters erstellen, ohne Parameter für meine Methode zu haben:

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

Hier ist ein weiterer Trick. Verwenden Sie ein generisches Vararg-Array

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

Mir ist aufgefallen, dass sich viele Menschen zur getGenericSuperclass()Lösung neigen:

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

Diese Lösung ist jedoch fehleranfällig. Es wird nicht richtig funktionieren, wenn die Nachkommen Generika enthalten. Bedenken Sie:

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

class Bar extends Foo<Double> {}

Welcher Typ wird Bar.persistentClasshaben? Class<Integer>? Nein, das wird es sein Class<Double>. Dies geschieht, weil getClass()immer die oberste Klasse zurückgegeben wird, was Barin diesem Fall der Fall ist, und die generische Superklasse ist Foo<Double>. Daher wird der Argumenttyp sein Double.

Wenn Sie eine zuverlässige Lösung benötigen, die nicht fehlschlägt, kann ich zwei vorschlagen.

  1. Verwenden Sie Guava. Es hat eine Klasse, die genau für diesen Zweck gemacht wurde : com.google.common.reflect.TypeToken. Es behandelt alle Eckkoffer einwandfrei und bietet einige weitere nette Funktionen. Der Nachteil ist eine zusätzliche Abhängigkeit. Wenn Sie diese Klasse verwendet haben, sieht Ihr Code einfach und klar aus:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Verwenden Sie die unten stehende benutzerdefinierte Methode. Es implementiert eine erheblich vereinfachte Logik ähnlich der oben erwähnten Guava-Klasse. Ich würde jedoch nicht garantieren, dass es fehleranfällig ist. Es löst jedoch das Problem mit den generischen Nachkommen.
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
quelle
-1

Verwenden:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
user4845560
quelle
Das ist falsch. toArray()kehrt zurück Object[]. Sie können und sollten sich nicht darauf verlassen, dass es sich um einen bestimmten Subtyp handelt.
Radiodef