Java – List, List<Object>, List<?> e List<? extends Object>

Quali sono le differenze tra List, List<Object>, List<?> e List<? extends Object> ? Se qualcuno vi ponesse questa domanda (ad esempio ad un colloquio di lavoro😉), sapreste rispondere? La questione riguarda chiaramente i generics, una funzionalità tra le più importanti introdotte in Java 5.

Per descrivere veramente bene queste differenze sarebbe necessaria una trattazione molto più ampia, ma si può dare una buona e valida spiegazione anche in uno spazio più ristretto (dando però per scontato che chi legge abbia già delle nozioni basilari sui generics).

Premesse

Innanzitutto in questo momento per List si intende la ben nota interfaccia java.util.List del framework standard, che da Java 5 è dichiarata come List<E> ed è quindi una interfaccia “generica”. Per fare questa discussione si potrebbe usare teoricamente anche un altro tipo “generico” (es. Set, Stack, ecc...). Generalmente si usa List perché il suo significato è ragionevolmente chiaro e ovvio: rappresenta una lista di oggetti che si possono aggiungere, modificare, estrarre e rimuovere in modo arbitrario.

Prima di vedere le differenze, è bene chiarire che tutta la discussione riguarda ciò che il compilatore consente di fare con una variabile/parametro di un tipo tra quelli elencati all’inizio. In modo particolare, l’aspetto più importante riguarda ciò che è lecito (oppure no) passare come argomento ad un metodo come ad esempio il add(E e) di List (e in generale altri metodi che hanno come parametro una type variable) in base alla parametrizzazione utilizzata nel tipo della variabile.

Tutto questo comunque, per chiarire meglio, non riguarda il comportamento a runtime di una collezione (o in generale di una classe generica). Una certa implementazione di una collezione potrebbe ad esempio rifiutare i valori null, oppure potrebbe essere non espandibile o essere del tutto “immutabile”. Ma questo è il comportamento “dinamico” della classe, che non c’entra con quello che il compilatore permette o non permette di fare su una variabile.

1) La forma List

Se da Java 5 in avanti si usa in un sorgente solamente List e non es. List<String>, List<Integer> (o altra parametrizzazione), si sta usando quello che si chiama il raw-type (traducibile in “tipo grezzo”). Un raw-type è un tipo “generico” utilizzato però senza alcuna parametrizzazione, praticamente come se fossimo ancora nell’epoca pre-Java 5 quando i generics non esistevano ancora.

Il concetto e l’utilizzo dei raw-type è stato introdotto per mantenere compatibilità all’indietro e per permettere una transizione più graduale verso i generics. È possibile ad esempio avere del codice non generico che usa una libreria che sfrutta i generics oppure al contrario un codice che sfrutta i generics che però usa una libreria non ancora aggiornata con i generics.

Usare un raw-type in un sorgente, pur essendo tuttora lecito, porta a dei warning emessi dal compilatore, che in pratica fanno un po’ da “bandierina” alzata per dire sostanzialmente allo sviluppatore: «guarda che stai usando un raw-type mentre quel tipo è “generico” e dovresti usarlo parametrizzato».

import java.util.*;

public class Prova1 {
    public static void main(String[] args) {
        List lista = new ArrayList();
        lista.add("abc");
        lista.add(123);

        String s = (String) lista.get(0);
        Integer i = (Integer) lista.get(1);
    }
}

Se si compila questo codice con javac -Xlint:unchecked Prova1.java si ottiene:

Prova1.java:6: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
        lista.add("abc");
                 ^
  where E is a type-variable:
    E extends Object declared in interface List
Prova1.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
        lista.add(123);
                 ^
  where E is a type-variable:
    E extends Object declared in interface List
2 warnings

A parte i warning (che si possono anche “sopprimere”, volendo), il List usato da Java 5 come raw-type permette di fare esattamente le stesse cose che si potevano fare con il List dell’epoca pre-Java 5, ovvero:

  • si possono inserire oggetti di qualunque tipo
  • si possono estrarre oggetti “vedendoli” solo come java.lang.Object (e se necessario, serve un cast esplicito ad un tipo più specifico)

2) La forma List<Object>

