W3docs

Classi generiche in Java

Impara a scrivere classi generiche Java con parametri di tipo, parametri multipli, l'operatore diamante e un esempio di Stack tipizzato.

Una classe generica è una classe la cui dichiarazione contiene uno o più parametri di tipo — segnaposto che il chiamante specifica al momento della creazione di un'istanza. Lo stesso corpo della classe descrive così un'intera famiglia di tipi: Box<String>, Box<Integer>, Box<User> sono tutti tipi distinti a tempo di compilazione che condividono un'unica sorgente. Questa è la forma più comune che i generics assumono, ed è il modo in cui è scritto ogni elemento di raccolta in java.util, ogni Optional, ogni Future e ogni CompletableFuture.

La sintassi

La lista dei parametri di tipo si trova tra il nome della classe e il corpo, tra parentesi angolari:

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get()              { return value; }
  public void set(T value)    { this.value = value; }
}

Leggi la dichiarazione come "un Box parametrizzato su un tipo T." All'interno della classe, T si comporta come qualsiasi altro tipo — puoi dichiarare campi di tipo T, metodi che restituiscono T, parametri di tipo T. Il compilatore lo tratta come un tipo reale ma sconosciuto fino a quando il chiamante non ne sceglie uno.

Nel punto di chiamata, si fornisce il tipo effettivo:

Box<String>  greeting = new Box<>("hello");
Box<Integer> answer   = new Box<>(42);

String s = greeting.get();   // already a String — no cast
int i    = answer.get();     // auto-unboxed from Integer

Il <> a destra è l'operatore diamante — il compilatore deduce l'argomento di tipo dalla dichiarazione sul lato sinistro. Puoi scrivere new Box<String>("hello") esplicitamente, ma quasi non è mai necessario.

Parametri di tipo multipli

Una classe può dichiarare più di un parametro di tipo. L'esempio classico è una coppia chiave/valore:

public class Entry<K, V> {
  private final K key;
  private final V value;

  public Entry(K key, V value) {
    this.key   = key;
    this.value = value;
  }

  public K key()   { return key; }
  public V value() { return value; }
}

Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int    n    = score.value();

La convenzione prevede nomi a singola lettera — K per chiave, V per valore, E per elemento, R per ritorno, T per "tipo generico." Quando serve maggiore chiarezza (raro), sono consentiti nomi più lunghi: Map<KeyType, ValueType> è legale, semplicemente desueto.

Limitare il parametro di tipo

Per impostazione predefinita un parametro di tipo rappresenta "qualsiasi tipo," quindi all'interno della classe è possibile chiamare solo i metodi comuni a ogni object (equals, toString, hashCode). Se la tua classe ha bisogno di fare qualcosa con i valori — confrontarli, sommarli, leggere una proprietà — puoi vincolare T con un limite superiore usando extends:

// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
  private final T value;

  public NumberBox(T value) { this.value = value; }

  public double asDouble() { return value.doubleValue(); }
}

NumberBox<Integer> n = new NumberBox<>(42);   // fine — Integer is a Number
// NumberBox<String> bad = ...;               // ❌ String is not a Number

Qui extends significa "è un sottotipo di," e funziona sia per classi che per interfacce. Puoi anche richiedere più vincoli contemporaneamente — <T extends Number & Comparable<T>> — con il vincolo di classe (se presente) elencato per primo. Il vincolo è anche ciò che rende il tipo utilizzabile: senza extends Number, value.doubleValue() non compilerebbe.

Costruttori generici

Il parametro di tipo è fissato dall'istanza, quindi ogni costruttore di una classe generica ha già accesso a T:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) { this.first = first; this.second = second; }
  public Pair(T both)            { this(both, both); }
}

Anche i costruttori stessi possono essere generici su parametri di tipo aggiuntivi indipendenti da quelli della classe — ma è abbastanza raro da essere trattato nel prossimo capitolo sui metodi generici.

Le classi generiche possono estendere altre classi generiche

Una sottoclasse può ereditare da una classe generica in tre modi. Ognuno ha un significato diverso:

// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }

// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }

// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }

La forma intermedia è la più comune — si propaga il parametro del genitore ai propri chiamanti. La prima forma è quella che si usa quando la sottoclasse è specializzata: un albero di nodi di stringhe soltanto.

Campi e il parametro di tipo

Ogni istanza di Box<...> porta con sé il suo T. Il bytecode no — a runtime la JVM vede solo Box (questa è la type erasure, trattata più avanti in questa sezione). La conseguenza è che il parametro di tipo appartiene all'istanza, non all'oggetto classe:

Box<String>  a = new Box<>("hi");
Box<Integer> b = new Box<>(5);

a.getClass() == b.getClass();   // true — both are class Box

È un fatto utile da tenere a mente: Box<String> e Box<Integer> sono tipi diversi per il compilatore ma la stessa classe a runtime. Torneremo su questo in Java Type Erasure.

I membri statici non vedono il parametro di tipo

I campi statici e i metodi statici appartengono alla classe, non a una singola istanza — quindi non possono vedere il T dell'istanza. Questo non è valido:

public class Box<T> {
  private static T defaultValue;        // ❌ won't compile — no T at the static level
  public  static T empty() { ... }      // ❌ same problem
}

Un metodo statico che ha bisogno di un parametro di tipo deve dichiararne uno proprio, indipendente da quello della classe. Questo è l'argomento del prossimo capitolo.

Progettare il proprio: un piccolo stack tipizzato

Una classe completa e funzionante per mettere tutto insieme — uno Stack generico con push, pop, peek e size. È parametrizzato su E (elemento), supportato internamente da un Object[] (a causa delle restrizioni degli array generici), e il cast non verificato su pop è il tipo di soluzione ben contenuta che si vede nel codice reale.

java— editable, runs on the server

Le annotazioni @SuppressWarnings("unchecked") si trovano sulle due letture che devono eseguire il cast da Object a E. Questi cast sono sicuri — push memorizza solo valori di tipo E — ma il compilatore non può vederlo, perché la erasure ha rimosso E dal bytecode. Sopprimere l'avviso localmente, sull'ambito più piccolo possibile, è la mossa giusta.

Cosa c'è dopo

Hai visto il parametro a livello di classe. A volte hai bisogno di un singolo metodo generico, con il proprio parametro di tipo indipendente da quello della classe — utile per metodi di utilità, helper statici e qualsiasi operazione la cui relazione di tipo vive solo in quel singolo metodo. Continua con Java Generic Methods.

Esercitazione

Pratica
Scrivi `public class Box<T> { private static T value; }`. Il compilatore rifiuta il codice. Perché?
Scrivi `public class Box<T> { private static T value; }`. Il compilatore rifiuta il codice. Perché?
Was this page helpful?