Generics in Java: Guida Completa con Esempi Pratici

Generics in Java

Generics in Java: guida completa per principianti e intermedi

I generics (o tipi generici) sono una delle caratteristiche più importanti del linguaggio Java moderno. Permettono di scrivere codice riutilizzabile, sicuro e più facile da mantenere, soprattutto quando si lavora con collezioni (liste, insiemi, mappe) e API complesse. Per approfondire ulteriormente, ti consiglio di consultare la documentazione ufficiale Oracle sui Generics, che offre spiegazioni dettagliate e esempi direttamente dalla fonte.

1. Cosa sono i generics?

I generics permettono di parametrizzare i tipi. Invece di lavorare con tipi generici come Object, puoi dichiarare classi, interfacce e metodi che funzionano con un tipo “segnaposto” (come T, E, K, V), che verrà poi sostituito da un tipo concreto (come String, Integer, MyClass) al momento dell’uso.

Esempio intuitivo: invece di avere una lista che contiene semplicemente Object (quindi qualsiasi cosa), puoi avere una List<String> che conterrà solo stringhe, o una List<Integer> che conterrà solo interi.

// Senza generics (vecchio stile, Java < 5)
List list = new ArrayList();
list.add("ciao");
list.add(123); // consentito, ma pericoloso

// Con generics
List<String> strings = new ArrayList<>();
strings.add("ciao");
// strings.add(123); // ERRORE di compilazione

2. Perché usare i generics?

I motivi principali per usare i generics sono:

  • Sicurezza di tipo a tempo di compilazione: molti errori vengono rilevati dal compilatore, invece che a runtime.
  • Meno cast espliciti: non hai bisogno di fare cast da Object al tipo reale.
  • Codice più leggibile: è chiaro quali tipi vengono usati e cosa ci si aspetta.
  • Riutilizzabilità: puoi scrivere una sola volta una classe o un metodo che funziona con diversi tipi.
// Senza generics
List list = new ArrayList();
list.add("ciao");
String s = (String) list.get(0); // serve il cast

// Con generics
List<String> list2 = new ArrayList<>();
list2.add("ciao");
String s2 = list2.get(0); // nessun cast necessario

3. Sintassi base dei generics

La sintassi base prevede l’uso delle parentesi angolari <> per specificare il tipo:

  • List<String>
  • Map<String, Integer>
  • Optional<Double>

Puoi avere uno o più parametri di tipo, separati da virgole. I nomi dei parametri di tipo sono convenzionalmente lettere maiuscole:

  • T – Tipo generico (Type)
  • E – Element (usato spesso nelle collezioni)
  • K, V – Key, Value (chiave e valore nelle mappe)
  • R – Return (tipo di ritorno)

4. Classi generiche in Java

Una classe generica è una classe che dichiara uno o più parametri di tipo nella definizione. Questo permette di riutilizzare la stessa struttura logica con tipi diversi.

// Classe Box che contiene un elemento di tipo T
public class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

// Uso della classe Box
public class Main {
    public static void main(String[] args) {
        Box<String> boxString = new Box<>("ciao");
        String s = boxString.getValue();

        Box<Integer> boxInt = new Box<>(42);
        Integer i = boxInt.getValue();
    }
}

La stessa classe Box può essere usata per qualsiasi tipo di dato, senza duplicare il codice.

4.1 Più parametri di tipo nei generics

Una classe può avere più parametri di tipo:

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

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

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

5. Metodi generici in Java

I metodi generici dichiarano i parametri di tipo tra il modificatore di accesso e il tipo di ritorno. Sono utili quando vuoi che un singolo metodo funzioni con diversi tipi, indipendentemente dal fatto che la classe che lo contiene sia generica o meno.

public class Utils {

    // Metodo generico che restituisce il primo elemento di una lista
    public static <T> T first(List<T> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    public static void main(String[] args) {
        List<String> strings = List.of("a", "b", "c");
        String firstString = Utils.first(strings);

        List<Integer> integers = List.of(1, 2, 3);
        Integer firstInt = Utils.first(integers);
    }
}

5.1 Metodi generici in classi generiche in Java

Una classe generica può avere sia il proprio parametro di tipo, sia parametri aggiuntivi nei singoli metodi.

public class Container<T> {
    private T value;

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

    public T getValue() { return value; }

    // Metodo generico indipendente da T
    public <U> void printTwo(U other) {
        System.out.println("Value: " + value);
        System.out.println("Other: " + other);
    }
}

6. Wildcards dei generics in Java: ?

La wildcard ? rappresenta un tipo sconosciuto. È utile quando vuoi scrivere metodi che lavorano con collezioni generiche, ma non ti interessa esattamente quale sia il tipo concreto, oppure vuoi supportare vari tipi correlati.

6.1 List<?>: tipo totalmente sconosciuto dei generics in Java

List<?> significa “lista di qualche tipo, non so quale”. Puoi leggere elementi solo come Object (o non leggerli affatto), e non puoi aggiungere nuovi elementi (eccetto null), perché il compilatore non può garantire la sicurezza del tipo.

public static void stampaTutti(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }

    // list.add("ciao"); // ERRORE: non posso aggiungere nulla
}

6.2 List<? extends T>: wildcard covariante in Java

? extends T significa “qualche tipo che è T o una sua sottoclasse“. È usata quando devi leggere valori come tipo T, ma non devi aggiungere nuovi elementi (a parte null).