La forma List<Object> è a tutti gli effetti l’equivalente “parametrizzato” del List dell’epoca pre-Java 5. Il fatto che sia parametrizzato significa perlomeno che non ci sono warning nel suo utilizzo. Il seguente codice compila perfettamente senza errori e senza alcun warning:

import java.util.*;

public class Prova2 {
    public static void main(String[] args) {
        List<Object> lista = new ArrayList<Object>();     // Ora è tutto <Object> !
        lista.add("abc");
        lista.add(123);

        String s = (String) lista.get(0);
        Integer i = (Integer) lista.get(1);
    }
}

Un List<Object> permette di fare esattamente le stesse cose del List dell’epoca pre-Java 5, ovvero:

  • si possono inserire oggetti di qualunque tipo
  • si possono estrarre oggetti “vedendoli” solo come java.lang.Object (e se necessario, serve un cast esplicito ad un tipo più specifico)

3) La forma List<?>

La teoria dei generics contempla anche i wildcard e i bound che sono stati introdotti per fornire una sorta di “polimorfismo” (limitato) anche sulle parametrizzazioni dei tipi “generici”. Tutto questo è stato aggiunto per sopperire in parte al fatto che i generics di base sono “invarianti” ovvero, per fare un esempio “classico”, List<String> non è un sottotipo di List<Object>.

Un wildcard è rappresentato dal punto di domanda “?” e in questo caso del List<?> si dice che è un unbounded wildcard perché non c’è un limite esplicitato.

Riguardo l’uso di un List<?> c’è una cosa importantissima e fondamentale da fissare bene:

List<?> NON è una lista in cui si possono inserire oggetti di “qualunque” tipo.

Una lista in cui si possono inserire oggetti di “qualunque” tipo è List<Object>. Il senso di List<?> invece è ben differente:

List<?> è una lista di cui non si conosce a priori la parametrizzazione concreta.

Se ci fosse un metodo come il seguente:

public static void elabora(List<?> listaOggetti)

A questo metodo si può passare una qualunque implementazione di List<String> o List<Integer> o List<Date> o qualsiasi altra parametrizzazione concreta. Ma il metodo elabora “non sa” (e non ha modo di saperlo, neanche a runtime!) quale è questa parametrizzazione concreta.

Ora, se non sa quale è la parametrizzazione concreta ... quali sono quindi le conseguenze? Cosa può fare il metodo elabora sulla lista? La risposta è molto semplice: non può inserire nulla nella lista!

Non può inserire es. un String ... e se la parametrizzazione concreta fosse List<Integer>?
Non può inserire es. un Integer ... e se la parametrizzazione concreta fosse List<Date>?
ecc...

Facendo un paragone, è un po’ come se qualcuno mi dicesse che ha un “contenitore” ma senza dirmi che dimensioni ha. Posso pensare di mettere in quel contenitore una palla da tennis? O una valigia? O una automobile? Senza sapere le dimensioni ... no!

Quindi List<?> permette di sfruttare una sorta di polimorfismo (può ricevere qualunque parametrizzazione concreta) con la restrizione però di non poter inserire oggetti nella lista. Se il metodo elabora potesse realmente inserire oggetti di qualunque tipo nel List<?>, il chiamante che magari ha in mano un List<String> potrebbe poi trovarsi nella lista ad esempio un Integer o altro. E questo causerebbe ovviamente problemi più avanti, rendendo tutta la “buona” teoria dei generics di fatto inutile.

C’è solo una cosa che si può tecnicamente inserire in un List<?>: un null “letterale”. Il null è concettualmente il sottotipo di tutti i tipi reference. Quindi qualunque sia la parametrizzazione concreta, è sicuramente in grado di ricevere un null. Facendo ancora il paragone con la realtà, è come se il null fosse un oggetto infinitesimamente piccolo e quindi “ci sta” dentro qualunque contenitore, anche se non ne conosciamo bene la dimensione.

List<String> stringhe = new ArrayList<String>();
stringhe.add("aa");
stringhe.add("bb");

List<?> lista = stringhe;

lista.add("cc");    // NO ERRORE!
lista.add(123);     // NO ERRORE!

lista.add(null);    // Ok con un null “letterale”

