Oggetti Class in Java
Ottieni oggetti Class<T> in Java con Object.getClass(), letterali .class e Class.forName.
Tutto ciò che riguarda la reflection parte da un oggetto Class. Per ogni tipo caricato dalla JVM — ogni classe, interfaccia, tipo array, enum, annotazione e persino ogni primitivo — esiste esattamente un'istanza di Class che lo descrive. Quell'oggetto è il tuo riferimento alla struttura del tipo: il suo nome, la sua superclasse, i suoi membri, le sue annotazioni. Questo capitolo tratta i tre modi per ottenere un Class, cosa contiene Class<T> e le piccole sorprese che colgono di sorpresa gli sviluppatori.
Tre modi per ottenere un Class
Esistono esattamente tre percorsi, ognuno adatto a una situazione diversa.
1. Il letterale .class — conosci il tipo in fase di compilazione.
Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class; // see "primitives" belowQuesto è sicuro in fase di compilazione e il più veloce — non c'è nessuna ricerca, il compilatore incorpora un riferimento direttamente. Usalo ogni volta che puoi nominare il tipo.
2. Object.getClass() — hai un'istanza.
Object o = "hello";
Class<?> c = o.getClass(); // class java.lang.StringgetClass() restituisce la classe a runtime dell'oggetto, che può essere una sottoclasse del tipo dichiarato della variabile. Object o = new ArrayList<>() fa sì che o.getClass() == ArrayList.class, non Object.class. Il suo tipo statico è Class<?> perché il compilatore sa solo che o è qualche Object.
3. Class.forName(String) — hai solo un nome.
Class<?> c = Class.forName("java.util.ArrayList");Questo è il percorso dinamico: un nome di classe completamente qualificato come stringa, risolto a runtime. Lancia ClassNotFoundException se nessuna classe con quel nome è caricabile. È ciò che usano i plugin loader e i driver JDBC. Esiste una variante caricata-ma-non-inizializzata: Class.forName(name, false, classLoader) salta gli inizializzatori statici finché la classe non viene usata attivamente per la prima volta.
Cosa contiene Class<T>
Un oggetto Class è una descrizione ricca. I metodi principali:
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();Da un Class si raggiunge ogni membro: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), più le loro controparti pubbliche/ereditate get… (la distinzione introdotta nel capitolo introduttivo). I capitoli successivi approfondiscono ciascuno.
Nome binario vs. nome semplice vs. nome canonico
I metodi di denominazione differiscono in modi che possono causare problemi quando si registrano o confrontano:
| Tipo | getName() | getSimpleName() | getCanonicalName() |
|---|---|---|---|
String | java.lang.String | String | java.lang.String |
int[] | [I | int[] | int[] |
String[] | [Ljava.lang.String; | String[] | java.lang.String[] |
nested Map.Entry | java.util.Map$Entry | Entry | java.util.Map.Entry |
| anonymous class | Outer$1 | "" (empty) | null |
getName() è il nome binario — la forma interna della JVM, con $ per l'annidamento e la codifica criptica [I / [L…; per gli array. È ciò che si aspetta Class.forName. getCanonicalName() è la forma sorgente che scriveresti, ed è null per i tipi che non puoi nominare nel sorgente (locali, classi anonime). Usa getName() per i round-trip con forName; usa getSimpleName()/getCanonicalName() per l'output leggibile.
Anche i primitivi e gli array hanno oggetti Class
Ogni primitivo ha il proprio Class, distinto dal suo wrapper:
int.class == Integer.class // false — two different Class objects
int.class.getName() // "int"
Integer.TYPE == int.class // true — TYPE is the primitive ClassAnche void ha void.class (e Void.TYPE). Le classi array vengono sintetizzate dalla JVM: int[].class, String[][].class. arrayClass.getComponentType() rimuove una dimensione (String[].class.getComponentType() è String.class). Queste distinzioni contano quando abbini i tipi di parametri in getMethod — getMethod("foo", int.class) e getMethod("foo", Integer.class) trovano overload diversi.
Identità della classe e class loader
L'identità di un oggetto Class non è solo il suo nome — è la coppia (nome, class loader definente). Lo stesso file .class caricato da due class loader diversi produce due oggetti Class distinti e incompatibili. Un cast tra di essi lancia ClassCastException anche se i nomi corrispondono. Questo è per lo più invisibile in un'applicazione normale (un solo loader) ma è alla radice di molti enigmi "ma è la stessa classe!" in application server, OSGi e sistemi di hot-reload. Per la reflection quotidiana, tratta gli oggetti Class come singleton per tipo e confrontali con ==.
Un esempio pratico: analizzare i tipi in tre modi
Il programma ottiene oggetti Class attraverso tutti e tre i percorsi, quindi interroga una manciata di tipi — una classe normale, un'interfaccia, un array, un primitivo e un tipo annidato — per evidenziare le differenze di denominazione e strutturali.
Cosa ricavare dall'esecuzione:
- Tutti e tre i percorsi hanno prodotto lo stesso tipo di oggetto: un letterale
.class, una chiamata agetClass()e una ricerca conforNamehanno ciascuno restituito unClasscompletamente utilizzabile. Il percorso che scegli dipende da cosa sai (il tipo, un'istanza o solo un nome) — il risultato è identico in termini di capacità. getClass()sulla variabileGreeter gha restituitoRobot, nonGreeter. Il tipo dichiarato è irrilevante;getClass()riporta sempre la classe concreta a runtime. Ecco perché il dispatch polimorfico e l'ispezione riflessiva vedono lo stesso tipo "reale".- I tre metodi di denominazione hanno divergito esattamente dove la tabella prevede:
String[]ha stampato il nome binario[Ljava.lang.String;dagetName()ma la forma leggibileString[]dalle forme semplice e canonica. Se mai vuoi reinserire un nome inforName, deve essere nella forma digetName(). int.class == Integer.classerafalsementreInteger.TYPE == int.classeratrue. Il primitivo e il suo wrapper sono oggettiClassdistinti, eInteger.TYPEè solo un alias per quello primitivo. Confonderli è la causa classica diNoSuchMethodExceptionquando si cerca un overload per tipo di parametro.Robot.class == new Robot().getClass()eratrue: all'interno di un singolo class loader, un tipo corrisponde esattamente a un oggettoClass, quindi==è il confronto corretto. Non hai mai bisogno di.equals()sugli oggettiClassnel codice con un singolo loader.
Errori comuni
forNameesegue gli inizializzatori statici (nella sua forma con un argomento). Il caricamento di una classe può avere effetti collaterali. Usa la forma con tre argomenti coninitialize=falsese vuoi solo ispezionare.getSimpleName()può essere vuoto (classi anonime) egetCanonicalName()può esserenull(locali, anonime). Non dare per scontato che siano sempre identificatori stampabili.- I generici vengono cancellati.
List<String>.classnon è valido; esiste soloList.class. UnClassnon porta informazioni sugli argomenti di tipo — queste si trovano inType/ParameterizedType, un'API di reflection separata (e più avanzata). Vedi restrizioni sui generici per capire come funziona l'erasure.
Con l'oggetto Class in mano, il capitolo successivo apre il primo cassetto dei membri: i campi — come ispezionarli, leggerli e scriverli, anche quando sono privati o final.