15  Col·leccions i genericitat

Objectius

  • Entendre que són les col·leccions de Java.
  • Comprendre què és la genericitat (<T>) i per què fa el codi més segur.
  • Distingir les tres grans famílies de col·leccions: List, Set i Map.
  • Triar entre ArrayList i LinkedList segons l’ús.
  • Recórrer col·leccions amb el for-each i amb iteradors.
  • Ordenar objectes amb Comparable (ordre natural) i Comparator (ordres a mida).

Ja hem vist els arrays: contenidors de mida fixa. Són útils però tenen tenen l’inconvenient que has de decidir la mida per endavant i un cop creats, no creixen.

Si tens un array de 10 posicions i arriba l’element número 11, tenim una situació que cal gestionar.

Les col·leccions de Java resolen això: són contenidors que creixen i decreixen sols, amb mètodes per afegir, esborrar, cercar i ordenar.

15.1 El mapa de les col·leccions

Totes les col·leccions comparteixen una arrel comuna, la interfície Collection, amb un contracte mínim (add, remove, size, contains…).

Els mapes (Map) van per lliure: no són Collection, perquè guarden parelles clau-valor.

classDiagram
    class Iterable~T~ {
        <<interface>>
        +iterator() Iterator~T~
    }
    class Collection~T~ {
        <<interface>>
        +add(e) boolean
        +remove(o) boolean
        +size() int
        +contains(o) boolean
    }
    class List~T~ {
        <<interface>>
        +get(i) T
        +set(i, e) T
    }
    class Set~T~ {
        <<interface>>
    }
    class Map~K,V~ {
        <<interface>>
        +put(k, v) V
        +get(k) V
    }
    Iterable <|-- Collection
    Collection <|-- List
    Collection <|-- Set
    List <|.. ArrayList
    List <|.. LinkedList
    Set <|.. HashSet
    Set <|.. TreeSet
    Map <|.. HashMap
    Map <|.. TreeMap

Quan escrius codi, la bona pràctica és declarar per la interfície i instanciar per la classe:

List<String> tasques = new ArrayList<>();   // Declaro List, instancio ArrayList

Així, si un dia canvies ArrayList per LinkedList, només toques una línia: la resta del codi parla amb List i ni se n’assabenta.

15.2 Genericitat

La genericitat permet dir a una classe quin tipus d’objectes guardarà.

Per exemple, List<Llibre> vol dir “una llista de llibres”. Aquesta class <Llibre> és el paràmetre de tipus.

Sense genericitat, una col·lecció guardaria Object (qualsevol cosa) i, en treure elements, hauries de fer càsting i esperar que fos del tipus esperat. Amb genericitat, el compilador comprova els tipus per tu: si intentes ficar un Soci dins d’una List<Llibre>, no compila.

Exemple:

// Sense genericitat (estil antic, EVITAR): cal càsting i és insegur
List llistaAntiga = new ArrayList();
llistaAntiga.add("Hola");
String s = (String) llistaAntiga.get(0);            // Càsting obligatori

// Amb genericitat: segura i sense càsting
List<String> noms = new ArrayList<>();              // <> = "diamant", infereix el tipus
noms.add("Marta");
String primer = noms.get(0);                        // Ja és String, sense càsting

Fixa’t en el diamant <> a la dreta: des de Java 7 no cal repetir el tipus, el compilador l’endevina a partir de l’esquerra.

AlertaError típic: barrejar tipus
List<Integer> edats = new ArrayList<>();
edats.add(25);
edats.add("trenta");   // ERROR de compilació: "trenta" no és Integer

El compilador et frena abans d’executar, és a dir, es detecta fàcilment i és ràpid de corregir.

NotaAutoboxing: tipus primitius dins de col·leccions

Les col·leccions només guarden objectes, no primitius. No pots fer List<int>. Has d’usar les classes embolcall: List<Integer>, List<Double>, List<Boolean>.

Java converteix int en Integer automàticament (autoboxing). A la pràctica escrius edats.add(25) amb naturalitat.

ConsellMini-repte

Declara una llista d’objectes Llibre i afegeix-hi dos llibres. Quin missatge dona el compilador si hi afegeixes un String?