public static void sommaNumeri(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) {
        sum += n.doubleValue();
    }
    System.out.println("Somma: " + sum);
}

// Può accettare: List<Integer>, List<Double>, List<Number>, ecc.

6.3 List<? super T>: wildcard controvariante in Java

? super T significa “qualche tipo che è T o una sua superclasse“. È usata quando vuoi scrivere valori di tipo T in una collezione, e non ti interessa il tipo esatto usato internamente.

public static void aggiungiInteri(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);

    // Per leggere, ottieni solo Object
    Object o = list.get(0);
}

// Può accettare: List<Integer>, List<Number>, List<Object>

Un modo mnemonico per ricordare la differenza: PECS (Producer Extends, Consumer Super). Se la collezione “produce” valori per te (la leggi), usa extends. Se la collezione “consuma” valori da te (ci scrivi dentro), usa super.

7. Bounded type parameters (limiti sui tipi) dei generics in Java

I bounded type parameters sono limiti che metti sui parametri di tipo di classi o metodi. Servono per dire che il tipo deve essere sottoclasse (o implementare) una certa classe o interfaccia.

7.1 Upper bound in Java: <T extends Classe>

<T extends Number> significa che T può essere Number o una sua sottoclasse (Integer, Double, ecc.). Questo ti permette di usare i metodi definiti in Number su valori di tipo T.

public class NumericBox<T extends Number> {
    private T value;

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

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

// Uso
NumericBox<Integer> b1 = new NumericBox<>(10);
NumericBox<Double> b2 = new NumericBox<>(3.14);
// NumericBox<String> b3 = new NumericBox<>("no"); // ERRORE di compilazione

7.2 Multiple bounds nei generics: <T extends Classe & Interfaccia1 & Interfaccia2>

Puoi imporre più vincoli a un parametro di tipo. In questo caso, il primo deve essere una classe (se presente) e gli altri devono essere interfacce.

public <T extends Number & Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

7.3 Lower bound nei wildcard: ? super T

7.3 Lower bound dei generics in Java: ? super T

8. Esempi pratici dei generics in Java

8.1 Libreria di utilità per collezioni con generics

Vediamo una piccola classe di utilità che usa generics, wildcard e bounded type parameters.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsUtil {

    // Crea una lista immutabile con un solo elemento
    public static <T> List<T> singletonList(T element) {
        List<T> list = new ArrayList<>();
        list.add(element);
        return Collections.unmodifiableList(list);
    }

    // Copia elementi da una lista sorgente a una lista destinazione
    // PECS: sorgente "produce" (extends), destinazione "consuma" (super)
    public static <T> void copy(List<? extends T> src, List<? super T> dest) {
        for (T item : src) {
            dest.add(item);
        }
    }

    // Stampa una lista qualunque
    public static void printList(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        List<Integer> ints = List.of(1, 2, 3);
        List<Number> nums = new ArrayList<>();

        copy(ints, nums); // OK: Integer estende Number
        printList(nums); // Stampa: 1, 2, 3
    }
}

8.2 Repository generico in Java

Immagina di voler scrivere un semplice “repository” in memoria per salvare oggetti di vari tipi (ad esempio User, Product, ecc.). Puoi usare i generics per riutilizzare la stessa logica.

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class InMemoryRepository<T> {
    private final List<T> data = new ArrayList<>();

    public void save(T entity) {
        data.add(entity);
    }

    public List<T> findAll() {
        return new ArrayList<>(data); // copia difensiva
    }

    public Optional<T> findFirst() {
        return data.isEmpty() ? Optional.empty() : Optional.of(data.get(0));
    }
}

class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
               "name='" + name + '\'' +
               '}';
    }
}

class Demo {
    public static void main(String[] args) {
        InMemoryRepository<User> userRepo = new InMemoryRepository<>();
        userRepo.save(new User("Mario"));
        userRepo.save(new User("Lucia"));

        System.out.println(userRepo.findAll());
        System.out.println(userRepo.findFirst());
    }
}

9. Limiti e particolarità dei generics in Java

I generics in Java sono implementati tramite un meccanismo chiamato type erasure (cancellazione dei tipi). Questo significa che le informazioni sui tipi generici vengono rimosse a runtime e servono principalmente a tempo di compilazione. Di conseguenza:

  • Non puoi istanziare direttamente un parametro di tipo: new T() non è consentito.
  • Non puoi creare array di tipi generici: new List<String>[10] è vietato.
  • Non puoi usare instanceof con i tipi generici specifici: if (obj instanceof List<String>) non è valido (puoi usare instanceof List).

10. Come studiare e fare pratica con i generics in Java

Per diventare davvero sicuro con i generics in Java, prova a consultare anche la guida Baeldung sui Generics, che fornisce approfondimenti pratici e casi d’uso reali. Inoltre:

  • Riscrivere piccole classi che usano Object in versione generica.
  • Analizzare il codice delle collezioni standard (List, Map, Optional, ecc.).
  • Scrivere metodi di utilità generici (filtri, trasformazioni, ecc.).
  • Esercitarti con wildcard extends/super e ricordare la regola PECS.

Con un po’ di pratica, i generics diventeranno uno strumento naturale per progettare API pulite e robuste, e ti aiuteranno a prevenire molti errori ancora prima di eseguire il tuo programma.

Commenti

Rispondi

Scopri di più da App Tutorial

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continua a leggere