Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Genèrics

Aquesta llista compila sense cap queixa:

List llista = new ArrayList();
llista.add("hola");
llista.add(42);                        // ningú no s'hi oposa
String s = (String) llista.get(1);     // ClassCastException en execució

El compilador no hi pot fer res, perquè per a ell la llista conté Object. L’error només apareix quan el programa ja s’està executant, potser davant de l’usuari, i el cast que l’ha provocat pot ser lluny del add() que va introduir el problema.

Si diem de quin tipus són els elements, l’error es detecta en compilar i el cast desapareix:

List<String> llista = new ArrayList<>();
llista.add("hola");
llista.add(42);                        // error de compilació
String s = llista.get(0);              // sense cast

Els genèrics (generics) permeten que un tipus sigui un paràmetre. En declarar la variable decidim quin tipus real ocupa aquest paràmetre, i a partir d’aquí el compilador ho comprova tot per nosaltres. Aquests són els dos guanys: errors en temps de compilació en lloc de temps d’execució, i codi sense casts, que és més curt i més fàcil de llegir.

Classes genèriques

Una classe genèrica es declara amb un o més paràmetres de tipus entre angles, just després del nom:

public class Caixa<T> {
    private T contingut;
    public void desa(T contingut) {
        this.contingut = contingut;
    }
    public T recupera() {
        return contingut;
    }
}

T no és cap tipus concret: és un buit que s’omple en fer servir la classe. Cada ús decideix el seu:

Caixa<String> caixaText = new Caixa<>();
caixaText.desa("hola");
String text = caixaText.recupera();    // el compilador ja sap que és String

Caixa<Integer> caixaNum = new Caixa<>();
caixaNum.desa(42);

Hi ha una convenció de noms per als paràmetres de tipus que trobaràs a tota la biblioteca estàndard:

NomSignificat
EElement, a les col·leccions
K, VClau (key) i valor (value), als mapes
TUn tipus qualsevol
NUn número
S, USegon i tercer tipus, quan en calen més

El diamant

En construir l’objecte no cal repetir el tipus: amb els angles buits (<>, l’operador diamant) el compilador el dedueix de la declaració.

Map<String, List<Integer>> notes = new HashMap<>();   // millor que new HashMap<String, List<Integer>>()

Interfícies genèriques

Una interfície també pot tenir paràmetres de tipus, i qui la implementa tria si els manté oberts o els fixa.

interface Contenidor<T> {
    T getValor();
}

La implementació genèrica deixa el paràmetre obert, i qui la faci servir decidirà el tipus:

public class ContenidorSimple<T> implements Contenidor<T> {
    private final T valor;
    public ContenidorSimple(T valor) {
        this.valor = valor;
    }
    @Override
    public T getValor() {
        return valor;
    }
}

La implementació concreta el fixa, i ja no és negociable:

public class ContenidorLong implements Contenidor<Long> {
    private final Long valor;
    public ContenidorLong(Long valor) {
        this.valor = valor;
    }
    @Override
    public Long getValor() {
        return valor;
    }
}

Des de fora, totes dues s’usen igual, a través de la interfície:

Contenidor<String> c1 = new ContenidorSimple<>("test");
System.out.println(c1.getValor().toUpperCase());

Contenidor<Long> c2 = new ContenidorLong(12L);
System.out.println(c2.getValor() * 2);

Mètodes genèrics

Un mètode pot tenir el seu propi paràmetre de tipus, encara que la classe no en tingui. Es declara entre angles abans del tipus de retorn:

public static <E> void mostraArray(E[] elements) {
    for (E element : elements) {
        System.out.printf("%s ", element);
    }
    System.out.println();
}

En cridar-lo no cal indicar el tipus, perquè el compilador l’infereix dels arguments:

Integer[] nums = { 1, 2, 3 };
String[] noms = { "Aina", "Bernat" };
mostraArray(nums);    // E és Integer
mostraArray(noms);    // E és String

Paràmetres acotats

Dins d’un mètode genèric, el compilador no sap res de T més enllà que és un objecte. Per tant, això no compila:

public static <T> T maxim(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;   // error: Object no té compareTo()
}

Cal acotar el paràmetre (bounded type parameter) amb extends, dient quin mínim ha de complir el tipus:

public static <T extends Comparable<T>> T maxim(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

Ara el compilador sap que qualsevol TcompareTo(), i alhora rebutja les crides amb tipus que no siguin comparables. La cota serveix per a dues coses a la vegada: permet fer servir mètodes del tipus acotat i restringeix qui pot cridar el mètode.

Es pot exigir més d’una cosa alhora amb &, encara que és poc habitual:

<T extends Serializable & Comparable<T>>

Compte: aquí extends vol dir “és un subtipus de”, tant si es tracta d’una classe com d’una interfície.

Comodins

Aquest mètode sembla que hauria de servir per a qualsevol llista de números:

public static double suma(List<Number> llista) {
    double total = 0;
    for (Number n : llista) {
        total += n.doubleValue();
    }
    return total;
}

suma(new ArrayList<Integer>());   // error de compilació

No funciona, i la raó és important: encara que Integer sigui un subtipus de Number, List<Integer> no és un subtipus de List<Number>. Els genèrics són invariants. Té sentit, perquè si ho fossin podríem passar una List<Integer> a un mètode que hi afegís un Double.

La solució és el comodí (wildcard) ?, que representa un tipus desconegut:

public static double suma(List<? extends Number> llista) { ... }

Ara List<Integer>, List<Double> i List<Number> són totes acceptables. Hi ha tres formes:

FormaSignificatQuè hi pots fer
? extends TipusUn subtipus desconegut de TipusLlegir elements com a Tipus. No hi pots afegir res
? super TipusUn supertipus desconegut de TipusAfegir elements de tipus Tipus. En llegir només obtens Object
?Qualsevol tipusPoc més que recórrer i comptar

La restricció de ? extends sorprèn al principi. Si el paràmetre és una List<? extends Number>, el compilador no sap si per sota hi ha una List<Integer> o una List<Double>, i per tant no pot deixar-te afegir cap número concret sense arriscar-se a corrompre-la.

D’aquí surt la regla que es coneix com a PECS, producer extends, consumer super:

  • Si el paràmetre només produeix dades que llegiràs, fes servir ? extends.
  • Si el paràmetre només consumeix dades que hi posaràs, fes servir ? super.
  • Si fa les dues coses, no hi posis comodí.

Esborrat de tipus

Els genèrics només existeixen durant la compilació. Un cop comprovat que tot encaixa, el compilador els esborra (type erasure) i genera bytecode amb Object i els casts necessaris. En execució, una List<String> i una List<Integer> són la mateixa cosa: una List.

Això explica uns quants errors que d’entrada semblen arbitraris:

if (obj instanceof List<String>) { }   // error: en execució no es pot distingir
T[] array = new T[10];                 // error: no es pot instanciar un tipus esborrat

void processa(List<String> l) { }      // error: totes dues signatures
void processa(List<Integer> l) { }     // acaben sent processa(List)

També explica per què els genèrics no admeten tipus primitius: List<int> no és vàlid, perquè el que queda després de l’esborrat ha de ser un Object. Cal fer servir les classes embolcall (Integer, Double, Boolean), amb l’autoboxing convertint automàticament en els dos sentits.

Referències

Last change: , commit: 87dfa51