15.3 List

Una List és una seqüència amb índex: cada element té una posició (0, 1, 2…), l’ordre d’inserció es manté i s’admeten duplicats.

És la col·lecció més semblant a un array però dinàmica.

Exemple:

List<String> compra = new ArrayList<>();
compra.add("Pa");
compra.add("Llet");
compra.add("Pa");                           // Duplicat permès
System.out.println(compra);                 // [Pa, Llet, Pa]
System.out.println(compra.get(1));          // Llet  (índex 1)
compra.set(1, "Iogurt");                    // Substitueix la posició 1
compra.remove("Pa");                        // Esborra la PRIMERA aparició de "pa"
System.out.println(compra.size());          // 2

ArrayList vs LinkedList

Les dues són una List, però per dins són molt diferents.

ArrayList guarda els elements en un array intern; LinkedList els encadena com una llista d’anells (cada node apunta al següent).

Operació ArrayList LinkedList
Accés per índex get(i) Ràpid (directe) Lent (cal recórrer)
Afegir/treure al final Ràpid Ràpid
Afegir/treure al principi o al mig Lent (desplaça elements) Ràpid (recol·loca enllaços)
Memòria Compacta Més (guarda enllaços)
Ús habitual El 90 % dels casos Cues/piles amb molta inserció als extrems
ConsellRegla pràctica

Comença sempre amb ArrayList. Només canvia a LinkedList si has mesurat que insereixes o esborres molt pels extrems i que això és el teu coll d’ampolla.

En cas de dubte, utilitza: ArrayList.

AlertaError típic: modificar mentre recorres
for (String x : compra) {
    if (x.equals("pa")) compra.remove(x);       // ConcurrentModificationException!
}

No pots afegir ni treure d’una col·lecció mentre la recorres amb un for-each.

La solució correcta és l’ús d’iteradors, que veurem més endavant.

ConsellMini-repte

Crea una List<Integer> amb les notes 5, 8, 3, 8. Mostra quantes n’hi ha i quantes vegades apareix el 8 amb Collections.frequency(notes, 8).

15.4 Set

Un Set és un conjunt matemàtic: no admet duplicats. Si afegeixes un element que ja hi és, no passa res (no es duplica). N’hi ha dues variants molt usades:

  • HashSet: rapidíssim, però sense ordre garantit.
  • TreeSet: manté els elements ordenats (ordre natural o d’un Comparator).
ConsellAnalogia: els socis d’un club

Un Set és el registre de sòcies d’un club: cada persona hi consta un sol cop. Tant se val que s’apunti dues vegades: al llistat hi surt una sola.

Exemple:

Set<String> etiquetes = new HashSet<>();
etiquetes.add("java");
etiquetes.add("poo");
etiquetes.add("java");                                  // Ignorat: ja hi era
System.out.println(etiquetes.size());                   // 2
System.out.println(etiquetes.contains("poo"));          // true

// TreeSet: sempre ordenat
Set<Integer> ordenat = new TreeSet<>();
ordenat.add(30); ordenat.add(10); ordenat.add(20);
System.out.println(ordenat);                            // [10, 20, 30]
AlertaError típic: equals/hashCode mal definits

HashSet decideix si dos objectes són “el mateix” amb equals() i hashCode().

Si crees una classe Llibre i no els sobreescrius, dos llibres amb el mateix ISBN es consideraran diferents i s’hi colaran duplicats lògics.

Regla d’or: si un objecte va a un Set o és clau d’un Map, sobreescriu equals i hashCode alhora.

ConsellMini-repte

Tens una List<String> amb correus repetits. Com obtindries la llista de correus únics en una sola línia? (Pista: new HashSet<>(llista).)

15.5 Mapa

Un Map associa una clau única a un valor. No té índexs numèrics: accedeixes pel valor a través de la clau. Les claus no es repeteixen; els valors sí.

  • HashMap: el més usat, sense ordre.
  • TreeMap: ordenat per clau.
ConsellAnalogia: la família

Un Map és un diccionari: busques una paraula (clau) i obtens la seva definició (valor).