Quindi a cosa serve un List<?> a livello pratico? Serve a fare in modo che una variabile o parametro possano ricevere una lista che ha una qualunque parametrizzazione, tipicamente con il solo scopo di estrarre poi gli oggetti “vedendoli” genericamente come Object. Se la variabile/parametro fosse List<Object>, potrebbe solo ricevere qualcosa parametrizzato come <Object> (es. ArrayList<Object>, LinkedList<Object>, ecc...) e non un’altra parametrizzazione es. ArrayList<String> o ArrayList<Date>.

In sostanza con un List<?> le possibilità sono le seguenti:

  • non si possono inserire oggetti
  • si può inserire, come caso particolare, solo un null letterale
  • si possono solo estrarre oggetti “vedendoli” in maniera generalizzata come java.lang.Object (e se necessario, serve un cast esplicito ad un tipo più specifico)

4) La forma List<? extends Object>

In questa forma c’è il wildcard ? e in più c’è anche il bound extends Object che serve a specificare in modo esplicito un limite “superiore”.

Questa forma è concettualmente similare al List<?> e a livello pratico basilare le due forme si possono ritenere equivalenti. La forma List<? extends Object> può infatti essere considerata, grosso modo, la forma “estesa” di List<?>. Entrambe le forme possono ricevere una lista con qualunque parametrizzazione e valgono esattamente le stesse questioni già dette prima per il List<?>, ovvero:

  • non si possono inserire oggetti
  • si può inserire, come caso particolare, solo un null letterale
  • si possono solo estrarre oggetti “vedendoli” in maniera generalizzata come java.lang.Object (e se necessario, serve un cast esplicito ad un tipo più specifico)

Quindi due variabili:

List<?> lista1;
List<? extends Object> lista2;

A questo livello basilare sono assolutamente equivalenti.

Attenzione però perché secondo le specifiche del linguaggio Java le due parametrizzazioni non sono perfettamente uguali. C’è infatti un aspetto molto particolare che riguarda anche gli array. Un array può essere creato solo di un tipo che in inglese si dice reifiable (“reificabile”). Un tipo reificabile è un tipo che è completamente rappresentabile a runtime.

Tutte le normali classi non generiche come String, Integer, Date, ecc.. sono completamente rappresentabili a runtime. I generics, ovvero i tipi “generici”, invece sono implementati per erasure e quindi ad esempio un ArrayList<String> non è completamente rappresentabile a runtime, poiché nell’oggetto non viene mantenuta alcuna informazione sul fatto che la lista è stata parametrizzata <String>.

Per scelta di design dei generics (che potrebbe sembrare strana e in controsenso) è stato stabilito che List<?> è un tipo reifiable mentre List<? extends Object> non lo è. Quindi è lecito creare un array di List<?> mentre invece è un errore di compilazione creare un array di List<? extends Object>.

Object o1 = new List<?>[10];   // Ok, lecito.
Object o2 = new List<? extends Object>[10];   // NO, ERRORE!
Object o3 = new List<String>[10];   // NO, ERRORE!

La seconda e la terza riga generano a livello di compilazione un error: generic array creation proprio perché List<? extends Object> e List<String> non sono tipi reifiable secondo le specifiche del linguaggio.

Conclusioni

Per concludere, un breve riassunto generalizzato delle quattro forme. Dato un certo tipo “generico” di nome Tipo<T>:

  • Usare solo Tipo da Java 5 è il raw-type (“tipo grezzo”), che è tuttora lecito ma causa dei warning nel suo utilizzo. Quindi se non ci sono altre questioni particolari di compatibilità con codice preesistente, sarebbe preferibile evitarlo.

  • Usare Tipo<Object> è l’equivalente “parametrizzato” del Tipo dichiarabile nell’epoca pre-Java 5. Si può usare ma se è possibile e sensato usare una parametrizzazione più specifica ed appropriata, es. Tipo<String>, sarebbe ovviamente preferibile.

  • Usare Tipo<?> permette di ricevere una qualunque parametrizzazione, a discapito però della possibilità di inserire oggetti (con l’unica eccezione del null letterale).

  • Usare Tipo<? extends Object> è a livello basilare equivalente a Tipo<?> e valgono sostanzialmente le stesse regole. I due tipi però non sono perfettamente uguali, in particolare per quanto riguarda l’utilizzo con gli array perché per scelta di design solo Tipo<?> è reifiable.