Java Class-Objekte
Class<T>-Objekte in Java erhalten mit Object.getClass(), .class-Literalen und Class.forName.
Alles in der Reflection beginnt mit einem Class-Objekt. Für jeden Typ, den die JVM lädt — jede Klasse, jedes Interface, jeden Array-Typ, jedes Enum, jede Annotation und sogar jeden Primitiven — gibt es genau eine Class-Instanz, die ihn beschreibt. Dieses Objekt ist Ihr Zugang zur Struktur des Typs: sein Name, seine Oberklasse, seine Member, seine Annotationen. Dieses Kapitel behandelt die drei Wege, ein Class-Objekt zu erhalten, was Class<T> enthält und die kleinen Überraschungen, über die man stolpern kann.
Drei Wege, ein Class-Objekt zu erhalten
Es gibt genau drei Routen, und jede passt zu einer anderen Situation.
1. Das .class-Literal — der Typ ist zur Kompilierzeit bekannt.
Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class; // see "primitives" belowDies ist typsicher zur Kompilierzeit und am schnellsten — es gibt keine Suche, der Compiler bakt eine Referenz ein. Verwenden Sie es, wann immer Sie den Typ benennen können.
2. Object.getClass() — Sie haben eine Instanz.
Object o = "hello";
Class<?> c = o.getClass(); // class java.lang.StringgetClass() gibt die Laufzeit-Klasse des Objekts zurück, die eine Unterklasse des deklarierten Variablentyps sein kann. Object o = new ArrayList<>() liefert o.getClass() == ArrayList.class, nicht Object.class. Sein statischer Typ ist Class<?>, weil der Compiler nur weiß, dass o irgendein Object ist.
3. Class.forName(String) — Sie haben nur einen Namen.
Class<?> c = Class.forName("java.util.ArrayList");Dies ist der dynamische Weg: ein vollqualifizierter Klassenname als String, der zur Laufzeit aufgelöst wird. Es wirft ClassNotFoundException, wenn keine solche Klasse geladen werden kann. Das ist, was Plugin-Loader und JDBC-Treiber verwenden. Eine geladene, aber nicht initialisierte Variante existiert: Class.forName(name, false, classLoader) überspringt statische Initialisierer, bis die Klasse zum ersten Mal aktiv verwendet wird.
Was Class<T> enthält
Ein Class-Objekt ist eine reichhaltige Beschreibung. Die wichtigsten Methoden:
Class<?> c = ArrayList.class;
c.getName(); // "java.util.ArrayList" (binary name)
c.getSimpleName(); // "ArrayList"
c.getCanonicalName(); // "java.util.ArrayList" (source-like)
c.getPackageName(); // "java.util"
c.getSuperclass(); // class java.util.AbstractList
c.getInterfaces(); // [List, RandomAccess, Cloneable, Serializable]
c.getModifiers(); // int bitset → Modifier.isPublic(...) etc.
c.isInterface(); // false
c.isEnum(); c.isArray(); c.isPrimitive(); c.isAnnotation();Von einem Class-Objekt aus erreichen Sie jedes Member: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), sowie ihre öffentlichen/geerbten get…-Gegenstücke (die Aufteilung aus dem Einführungskapitel). Die späteren Kapitel gehen auf jedes einzeln ein.
Binärname vs. einfacher Name vs. kanonischer Name
Die Benennungsmethoden unterscheiden sich auf eine Art, die beim Loggen oder Vergleichen Probleme bereitet:
| Typ | getName() | getSimpleName() | getCanonicalName() |
|---|---|---|---|
String | java.lang.String | String | java.lang.String |
int[] | [I | int[] | int[] |
String[] | [Ljava.lang.String; | String[] | java.lang.String[] |
verschachteltes Map.Entry | java.util.Map$Entry | Entry | java.util.Map.Entry |
| anonyme Klasse | Outer$1 | "" (leer) | null |
getName() ist der Binärname — die interne Form der JVM, mit $ für Verschachtelungen und der kryptischen [I / [L…;-Array-Kodierung. Das ist, was Class.forName erwartet. getCanonicalName() ist die Quellform, die man schreiben würde, und sie ist null für Typen, die man im Quellcode nicht benennen kann (lokale, anonyme Klassen). Verwenden Sie getName() für forName-Round-Trips; verwenden Sie getSimpleName()/getCanonicalName() für menschenlesbare Ausgaben.
Primitive und Arrays haben ebenfalls Class-Objekte
Jeder primitive Typ hat sein eigenes Class-Objekt, das sich von seinem Wrapper unterscheidet:
int.class == Integer.class // false — two different Class objects
int.class.getName() // "int"
Integer.TYPE == int.class // true — TYPE is the primitive Classvoid hat sogar void.class (und Void.TYPE). Array-Klassen werden von der JVM synthetisiert: int[].class, String[][].class. arrayClass.getComponentType() schält eine Dimension ab (String[].class.getComponentType() ist String.class). Diese Unterschiede sind wichtig, wenn Sie Parametertypen in getMethod abgleichen — getMethod("foo", int.class) und getMethod("foo", Integer.class) finden verschiedene Überladungen.
Class-Identität und Klassenloader
Die Identität eines Class-Objekts ist nicht nur sein Name — es ist das Paar (Name, definierender Klassenloader). Dieselbe .class-Datei, die von zwei verschiedenen Klassenloadern geladen wird, ergibt zwei verschiedene, inkompatible Class-Objekte. Ein Cast zwischen ihnen wirft ClassCastException, obwohl die Namen übereinstimmen. Das ist in einer einfachen Anwendung (ein Loader) meist unsichtbar, ist aber die Wurzel vieler "aber es ist doch dieselbe Klasse!"-Rätsel in App-Servern, OSGi und Hot-Reload-Systemen. Für alltägliche Reflection behandeln Sie Class-Objekte als Singletons pro Typ und vergleichen Sie sie mit ==.
Ein ausgearbeitetes Beispiel: Typen auf drei Arten untersuchen
Das Programm erhält Class-Objekte über alle drei Routen und fragt dann eine Handvoll Typen ab — eine reguläre Klasse, ein Interface, ein Array, einen Primitiven und einen verschachtelten Typ — um die Benennungs- und Strukturunterschiede aufzudecken.
Was man aus dem Lauf mitnehmen kann:
- Alle drei Routen lieferten dieselbe Art von Objekt: ein
.class-Literal, eingetClass()-Aufruf und eineforName-Suche erzeugten jeweils ein vollständig nutzbaresClass-Objekt. Die Route, die man wählt, hängt davon ab, was man weiß (den Typ, eine Instanz oder nur einen Namen) — das Ergebnis ist in seiner Fähigkeit identisch. getClass()auf derGreeter g-Variable gabRobotzurück, nichtGreeter. Der deklarierte Typ ist irrelevant;getClass()meldet immer die konkrete Laufzeitklasse. Deshalb sehen polymorphe Dispatch und reflektive Inspektion denselben "echten" Typ.- Die drei Benennungsmethoden wichen genau dort voneinander ab, wo die Tabelle es vorhersagt:
String[]druckte den Binärnamen[Ljava.lang.String;vongetName(), aber das lesbareString[]aus den einfachen und kanonischen Formen. Wenn Sie einen Namen je wieder inforNameeinspeisen, muss er diegetName()-Form haben. int.class == Integer.classwarfalse, währendInteger.TYPE == int.classtruewar. Der primitive und sein Wrapper sind verschiedeneClass-Objekte, undInteger.TYPEist nur ein Alias für das primitive. Sie zu verwechseln ist die klassische Ursache fürNoSuchMethodException, wenn man eine Überladung nach Parametertyp sucht.Robot.class == new Robot().getClass()wartrue: innerhalb eines Klassenloaders wird ein Typ genau einemClass-Objekt zugeordnet, daher ist==der korrekte Vergleich. Sie brauchen nie.equals()aufClass-Objekten in Single-Loader-Code.
Häufige Fallstricke
forNameführt statische Initialisierer aus (in seiner einargumentigen Form). Das Laden einer Klasse kann Nebeneffekte haben. Verwenden Sie die Drei-Argument-Form mitinitialize=false, wenn Sie nur inspizieren möchten.getSimpleName()kann leer sein (anonyme Klassen) undgetCanonicalName()kannnullsein (lokale, anonyme Klassen). Gehen Sie nicht davon aus, dass sie immer druckbare Bezeichner sind.- Generics werden gelöscht.
List<String>.classist illegal; es gibt nurList.class. EinClass-Objekt trägt keine Typargumentinformationen — diese leben inType/ParameterizedType, einer separaten (und fortgeschritteneren) Reflection-API. Siehe Einschränkungen bei Generics für den Grund, warum Erasure so funktioniert.
Mit dem Class-Objekt in der Hand öffnet das nächste Kapitel die erste Schublade der Member: Felder — sie inspizieren, lesen und schreiben, auch wenn sie privat oder final sind.