No busques “la definició número 5”; busques per paraula.

Exemple:

Map<String, Integer> estoc = new HashMap<>();
estoc.put("pomes", 50);                                     // Clau "pomes" → valor 50
estoc.put("peres", 30);
estoc.put("pomes", 42);                                     // MATEIXA clau → sobreescriu (ara 42)

System.out.println(estoc.get("pomes"));                     // 42
System.out.println(estoc.getOrDefault("kiwis", 0));         // 0 (no hi és)
System.out.println(estoc.containsKey("peres"));             // true

// Recórrer un mapa: parelles (entrySet)
for (Map.Entry<String, Integer> e : estoc.entrySet()) {
    System.out.println(e.getKey() + " → " + e.getValue());
}

Una orientació per quan fer servir cada tipus de col·lecció:

Necessito… Col·lecció Exemple
Ordre i posicions, admet duplicats List Historial de comandes
Elements únics, sense parella Set Etiquetes d’un article
Associar una clau a un valor Map DNI → Soci, producte → estoc
AlertaError típic: get sobre clau inexistent

Abans d’accedir a un valor per la seva clau, hem d’estar segurs que existeix o controlar-ho:

int n = estoc.get("kiwis");     // NullPointerException

Si la clau pot no existir, fes servir getOrDefault(clau, valorPerDefecte) o comprova abans amb containsKey.

ConsellMini-repte

Fes un Map<String, Integer> que compti quantes vegades apareix cada paraula d’una frase.

Pista: comptador.merge(paraula, 1, Integer::sum); incrementa o inicialitza en una línia.

15.6 Iteradors

El for-each és la manera còmoda de recórrer qualsevol element Iterable.

Però quan necessites esborrar elements durant el recorregut, cal l’iterador explícit: un objecte que recorre la col·lecció amb:

  • hasNext(): comprova si en queda algun element
  • next(): obté el següent element i avançar la posició.
ConsellAnalogia: el museu

Un iterador és un guia de museu: et va portant de sala en sala.

  • hasNext() es pregunta si encara queda alguna sala per veure.
  • next() passa a la següent sala.

Amb remove() pot, a més, tancar la sala per la qual acaba de passar sense trencar el recorregut.

Exemple:

List<Integer> notes = new ArrayList<>(List.of(5, 8, 3, 9, 4));

// For-each: recorregut senzill (només lectura)
int suma = 0;
for (int n : notes) {
    suma += n;
}

// Iterador: recorregut amb esborrat segur
Iterator<Integer> it = notes.iterator();
while (it.hasNext()) {
    int n = it.next();
    if (n < 5) {
        it.remove();            // Esborra el suspès de forma SEGURA
    }
}
System.out.println(notes);      // [5, 8, 9]

flowchart LR
    A["hasNext()?"] -->|true| B["next()"]
    B --> C{"condició?"}
    C -->|sí| D["remove()"]
    C -->|no| A
    D --> A
    A -->|false| E["fi del recorregut"]

NotaTres maneres de recórrer
  1. For-each: lectura simple, el codi més net.
  2. Iterador: quan cal esborrar mentre recorres.
  3. Streams: quan vols transformar o filtrar de forma declarativa. Es veurà més endavant.
ConsellMini-repte

Recorre un Map<String, Integer> amb un iterador sobre entrySet() i esborra totes les entrades amb valor 0.

15.7 Ordenar objectes

Ordenar Integer o String és fàcil: ja saben comparar-se.

Però, com s’ordenen objectes Llibre? Per títol? Per any? Per pàgines?

Java ofereix dues eines:

  • Comparable — la classe defineix el seu ordre natural implementant compareTo. N’hi ha un de sol.
  • Comparator — un objecte extern que defineix un ordre concret. En pots tenir molts (per títol, per any…).

El mètode compareTo/compare retorna:

  • Un negatiu si “aquest va abans”.
  • Un zero si són iguals.
  • Un positiu si “aquest va després”.
ConsellAnalogia

Comparable és l’ordre de fàbrica d’un producte (com ve ordenat el catàleg).

Comparator és ordenar la teva prestatgeria com tu vulguis: per color, per mida, per data… Cada Comparator és un criteri diferent aplicat des de fora.

