W3docs

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];   // ❌ — same

Questo è 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);                    // KABOOM

Il 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 come T[].
  • 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 flexibility

Vale 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, dangerous

Mescolare 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.

java— editable, runs on the server

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.

Esercitati

Pratica
Vuoi un metodo generico che restituisce `true` quando il suo argomento è un'istanza di `T`. Scrivi `<T> boolean isIt(Object o) { return o instanceof T; }`. Il compilatore lo rifiuta. Qual è la soluzione standard?
Vuoi un metodo generico che restituisce `true` quando il suo argomento è un'istanza di `T`. Scrivi `<T> boolean isIt(Object o) { return o instanceof T; }`. Il compilatore lo rifiuta. Qual è la soluzione standard?
Was this page helpful?