Blog

Ein allgemeines Dienstprogramm zum Abrufen von Java-Werten eines generischen Typs

Andrew Phillips

Aktualisiert Oktober 23, 2025
6 Minuten

In einem kürzlich erschienenen Beitrag hat Arjan Blokzijl erörtert, wie Class.getGenericSuperclass verwendet werden kann, um auf generische Typinformationen zuzugreifen. Das kann sehr nützlich sein, aber die Implementierung einer allgemeinen Methode zu diesem Zweck ist nicht so einfach, wie es vielleicht scheint. In diesem Beitrag werden wir einige der Probleme umreißen und sie hoffentlich lösen, indem wir eine Utility-Klasse erstellen. Denn mit etwas Glück ist dies die Art von Code, die nur einmal geschrieben werden muss! Für die Neugierigen oder Ungeduldigen, hier ist er. Bevor wir in die Details eintauchen, sei darauf hingewiesen, dass dieses Dienstprogramm nur auf generische Typinformationen zugreifen kann, die in der Klassenhierarchie zur Kompilierzeit verfügbar sind; zum Beispiel wird der Agent in [java] class SecretAgentVehicle extends Vehicle<Agent> [/java] Die Java-Implementierung von Generika über Typ-Löschung bedeutet, dass es nicht möglich ist, auf Laufzeit-Typ-Informationen zuzugreifen - es gibt keine Möglichkeit keine Möglichkeit um den Agent von [java] Liste<Agent> list = new ArrayList<Agent>(); [/java] da es weder in der Schnittstelle List noch in der Oberklasse AbstractList von ArrayList angegeben ist. . Ich würde mich übrigens gerne irren - wenn Sie einen cleveren Weg kennen, um auf diese Informationen zuzugreifen, lassen Sie es mich bitte wissen! Wie Arjan gezeigt hat, sind die Informationen über generische Typen über ParameterizedType.getActualTypeArguments, wobei der ParameterizedType von Class.getGenericSuperclass oder Class.getGenericInterfaces zurückgegeben wird (Sie benötigen einen Cast von Type). Dies funktioniert jedoch nur, wenn die übergeordnete Klasse eine generische Klasse ist (bzw. wenn die Klasse eine generische Schnittstelle implementiert). Wenn die übergeordnete Klasse eine "normale" Klasse ist, ist der zurückgegebene Type einfach das Ergebnis von Class.getSuperclass (bzw. Class.getInterfaces) und Sie erhalten eine ClassCastException, wenn Sie versuchen, auf ParameterizedType zu casten. Autsch! Was ist also in einer Situation wie dieser zu tun? [java] class DoubleOhVehicle extends SecretAgentVehicle [/java] in der wir vermutlich noch wissen möchten, dass DoubleOhVehicle den Typparameter Agent hat? Nun, Sie müssen einfach in der Klassenhierarchie nach oben klettern, bis Sie die gewünschte Klasse erreichen, und eine Schleife wäre eine Möglichkeit, dies zu tun (eine rekursive Implementierung ist ebenfalls eine gute Lösung). Beachten Sie, dass wir davon ausgehen, dass die Eingabeklasse tatsächlich eine Unterklasse von SecretAgentVehicle ist, was z.B. mit gebundenen Typparametern erzwungen werden kann. [java] assert SecretAgentVehicle.class.isAssignableFrom(clazz); Type supertype = clazz; do { supertype = supertype.getGenericSuperclass(); } while (!supertype.equals(Fahrzeug.class); Type[] actualTypeArguments = ((ParameterizedType) supertype).getActualTypeArguments(); [/java] Oder besser gesagt, es wäre schön, wenn Sie so etwas schreiben könnten...aber es wird nicht funktionieren. Leider verfügt Type nicht über eine getGenericSuperclass-Methode - für die Traversierung der Hierarchie müssen explizit Klasseninstanzen verwendet werden. [java] Class<? extends SecretAgentVehicle> clazz; while (!clazz.equals(SecretAgentVehicle.class)) { clazz = clazz.getSuperclass(); } Type[] actualTypeArguments = ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments(); [/java] Voilà! Wir können jetzt mit Unterklassen umgehen. Was könnte es sonst noch zu tun geben? Nun, betrachten Sie die folgende Klassenhierarchie: [java] Klasse Aktivität<U, V> class SecretAgentActivity<S> extends Activity<Agent, S> class SecretAgentMission extends SecretAgentActivity<Mission> class BondMission extends SecretAgentMission [/java] Die (geordneten!) Typ-Parameter von BondMission sind natürlich Agent und Mission, aber [java] ((ParameterizedType) SecretAgentActivity.class.getGenericSuperclass()).getActualTypeArguments(); [/java] liefert nur [Agent, S]. So ärgerlich das auch sein mag, es ist in der Tat alles, was man erwarten kann: SecretAgentActivity.class hat keine Möglichkeit zu wissen, dass der zweite Parameter Mission ist, da dieser nur in einer Unterklasse definiert ist. Beachten Sie auch, dass die "unstatierte" Variable als S und nicht als V zurückgegeben wird, d.h. unter Verwendung des Typparameters aus der Unterklasse. Um mit dieser Situation umzugehen, müssen wir also nicht nur die Hierarchie nach oben durchlaufen (wobei wir bedenken müssen, dass nicht alle Klassen in der Hierarchie notwendigerweise generisch sind), sondern auch die Zuordnungen von Typparametern zu Instanzen im Auge behalten. Wie funktioniert das? In unserem Beispiel können wir die Typ-Parameter für SecretAgentActivity.class mit den tatsächlichen Typ-Argumenten vergleichen und feststellen, dass der Parameter S dem Typ Mission zugeordnet ist. Wenn wir dann die tatsächlichen Parameter [Agent, S] von Activity.class untersuchen, können wir diese Zuordnung verwenden, um S in Mission"aufzulösen" und so das gewünschte Ergebnis zu erhalten. Da unser Ziel darin besteht, eine universell einsetzbare Methode zu schreiben, gibt es eine weitere Frage, die wir nicht angesprochen haben. Das Durchlaufen der Klassenhierarchie von einer bestimmten Klasse nach oben ist gut, aber... wann soll man aufhören? Im obigen Beispiel ist die Frage "Was ist der Typparameter für BondMission als SecretAgentActivity (Antwort: [Mission])?" kann genauso gültig sein wie "Was sind die Parameter für BondMission als Aktivität ([Agent, Mission] )?" Aus diesem Grund akzeptiert die Methode zwei Argumente: die Klasse, deren Typargumente erforderlich sind, und die "Kontext"-Klasse, von der sie als Unterklasse betrachtet werden soll. Was ist, wenn die Zielklasse und die Oberklasse "Kontext" identisch sind, oder wenn jemand nach den Typen von SecretAgentActivity.class als Unterklasse von Activity fragt? ? Nun, wir wissen, dass nicht alle Typvariablen verfügbar sein werden, aber wie sollte der Code darauf reagieren? In diesem Fall wird für "unauflösbare" Typparameter null zurückgegeben ( SecretAgentActivity hätte also als Activity-Unterklasse die Typen [Agent, null] ). Dies scheint eine vernünftige Reaktion zu sein (und ist zudem einfach zu implementieren), könnte aber leicht geändert werden, falls gewünscht.

Bis zu diesem Punkt haben wir uns nur auf Klassenhierarchien konzentriert. Aber bei so vielen generischen Schnittstellen wäre unser Dienstprogramm viel nützlicher, wenn die Unterklasse oder die Kontext-Oberklasse oder beide Schnittstellen sein könnten. Zum Beispiel für eine Gruppe von Klassen [java] interface AgentAttributes<V> extends Map<Agent, V> interface AgentCodenames extends AgentAttributes<String> class DigitCodenames implementiert AgentCodenames [/java] würden wir auch gerne beantworten können

  • getActualTypeArguments(DigitCodeames.class, AgentAttributes.class) mit [String] und
  • getActualTypeArguments(AgentCodeames.class, Map.class) mit [Agent, String].

Im Prinzip sollte dies nur geringfügige Änderungen erfordern. Eine davon ist, dass die Informationen zum Schnittstellentyp über Class.getGenericInterfaces verfügbar sind, nicht über Class.getGenericSuperclass, und wir müssen die betreffende Schnittstelle aus dem zurückgegebenen Array von Schnittstellen (von denen nicht alle notwendigerweise generisch sind!) extrahieren.Es gibt jedoch noch einen grundlegenderen Unterschied: Der Vererbungspfad von einer Klasse oder Schnittstelle zu einer Superschnittstelle ist nicht unbedingt eindeutig! Dies ist durchaus akzeptabel [java] interface Person interface Assassin extends Person interface Ladykiller extends Person interface Agent extends Assassin, Ladykiller [/java] wobei sowohl [Agent, Assassin, Person] als auch [Agent, Ladykiller, Person] Pfade von Agent zu Person sind. Was soll man wählen? Nun, zum Glück spielt das in diesem Fall keine Rolle, denn der Compiler verbietet widersprüchliche (typisierte) Vererbungspfade, wie z.B. [java] interface Registry<T> extends Set<T> interface CodenameRegistry extends Registry<String> interface DoubleOhRegistry extends Registry<Byte> interface Mi6Registry extends CodenameRegistry, DoubleOhRegistry [/java] Daher können wir einfach einen der Pfade auswählen, indem wir ClassUtils.getSuperclassChain verwenden. Noch einmal der Link zum Code.

Verfasst von

Andrew Phillips

Contact

Let’s discuss how we can support your journey.