Exemple amb Comparable:

public class Llibre implements Comparable<Llibre> {
    private String titol;
    private int any;

    public Llibre(String titol, int any) {
        this.titol = titol;
        this.any = any;
    }
    public String getTitol() { return titol; }
    public int getAny() { return any; }

    // Ordre natural: alfabètic per títol
    @Override
    public int compareTo(Llibre altre) {
        return this.titol.compareTo(altre.titol);
    }
    @Override
    public String toString() { return titol + " (" + any + ")"; }
}

I ho podem utilitzar:

List<Llibre> biblioteca = new ArrayList<>(List.of(
    new Llibre("Java a fons", 2021),
    new Llibre("Algorísmica", 2019),
    new Llibre("POO pràctica", 2023)
));

Collections.sort(biblioteca);        // Usa compareTo → per títol
System.out.println(biblioteca);

Exemple amb Comparator:

// Per any, de més antic a més nou
biblioteca.sort(Comparator.comparingInt(Llibre::getAny));

// Per títol, descendent
biblioteca.sort(Comparator.comparing(Llibre::getTitol).reversed());

// Per any i, en cas d'empat, per títol
biblioteca.sort(
    Comparator.comparingInt(Llibre::getAny)
              .thenComparing(Llibre::getTitol)
);

Els Comparator es construeixen de forma declarativa amb comparing, comparingInt, reversed i thenComparing.

Aquestes referències a mètode (Llibre::getAny) es veuran més endavant.

AlertaError típic: compareTo incoherent amb equals

Si compareTo retorna 0 per a dos objectes que equals considera diferents (o a l’inrevés), un TreeSet o un TreeMap es comportaran de forma estranya.

Mantén els dos criteris coherents: si dos objectes són iguals per equals, compareTo hauria de tornar 0.

NotaCollections: la navalla suïssa

La classe utilitària Collections porta mètodes molt útils: sort, reverse, shuffle, max, min, frequency, unmodifiableList… Val la pena fer-hi una ullada.

ConsellMini-repte

Afegeix a Llibre un camp pagines. Ordena la biblioteca per nombre de pàgines descendent i, en cas d’empat, alfabèticament per títol.

Fes-ho amb un sol Comparator encadenat.

15.8 Resum

  • Les col·leccions són contenidors dinàmics: creixen i decreixen sols, a diferència dels arrays.
    • List és una seqüència amb índex i duplicats (ArrayList per defecte; LinkedList per a molta inserció als extrems).
    • Set conté només elements únics (HashSet sense ordre; TreeSet ordenat). Exigeix bons equals/hashCode.
    • Map emmagatzema parelles de clau/valor (HashMap; TreeMap ordenat per clau). S’accedeix per clau.
  • La genericitat (<T>) etiqueta la col·lecció amb el tipus que guarda; el compilador comprova els tipus i evita càstings.
  • Els iteradors permeten esborrar amb seguretat durant el recorregut.
  • Comparable defineix l’ordre natural (un); Comparator defineix ordres a mida (molts).
  • Declara pel tipus interfície, instancia per la classe concreta.

15.9 Per practicar

  1. Agenda telefònica. Crea un Map<String, String> (nom → telèfon) amb operacions afegir, consultar, esborrar i llistar ordenat per nom.
    • Compara HashMap amb TreeMap: quina diferència veus en llistar?
  2. Sense repetits. Donada una List<String> amb noms repetits, obtén la llista de noms únics conservant l’ordre d’aparició. (Pista: LinkedHashSet.)
  3. Rànquing de jugadors. Modela una classe Jugador (nom, punts). Ordena una llista per punts descendent amb un Comparator i mostra el podi (3 primers).
  4. Comptador de paraules. Llegeix una frase, compta quantes vegades apareix cada paraula amb un Map<String, Integer> i mostra el resultat ordenat per freqüència.
  5. Ampliació del projecte BiblioTech. Guarda els llibres en una List<Llibre> i els socis en un Map<String, Soci> amb el DNI com a clau. Implementa cerca de llibre per títol i alta/baixa de socis.