Restrizioni sui Generics in Java
Cosa non si può fare con i generics in Java: niente primitivi, parametri di tipo statici, array generici e altro ancora.
Questo capitolo è il catalogo delle cose che non si possono fare con i generics in Java, con una breve spiegazione del perché ciascuna è vietata. Quasi ogni restrizione risale a un unico fatto visto nel capitolo precedente: il parametro di tipo viene cancellato prima che venga emesso il bytecode, quindi qualsiasi cosa che richiede che il parametro esista a runtime non funzionerà. Leggi questo capitolo come la scheda di riferimento finale per la parte — è l'elenco dei momenti "ho provato, il compilatore si è lamentato" trasformati in una singola pagina.
1. Nessun primitivo come argomento di tipo
List<int> ints = new ArrayList<>(); // ❌
List<Integer> ints = new ArrayList<>(); // ✓La forma cancellata a livello di bytecode di List<E> memorizza i suoi elementi come Object, e i primitivi non sono Object. La soluzione è la corrispondente classe wrapper — Integer, Long, Double, Boolean, Character, ecc. L'autoboxing colma la distanza: ints.add(5) e int x = ints.get(0) funzionano entrambi, con il costo che ogni elemento paga per un oggetto Integer nell'heap.
Project Valhalla è lo sforzo in corso per far funzionare davvero List<int>, tramite i tipi valore e i generics specializzati. A partire da Java 25 non è ancora disponibile.
2. Nessun new T()
public class Box<T> {
public T newInstance() { return new T(); } // ❌
}A runtime, non esiste T — la JVM ha solo Object (o il bound). Non ha un oggetto classe su cui chiamare un costruttore, né modo di sapere quale costruttore invocare. La soluzione standard è passare una factory come parametro:
public class Box<T> {
public T newInstance(Supplier<T> factory) { return factory.get(); }
}
Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);Il Supplier<T> porta la factory effettiva a runtime, in un modo che il parametro di tipo non potrebbe mai fare.
3. Nessun T.class o instanceof T
public <T> boolean isIt(Object o) {
return o instanceof T; // ❌
}
public <T> Class<T> klass() {
return T.class; // ❌
}Ancora una volta, nessun T a runtime. La soluzione in entrambi i casi è passare il token Class<T> come argomento:
public <T> boolean isIt(Object o, Class<T> type) {
return type.isInstance(o);
}Class.isInstance(Object) è la forma riflettiva di instanceof, e funziona con l'oggetto Class a runtime che hai passato. La libreria standard lo fa in molti punti — Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), i deserializzatori JSON, e così via.
4. Nessun array di un tipo generico
T[] arr = new T[10]; // ❌ — generic array creation
List<String>[] lists = new List<String>[10]; // ❌ — sameQuesto è più sottile. Gli array in Java sono reificati — un Integer[] sa a runtime che è un Integer[], e i salvataggi vengono controllati. Ma i generics sono cancellati — la JVM non riesce a distinguere List<String>[] da List<Integer>[]. Se entrambe le restrizioni non fossero applicate, potresti corrompere l'heap con poche righe:
List<String>[] strs = new List<String>[1]; // pretend this is legal
Object[] objs = strs; // arrays are covariant
objs[0] = List.of(42); // stores an Integer list
String s = strs[0].get(0); // KABOOMIl compilatore rifiuta la creazione di array generici piuttosto che lasciar accadere questo.
Soluzioni alternative:
- Usa
(T[]) new Object[n]con un@SuppressWarnings("unchecked")(lo hai visto nel generic Stack precedente). Sicuro se l'array è interno e non lo lasci mai trapelare comeT[]. - Oppure usa semplicemente un
List<T>al posto di un array. In nove casi su dieci questa è la risposta giusta.
5. Nessun campo statico di un parametro di tipo
public class Box<T> {
private static T defaultValue; // ❌
public static T empty() { ... } // ❌
}Il parametro di tipo appartiene a un'istanza — ogni Box<...> porta il proprio T. I membri statici appartengono alla classe stessa, che non ha un T. I due ambiti non si connettono.
Se hai bisogno di un metodo statico che sia polimorfico in un tipo, dichiara il proprio parametro di tipo (lo abbiamo trattato nei metodi generici):
public class Box<T> {
public static <U> Box<U> empty() { return new Box<>(null); }
}<U> è locale al metodo — indipendente da qualsiasi T a livello di classe.
6. Nessun tipo eccezione generico
public class MyException<T> extends Exception { ... } // ❌Le tabelle di gestione delle eccezioni della JVM cercano i blocchi catch per classe cancellata. Se due diversi tipi di eccezione generici si cancellassero entrambi nella stessa classe, un catch (MyException<String> e) catturerebbe anche un MyException<Integer> — il che corromperebbe silenziosamente il sistema dei tipi. Piuttosto che cercare di far funzionare questo, Java vieta del tutto la dichiarazione. Non puoi nemmeno avere un parametro di tipo generico in una clausola catch:
try { ... } catch (T e) { ... } // ❌Se la tua eccezione ha davvero bisogno di portare un payload tipizzato, memorizza il payload come campo generico su un'eccezione non generica:
public class TaggedException extends Exception {
public final Object payload;
public TaggedException(String message, Object payload) {
super(message);
this.payload = payload;
}
}Oppure dichiara il sito del lancio in modo ristretto e riserva i payload tipizzati per i percorsi di ritorno normali.
7. Nessun overload che differisce solo per i parametri generici
public void process(List<String> list) { ... }
public void process(List<Integer> list) { ... } // ❌ — both erase to process(List)Dopo la cancellazione, entrambi i metodi hanno firma process(List). Java gestisce la risoluzione degli overload tramite firme cancellate, quindi non riesce a distinguerli. La soluzione è dare loro nomi diversi — processStrings e processInts — oppure accettare una List<Object> e verificare a runtime.
8. I tipi generici sono invarianti
Questa non è una regola del tipo "il compilatore rifiuta questo", ma "il compilatore rifiuta ciò che ti aspettavi fosse legale", e l'abbiamo trattata in dettaglio in Wildcards:
List<Integer> ints = ...;
List<Number> nums = ints; // ❌ — generic types are invariant
List<? extends Number> nums = ints; // ✓ — wildcard restores the flexibilityVale la pena saperlo perché è la restrizione in cui ci si imbatte più spesso. I wildcard sono la valvola di sfogo.
9. Nessun tipo enum generico
public enum Box<T> { // ❌
EMPTY, FULL;
T value;
}Gli enum vengono tradotti in una singola classe con un insieme fisso di costanti — non c'è modo per le costanti di condividere un singolo T sensato. La soluzione di solito è rendere i metodi generici, non l'enum stesso:
public enum Box {
EMPTY, FULL;
public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}Oppure, se ogni costante vuole davvero il proprio tipo, usa una gerarchia di classi non-enum e una Map<Name, Box>.
10. Chiamare un metodo generico tramite un tipo raw
List rawList = new ArrayList();
rawList.add("hi"); // unchecked-warning, but allowed
List<String> typed = rawList; // unchecked-warning, dangerousMescolare tipi raw e generici disabilita tutti i controlli a compile-time dei generics per quella variabile. Il compilatore avvertirà (Unchecked call to add(E) as a member of raw type java.util.List), e ignorare l'avvertimento ti riporta alla pistola puntata ai piedi di pre-Java-5 — valori di tipo sbagliato introdotti silenziosamente, che esplodono alla prossima lettura.
I tipi raw esistono per compatibilità con le versioni precedenti, non come funzionalità. Tratta l'avvertimento come un errore in qualsiasi nuovo codice.
Un esempio pratico: ogni restrizione, fianco a fianco
Il programma seguente tenta di fare ciascuna delle cose che le regole vietano (commentate in modo che il file compili), poi mostra la soluzione canonica per ognuna. Leggi i commenti — si mappano uno a uno sulle restrizioni numerate sopra.
Ogni restrizione numerata corrisponde a una soluzione di una riga nel programma. Il pattern comune a tutte è lo stesso: qualsiasi cosa che vuole il parametro di tipo a runtime riceve le informazioni esplicitamente — un token Class<T>, un Supplier<T>, un parametro di tipo a livello di metodo, un wildcard. La cancellazione ha rimosso la forma implicita; tu la reintroduci al confine dell'API.
Chiudiamo la Parte 10
I generics sono la funzionalità singola più profonda del linguaggio al di fuori della JVM stessa. Ora hai il vocabolario operativo: parametri di tipo su classi, metodi e interfacce; bound; wildcard e PECS; erasure; e il catalogo delle restrizioni che l'erasure impone alla progettazione. Ogni moderna API Java è plasmata da queste regole, e leggere il codice della libreria (o progettare il proprio) è molto più facile con questo modello in testa.
Cosa viene dopo
I generics non sono un fine in sé — esistono perché Java aveva bisogno di un modo per esprimere "contenitore di T" senza copiare e incollare una classe per ogni tipo di elemento. La prossima parte del libro è il luogo verso cui ogni pezzo di macchinario generico in questa parte ti stava segretamente preparando: il Collections Framework. List, Set, Map, Queue, e le dozzine di implementazioni dietro di loro — tutte parametrizzate, tutte progettate attorno alle regole che hai appena imparato. Continua con Java Collections intro.