Cada cerca de BiblioTech recorre una llista sencera. Cataleg.eliminar(referencia) compara amb tots els materials fins a trobar el seu. GestorPrestecs.retornar(referencia, dia) fa el mateix. Agrupar préstecs per empleat exigeix bucles imbricats: per a cada empleat, recórrer tots els préstecs. Amb cinc materials no es nota; amb cinquanta mil, cada cerca són cinquanta mil comparacions, i l'informe agrupat són dos mil cinc-cents milions d'operacions.

HashMap acaba amb això. És l'estructura que troba un valor a partir de la seva clau en temps constant, sense importar si el mapa té deu elements o deu milions. No és una millora incremental sobre recórrer una llista: és un canvi de categoria, d'O(n) a O(1), i per això és —juntament amb ArrayList— la col·lecció més usada de Java i una de les idees més importants de tota la informàtica.

Aquesta lliçó té dues meitats. La primera és pràctica: la interfície Map, la seva API completa i com aplicar-la a BiblioTech. La segona és la maquinària interna: la funció hash, els cubells, les col·lisions, el factor de càrrega i el rehash. I allà es compleix la promesa que es va fer explícitament a 03-09: veuràs, amb una demostració executable, per què equals i hashCode havien d'anar sempre junts, i què li passa a un objecte que entra en un mapa amb un dels dos mal implementat. Si alguna vegada has sentit "sobreescriu sempre tots dos" sense entendre'n el motiu, aquesta és la lliçó.

Contingut

  1. Què és un mapa i per què ho canvia tot
  2. La interfície Map: operacions bàsiques
  3. Recórrer un mapa correctament
  4. Els mètodes que t'estalvien la meitat del codi
  5. Com funciona un HashMap per dins
  6. Col·lisions, llistes de col·lisió i arbres
  7. Factor de càrrega i rehash
  8. La promesa complerta: per què equals i hashCode van junts
  9. El perill de les claus mutables
  10. Requisits d'una bona clau
  11. HashMap, LinkedHashMap i TreeMap
  12. Una memòria cau LRU en cinc línies
  13. Hashtable: el llegat
  14. Aplicació a BiblioTech
  15. Errors Habituals i Consells
  16. Exercicis

  1. Què és un mapa i per què ho canvia tot

Un mapa (també anomenat diccionari o taula associativa) guarda parells clau → valor. La clau identifica; el valor és allò identificat. Les claus són úniques: cadascuna porta a exactament un valor.

Map<String, Material> index = new HashMap<>();
index.put("978-0000000001", javaEficac);
index.put("978-0000000002", patronsDisseny);

Material trobat = index.get("978-0000000001");   // instantani

Compara aquest get amb el que feies abans:

// Abans: O(n). Amb 50.000 materials, fins a 50.000 comparacions de cadenes.
public Material cercarPerReferencia(String referencia) {
    for (Material m : materials) {
        if (m.getReferencia().equals(referencia)) { return m; }
    }
    return null;
}

// Ara: O(1). Una operacio, sempre, tingui el mapa 10 o 10.000.000 d'entrades.
public Material cercarPerReferencia(String referencia) {
    return index.get(referencia);
}

La diferència pràctica és brutal:

Elements Cerca en List (O(n)) Cerca en HashMap (O(1))
10 5 comparacions de mitjana 1 operació
1 000 500 1
1 000 000 500 000 1
100 000 000 50 000 000 1

Que el cost no creixi gens amb la mida és el que fa del mapa una eina estructuralment diferent. I no és màgia: el preu és memòria addicional i el requisit que les claus tinguin un hashCode correcte. L'apartat 5 explica com s'aconsegueix.

Recorda de 05-02 que Map no és una Collection: guarda parells, no elements solts, i add(x), contains(x) o iterator() hi serien ambigus. Es connecta amb la resta del Framework a través de les seves tres vistes: keySet(), values() i entrySet().

  1. La interfície Map: operacions bàsiques

import java.util.HashMap;
import java.util.Map;

Map<String, Material> index = new HashMap<>();

Els dos paràmetres de tipus són, per aquest ordre, el tipus de la clau i el tipus del valor. Map<String, Material> és "un mapa amb claus que són cadenes i valors que són materials". Com a 05-02, llegeix-ho així i deixa la teoria per a 10-01.

put: inserir o substituir

Material anterior = index.put("978-0000000001", javaEficac);
System.out.println(anterior);      // null: no hi havia res amb aquesta clau

Material previ = index.put("978-0000000001", altraEdicio);
System.out.println(previ);         // el javaEficac anterior: l'ha SUBSTITUIT

put retorna el valor anterior associat a aquella clau, o null si no n'hi havia cap. És un detall útil: permet saber si estaves inserint o reemplaçant sense fer cap consulta prèvia.

I la propietat fonamental: si la clau ja existeix, el valor se substitueix. Un mapa no té mai dues entrades amb la mateixa clau.

get i getOrDefault

Material m = index.get("978-0000000001");      // el valor, o null si no hi es
Material x = index.get("no-existeix");         // null

// getOrDefault evita el null i tota una familia de comprovacions
int termini = terminisPerTipus.getOrDefault("Audiollibre", 15);   // 15 si no esta registrat

getOrDefault(clau, perDefecte) és dels mètodes més útils del Framework. Substitueix això:

Integer valor = mapa.get(clau);
int resultat = (valor == null) ? 0 : valor;

per això:

int resultat = mapa.getOrDefault(clau, 0);

containsKey, containsValue, remove

boolean estaCatalogat  = index.containsKey("978-0000000001");   // O(1)
boolean apareixElLlibre = index.containsValue(javaEficac);      // O(n): recorre TOT

Material tret = index.remove("978-0000000001");   // retorna el valor eliminat, o null
boolean haEstatTret = index.remove("978-0000000002", patronsDisseny);  // nomes si coincideix el valor

Fixa't en l'asimetria: containsKey és O(1) i containsValue és O(n). El mapa està indexat per clau, no per valor; buscar un valor obliga a recórrer totes les entrades. Si necessites buscar en totes dues direccions sovint, mantén dos mapes o replanteja el disseny.

size, isEmpty, clear

System.out.println(index.size());         // nombre de parells
System.out.println(index.isEmpty());
index.clear();

  1. Recórrer un mapa correctament

Un mapa ofereix tres vistes, i triar la correcta importa per al rendiment.

Map<String, Material> index = new HashMap<>();
// ... ple ...

// 1. Nomes les claus
for (String referencia : index.keySet()) {
    System.out.println(referencia);
}

// 2. Nomes els valors
for (Material m : index.values()) {
    System.out.println(m.getTitol());
}

// 3. Claus I valors: SEMPRE amb entrySet
for (Map.Entry<String, Material> entrada : index.entrySet()) {
    System.out.printf("%-16s -> %s%n", entrada.getKey(), entrada.getValue().getTitol());
}

La tercera és la important. L'error freqüent és aquest:

// INEFICIENT: dues operacions per entrada
for (String referencia : index.keySet()) {
    Material m = index.get(referencia);        // cerca extra, innecessaria
    System.out.println(referencia + " -> " + m.getTitol());
}

Cada get és una cerca completa (calcular hash, localitzar cubell, comparar). Amb entrySet, la clau i el valor arriben junts al mateix objecte Map.Entry, sense cap cerca addicional. Quan necessitis clau i valor, fes servir sempre entrySet.

Map.Entry és una interfície imbricada (04-03) que representa un parell:

Mètode de Map.Entry Què fa
getKey() La clau
getValue() El valor
setValue(v) Canvia el valor al mapa: és una vista, no una còpia

Aquest setValue permet modificar durant el recorregut de manera segura:

for (Map.Entry<String, Integer> e : comptadors.entrySet()) {
    e.setValue(e.getValue() + 1);      // legal i eficient
}

I forEach sobre un mapa rep un BiConsumer (04-06), que accepta els dos arguments:

index.forEach((referencia, material) ->
    System.out.printf("%-16s -> %s%n", referencia, material.getTitol()));

Les tres vistes són vistes vives, no còpies: eliminar de keySet() elimina l'entrada del mapa.

index.keySet().remove("978-0000000001");      // elimina l'entrada SENCERA
index.values().removeIf(m -> !m.estaDisponible());   // elimina les entrades els valors de les quals compleixin

I, com qualsevol col·lecció, modificar-les durant un for-each provoca ConcurrentModificationException, amb les mateixes solucions de 05-02.

Un avís que es repetirà: l'ordre de recorregut d'un HashMap no està garantit i pot canviar en inserir elements. No escriguis mai codi —ni tests— que en depengui. Si necessites ordre, fes servir LinkedHashMap o TreeMap (apartat 11).

  1. Els mètodes que t'estalvien la meitat del codi

Java 8 va afegir a Map un grup de mètodes que resolen patrons quotidians i que molta gent continua sense fer servir, escrivint cinc línies on en bastaria una.

putIfAbsent

// En lloc de:
if (!index.containsKey(ref)) { index.put(ref, material); }

// Escriu:
index.putIfAbsent(ref, material);

Insereix només si la clau no hi era (o el seu valor era null). Retorna el valor existent, o null si ha inserit.

computeIfAbsent: el rei dels mapes de llistes

Aquest és, probablement, el mètode més útil de tota la interfície. Resol el patró "agrupar elements per una clau":

// SENSE computeIfAbsent: el patro classic, verbos i facil de trencar
Map<Empleat, List<Prestec>> perEmpleat = new HashMap<>();
for (Prestec p : prestecs) {
    List<Prestec> llista = perEmpleat.get(p.getEmpleat());
    if (llista == null) {
        llista = new ArrayList<>();
        perEmpleat.put(p.getEmpleat(), llista);
    }
    llista.add(p);
}

// AMB computeIfAbsent: una linia
for (Prestec p : prestecs) {
    perEmpleat.computeIfAbsent(p.getEmpleat(), k -> new ArrayList<>()).add(p);
}

Es llegeix així: "dona'm la llista associada a aquest empleat; si no existeix, crea-la amb aquesta funció, desa-la i torna-me-la". El resultat sempre és una llista vàlida, així que pots encadenar-hi l'.add(p) amb total seguretat.

La funció rep la clau com a argument, cosa que de vegades resulta útil:

Map<String, List<Material>> perTipus = new HashMap<>();
for (Material m : cataleg) {
    perTipus.computeIfAbsent(m.getTipus(), tipus -> new ArrayList<>()).add(m);
}

Un avís important: la funció només s'executa si la clau no hi és. Això la fa eficient (no crea llistes inútils) però també significa que no ha de tenir efectes col·laterals dels quals depenguis.

merge: el rei dels comptadors

Resol el patró "acumular per clau":

// SENSE merge
Map<String, Integer> comptadorPerTipus = new HashMap<>();
for (Material m : cataleg) {
    Integer actual = comptadorPerTipus.get(m.getTipus());
    comptadorPerTipus.put(m.getTipus(), (actual == null) ? 1 : actual + 1);
}

// AMB merge
for (Material m : cataleg) {
    comptadorPerTipus.merge(m.getTipus(), 1, Integer::sum);
}

merge(clau, valorInicial, funcioDeCombinacio) funciona així: si la clau no hi és, guarda valorInicial; si hi és, aplica la funció al valor existent i al nou, i guarda el resultat. Integer::sum és la referència a mètode (04-06) que suma dos enters.

Serveix per a qualsevol acumulació, no només per comptar:

Map<Empleat, Double> multaPerEmpleat = new HashMap<>();
for (Prestec p : prestecs) {
    multaPerEmpleat.merge(p.getEmpleat(), p.calcularMulta(diaActual), Double::sum);
}

Map<String, String> titolsPerTipus = new HashMap<>();
for (Material m : cataleg) {
    titolsPerTipus.merge(m.getTipus(), m.getTitol(), (a, b) -> a + ", " + b);
}

Una alternativa igual de llegible per comptar, amb getOrDefault:

comptadorPerTipus.put(tipus, comptadorPerTipus.getOrDefault(tipus, 0) + 1);

compute i computeIfPresent

// compute: recalcula SEMPRE, amb el valor actual (que pot ser null)
comptadors.compute("Llibre", (k, v) -> (v == null) ? 1 : v + 1);

// computeIfPresent: nomes actua si la clau JA existeix
inventari.computeIfPresent("978-0000000001", (k, v) -> v - 1);

Un detall important dels tres: si la funció retorna null, l'entrada s'elimina del mapa. És un idioma útil per netejar sobre la marxa:

// Descompta una unitat i elimina l'entrada quan arriba a zero
inventari.computeIfPresent(isbn, (k, v) -> (v <= 1) ? null : v - 1);

replaceAll

tarifes.replaceAll((tipus, tarifa) -> tarifa * 1.10);   // pujada del 10 % a tot

Taula resum

Mètode Quan fer-lo servir
getOrDefault(k, def) Llegir amb valor per defecte, sense comprovar null
putIfAbsent(k, v) Inserir només si no hi era
computeIfAbsent(k, f) Mapes de llistes o conjunts: agrupar per clau
computeIfPresent(k, f) Actualitzar només el que ja existeix
compute(k, f) Recalcular sempre, existeixi o no
merge(k, v, f) Comptadors i acumuladors
replaceAll(f) Transformar tots els valors
forEach(bc) Recórrer clau i valor amb un BiConsumer

Aquests set mètodes eliminen una quantitat enorme de codi repetitiu. Al mòdul 10, els Streams afegiran Collectors.groupingBy i counting(), que expressen el mateix de manera encara més declarativa; fins llavors, computeIfAbsent i merge són les teves eines.

  1. Com funciona un HashMap per dins

Ara la maquinària. Entendre-la és el que et permetrà fer servir mapes sense sorpreses.

Un HashMap és, per dins, un array de "cubells" (buckets). Cada cubell pot contenir zero, una o diverses entrades.

transient Node<K,V>[] table;          // l'array de cubells
transient int size;                   // nombre de parells emmagatzemats
int threshold;                        // capacitat * factor de carrega
final float loadFactor;               // 0.75 per defecte

La idea, en tres passos:

Pas 1: calcular el hash de la clau. Es crida clau.hashCode(), que retorna un int (uns 4300 milions de valors possibles).

"978-0000000001".hashCode();     // per exemple, -1234567890

Pas 2: convertir aquest hash en un índex de l'array. Com que l'array té, per exemple, 16 cubells, cal reduir el hash a un número entre 0 i 15. HashMap fa servir una operació de bits equivalent a hash % 16, però moltíssim més ràpida:

int index = (table.length - 1) & hash;    // equival a hash % 16 quan length es potencia de 2

Per això la capacitat d'un HashMap és sempre una potència de dos: permet substituir el mòdul (una divisió, cara) per un AND de bits (una instrucció). A més, HashMap aplica abans una funció de barreja que combina els bits alts amb els baixos, perquè claus els hashes de les quals només difereixin en els bits alts no acabin totes al mateix cubell:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Pas 3: guardar o buscar en aquell cubell.

flowchart TB
    K["clau: '978-0000000001'"] --> H["hashCode() → -1234567890"]
    H --> M["barreja de bits: h XOR (h >>> 16)"]
    M --> I["index = (16-1) AND hash → 7"]
    I --> T["table[7]"]

    subgraph taula["Object[] table (capacitat 16)"]
        B0["[0] null"]
        B1["[1] → (ISBN-2, Patrons)"]
        B2["[2] null"]
        B7["[7] → (ISBN-1, Java Eficac)"]
        B9["[9] → (REV-2024-03, Java Magazine)"]
        B15["[15] null"]
    end
    T --> B7

I aquí hi ha el truc: trobar el cubell correcte no requereix buscar res. Es calcula. Tant se val que el mapa tingui 10 entrades o 10 milions: calcular el hash i l'índex costa el mateix. Això és O(1).

Buscar és exactament el mateix procés:

Material m = index.get("978-0000000001");
// 1. hash de la clau -> 2. index de cubell -> 3. mirar en aquell cubell

  1. Col·lisions, llistes de col·lisió i arbres

Un int té uns 4300 milions de valors, però l'array només té 16 cubells. És inevitable que dues claus diferents acabin al mateix: això és una col·lisió.

HashMap les resol amb encadenament: cada cubell guarda una llista enllaçada d'entrades.

flowchart LR
    subgraph taula["table"]
        B0["[0] null"]
        B3["[3] →"]
        B7["[7] →"]
    end
    B3 --> E1["(clau A, valor 1)"]
    E1 --> E2["(clau B, valor 2)"]
    E2 --> E3["(clau C, valor 3)"]
    B7 --> E4["(clau D, valor 4)"]

Les claus A, B i C tenen hashes diferents però cauen al cubell 3. En buscar A:

  1. Es calcula el seu hash i l'índex: cubell 3.
  2. Es recorre la llista d'aquell cubell comparant: primer per hash (ràpid, és un int) i, si coincideix, amb equals (la comparació real).

Aquí hi ha el paper de cada mètode, i és la clau de tot: hashCode decideix en quin cubell buscar; equals decideix quina de les entrades d'aquell cubell és la correcta. Tots dos són imprescindibles, i per això han de ser coherents.

El cost real

Situació Cost de get
Sense col·lisions (cas normal) O(1)
Poques col·lisions O(1) amb una constant una mica més gran
Totes les claus al mateix cubell O(n): degenera en una llista

El pitjor cas passa si hashCode està mal implementat. L'exemple extrem:

@Override public int hashCode() { return 42; }    // legal, pero catastrofic

És tècnicament correcte —el contracte només exigeix que objectes iguals tinguin el mateix hash—, però totes les claus caurien al mateix cubell i el mapa es comportaria com una llista enllaçada: O(n) a cada operació.

La conversió a arbre (Java 8+)

Per limitar el dany, des de Java 8 HashMap vigila la longitud de cada cubell. Si un cubell acumula 8 o més entrades (i la taula té almenys 64 cubells), la llista d'aquell cubell es converteix en un arbre roig-negre, un arbre binari equilibrat:

flowchart TB
    subgraph abans["Cubell amb 8+ entrades: LLISTA → O(n)"]
        L1["e1"] --> L2["e2"] --> L3["e3"] --> L4["e4"] --> L5["..."] --> L8["e8"]
    end
    subgraph despres["Convertit en ARBRE → O(log n)"]
        R["e4"] --> A1["e2"]
        R --> A2["e6"]
        A1 --> B1["e1"]
        A1 --> B2["e3"]
        A2 --> B3["e5"]
        A2 --> B4["e7"]
    end

Amb això, el pitjor cas passa d'O(n) a O(log n). És una xarxa de seguretat, no una excusa: si els teus cubells es converteixen en arbres, el teu hashCode està mal repartit. (Si el cubell baixa de 6 entrades, torna a ser una llista.)

Aquest canvi es va introduir per motius de seguretat: existia un atac de denegació de servei que enviava a un servidor milers de claus amb el mateix hash per degradar-ne els mapes a O(n).

  1. Factor de càrrega i rehash

Com més entrades hi hagi per al mateix nombre de cubells, més col·lisions. El factor de càrrega (loadFactor) és el llindar d'ocupació a partir del qual HashMap decideix créixer. El seu valor per defecte és 0,75.

llindar = capacitat × factorDeCarrega

Amb la capacitat inicial de 16: llindar = 16 × 0,75 = 12. En inserir l'entrada número 13, es dispara un rehash:

  1. Es crea un array nou del doble de cubells (32).
  2. Es recol·loca cada entrada, recalculant-ne l'índex amb la nova capacitat.
  3. Es descarta l'array antic.
flowchart LR
    A["16 cubells<br/>12 entrades<br/>llindar assolit"] --> B["s'insereix la 13"]
    B --> C["new Node[32]"]
    C --> D["recollocar les 13 entrades<br/>index = (32-1) AND hash"]
    D --> E["32 cubells<br/>nou llindar: 24"]

El rehash és O(n) i és la raó que put sigui O(1) amortitzat i no O(1) pur, exactament igual que el redimensionament d'ArrayList (05-03). Com que la capacitat es duplica, el cost mitjà per inserció continua sent constant.

Per què 0,75

És un compromís mesurat:

Factor de càrrega Col·lisions Memòria malbaratada Rehashes
0,5 Molt poques Molta (mitja array buida) Freqüents
0,75 Poques Raonable Raonables
1,0 Bastants Cap Rars
2,0 Moltes: es degrada a llistes Cap Molt rars

Amb 0,75 i un hashCode decent, la probabilitat que un cubell tingui més d'un element és baixa, i el malbaratament de memòria és d'un 25 %. Canvia'l només si tens una mesura que ho justifiqui.

Dimensionar bé un mapa gran

Si saps quantes entrades hi posaràs, dimensiona el mapa en construir-lo i estalvia't tots els rehashes:

// Malament: 50.000 entrades provoquen uns 12 rehashes, amb recollocacions O(n) cada vegada
Map<String, Material> index = new HashMap<>();

// Be: capacitat suficient des del principi
Map<String, Material> index = new HashMap<>((int) (50_000 / 0.75f) + 1);

La fórmula entradesPrevistes / 0.75 + 1 garanteix que no hi hagi cap rehash. Compte amb l'error clàssic d'escriure new HashMap<>(50_000): això indica capacitat, no nombre d'entrades, i amb factor 0,75 el rehash saltaria en arribar a 37 500.

Com sempre: això importa amb desenes de milers d'entrades. Amb dues-centes, new HashMap<>() és perfecte.

  1. La promesa complerta: per què equals i hashCode van junts

Ha arribat el moment. A 03-09 vas aprendre el contracte:

Si a.equals(b) és true, llavors a.hashCode() == b.hashCode() ha de ser true.

(El contrari no s'exigeix: dos objectes diferents poden compartir hash.)

I es va prometre que al mòdul 5 en veuries el perquè. Aquí està, amb codi executable.

Experiment 1: equals sí, hashCode no

Una classe que sobreescriu equals correctament però s'oblida de hashCode:

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

/** Clau composta d'un exemplar. equals correcte, hashCode ABSENT. */
public class ClauExemplarTrencada {

    private final String isbn;
    private final int    numeroExemplar;

    public ClauExemplarTrencada(String isbn, int numeroExemplar) {
        this.isbn = isbn;
        this.numeroExemplar = numeroExemplar;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (!(o instanceof ClauExemplarTrencada altra)) { return false; }
        return numeroExemplar == altra.numeroExemplar && Objects.equals(isbn, altra.isbn);
    }

    // FALTA hashCode: s'hereta el d'Object, basat en l'adreca de memoria
}

I el resultat:

ClauExemplarTrencada c1 = new ClauExemplarTrencada("978-0000000001", 1);
ClauExemplarTrencada c2 = new ClauExemplarTrencada("978-0000000001", 1);

System.out.println("c1.equals(c2): " + c1.equals(c2));            // true: son "iguals"
System.out.println("hash c1: " + c1.hashCode());                  // 1735600054  (per exemple)
System.out.println("hash c2: " + c2.hashCode());                  // 21685669    DIFERENT

Map<ClauExemplarTrencada, String> ubicacions = new HashMap<>();
ubicacions.put(c1, "Prestatge A-3");

System.out.println("get(c1): " + ubicacions.get(c1));             // Prestatge A-3
System.out.println("get(c2): " + ubicacions.get(c2));             // null   <-- EL PROBLEMA
System.out.println("containsKey(c2): " + ubicacions.containsKey(c2));  // false
System.out.println("size: " + ubicacions.size());                 // 1

ubicacions.put(c2, "Prestatge B-1");
System.out.println("size despres de posar c2: " + ubicacions.size());  // 2  <-- CLAU DUPLICADA

L'objecte ha desaparegut del mapa. I pitjor: ara hi ha dues claus iguals en un mapa que garanteix claus úniques.

Segueix-ne el rastre pas a pas:

flowchart TB
    P["put(c1, 'A-3')"] --> P1["c1.hashCode() = 1735600054"]
    P1 --> P2["index = 6 → desat a table[6]"]

    G["get(c2)"] --> G1["c2.hashCode() = 21685669"]
    G1 --> G2["index = 5 → mira a table[5]"]
    G2 --> G3["table[5] esta BUIDA"]
    G3 --> G4["retorna null: equals no arriba MAI a cridar-se"]

Aquí hi ha l'explicació completa: get busca al cubell que indica hashCode. Si el hash és diferent, mira en un altre cubell, i equals no s'executa mai. Que c1.equals(c2) sigui true és irrellevant si mai no arriben a comparar-se.

I put(c2, ...) afegeix una segona entrada perquè, des del punt de vista del mapa, c2 va a un altre cubell i no hi ha res amb què comparar-la.

Experiment 2: la versió correcta

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

/** La mateixa clau, amb equals i hashCode COHERENTS. */
public class ClauExemplar {

    private final String isbn;
    private final int    numeroExemplar;

    public ClauExemplar(String isbn, int numeroExemplar) {
        this.isbn = Objects.requireNonNullElse(isbn, "");
        this.numeroExemplar = numeroExemplar;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (!(o instanceof ClauExemplar altra)) { return false; }
        return numeroExemplar == altra.numeroExemplar && isbn.equals(altra.isbn);
    }

    /** Els MATEIXOS camps que fa servir equals, ni un mes ni un menys. */
    @Override
    public int hashCode() {
        return Objects.hash(isbn, numeroExemplar);
    }

    @Override
    public String toString() {
        return isbn + "#" + numeroExemplar;
    }
}
ClauExemplar c1 = new ClauExemplar("978-0000000001", 1);
ClauExemplar c2 = new ClauExemplar("978-0000000001", 1);

System.out.println("hash c1: " + c1.hashCode());     // 1635958654
System.out.println("hash c2: " + c2.hashCode());     // 1635958654   IGUAL

Map<ClauExemplar, String> ubicacions = new HashMap<>();
ubicacions.put(c1, "Prestatge A-3");

System.out.println("get(c2): " + ubicacions.get(c2));   // Prestatge A-3   <-- FUNCIONA
ubicacions.put(c2, "Prestatge B-1");
System.out.println("size: " + ubicacions.size());       // 1: c2 ha SUBSTITUIT c1

Ara sí. Mateix hash → mateix cubell → equals s'executa → el mapa reconeix que són la mateixa clau.

I per això els record de 04-07 són claus perfectes: generen automàticament equals i hashCode a partir de tots els seus components, sempre coherents entre si.

public record ClauExemplarRecord(String isbn, int numeroExemplar) { }   // ja es una clau valida

La regla, en tres línies

Situació Conseqüència
equals sí, hashCode no L'objecte desapareix de mapes i conjunts; apareixen duplicats
hashCode sí, equals no Van al mateix cubell, però equals compara referències: tampoc no es troben
Tots dos, incoherents entre si Comportament impredictible segons els camps que faci servir cadascun
Tots dos, sobre els mateixos camps Correcte

I la regla operativa definitiva: si sobreescrius un, sobreescriu l'altre, i fes-ho sobre exactament els mateixos camps. Tots els IDE generen tots dos alhora precisament per això.

  1. El perill de les claus mutables

Hi ha una segona manera de trencar un mapa, més subtil i més difícil de depurar: mutar una clau després d'haver-la inserit.

package com.nexussoftware.bibliotech.domini;

import java.util.Objects;

/** Clau MUTABLE: el camp isbn pot canviar. Mala idea. */
public class ClauMutable {
    private String isbn;                       // NO es final

    public ClauMutable(String isbn) { this.isbn = isbn; }
    public void setIsbn(String isbn) { this.isbn = isbn; }    // el veri

    @Override public boolean equals(Object o) {
        return (o instanceof ClauMutable c) && Objects.equals(isbn, c.isbn);
    }
    @Override public int hashCode() { return Objects.hash(isbn); }
    @Override public String toString() { return "Clau[" + isbn + "]"; }
}
ClauMutable clau = new ClauMutable("978-0000000001");
Map<ClauMutable, String> mapa = new HashMap<>();
mapa.put(clau, "Java Eficac");

System.out.println(mapa.get(clau));         // Java Eficac     correcte

clau.setIsbn("978-0000000002");             // MUTEM la clau JA INSERIDA

System.out.println(mapa.get(clau));         // null   <-- l'objecte s'ha perdut
System.out.println(mapa.size());            // 1      <-- pero continua aqui dins
System.out.println(mapa.containsKey(clau)); // false
System.out.println(mapa);                   // {Clau[978-0000000002]=Java Eficac}

L'entrada continua existint, es veu en imprimir el mapa, compta per al size... i és inabastable.

flowchart TB
    A["put(clau, valor)<br/>hashCode = 1234 → cubell 2"] --> B["l'entrada queda a table[2]"]
    B --> C["clau.setIsbn(...)<br/>ara hashCode = 9876"]
    C --> D["get(clau): index = 9876 mod 16 → cubell 4"]
    D --> E["table[4] no te aquesta entrada"]
    E --> F["null: l'entrada de table[2] es INABASTABLE"]

El mapa va col·locar l'entrada al cubell corresponent al hash que la clau tenia en el moment d'inserir-la. En canviar el hash, ningú no recol·loca res: el mapa no vigila les seves claus. L'entrada queda òrfena, ocupant memòria i sense poder recuperar-se ni tan sols amb remove.

Aquest problema és especialment traïdor amb col·leccions com a clau:

List<String> llista = new ArrayList<>(List.of("a", "b"));
Map<List<String>, String> mapa = new HashMap<>();
mapa.put(llista, "valor");

llista.add("c");                        // el hashCode de la llista DEPEN del seu contingut (05-03)
System.out.println(mapa.get(llista));   // null

I amb objectes de domini mutables:

// Si Empleat tingues equals/hashCode basats en el nom i el nom pogues canviar:
Map<Empleat, List<Prestec>> perEmpleat = new HashMap<>();
perEmpleat.put(marta, prestecsDeMarta);
marta.setNom("Marta Ruiz Garcia");      // catastrofe silenciosa

La solució és sempre la mateixa: les claus han de ser immutables, o com a mínim ho han de ser els camps que intervenen a equals i hashCode.

  1. Requisits d'una bona clau

Requisit Per què Com aconseguir-ho
Immutable Si canvia el seu hash, l'entrada es perd Camps final, sense setters (03-07)
equals correcte Distingeix entrades dins del cubell Sobre els camps que defineixen la identitat
hashCode correcte Localitza el cubell Sobre els mateixos camps que equals
Ben repartit Evita que tot caigui en un cubell Objects.hash(...) ho fa bé
Barat de calcular Es crida a cada operació Camps simples, o guardar-lo en memòria cau si és car
No null en general HashMap l'admet, altres mapes no Prefereix claus reals

Els millors candidats a clau, per ordre de preferència:

  1. String: immutable, amb equals/hashCode correctes i hash desat internament. És la clau més comuna amb diferència.
  2. Embolcalls numèrics (Integer, Long): immutables i amb hash trivial.
  3. enum: immutables per construcció, amb identitat única. I si la teva clau és un enum, considera EnumMap, una implementació especialitzada extremadament eficient.
  4. record (04-07): equals i hashCode generats, coherents i sobre camps immutables. El candidat ideal per a claus compostes.
  5. Classes pròpies immutables amb tots dos mètodes ben escrits.

Els pitjors candidats:

  • Objectes mutables de domini (Empleat, Prestec) si els seus camps d'identitat poden canviar.
  • Col·leccions mutables (ArrayList, HashSet): el seu hash depèn del contingut.
  • Arrays: el seu hashCode és l'heretat d'Object, basat en identitat. mapa.get(new int[]{1,2}) no trobarà mai l'entrada desada amb new int[]{1,2}. Fes servir List.of(1, 2) en el seu lloc.
  • Objectes amb hashCode car que no es desa en memòria cau.

Un exemple de clau composta ben feta a BiblioTech:

/** Identifica un exemplar concret: mateix ISBN, diferent numero de copia. */
public record ClauExemplar(String isbn, int numeroExemplar) {
    public ClauExemplar {
        if (isbn == null || isbn.isBlank()) { isbn = "SENSE-ISBN"; }
        if (numeroExemplar < 1) { numeroExemplar = 1; }
    }
}

Map<ClauExemplar, String> ubicacions = new HashMap<>();
ubicacions.put(new ClauExemplar("978-0000000001", 1), "Prestatge A-3");
ubicacions.put(new ClauExemplar("978-0000000001", 2), "Prestatge A-4");

System.out.println(ubicacions.get(new ClauExemplar("978-0000000001", 2)));  // Prestatge A-4

Una línia de declaració i ja és una clau perfecta.

  1. HashMap, LinkedHashMap i TreeMap

Les tres implementacions principals de Map, comparades:

Aspecte HashMap LinkedHashMap TreeMap
Estructura interna Array de cubells Cubells + llista doblement enllaçada Arbre roig-negre
Ordre de recorregut Cap de garantit Inserció (o accés) Claus ordenades
get / put / remove O(1) O(1) O(log n)
containsKey O(1) O(1) O(log n)
Memòria per entrada Menor +2 referències per entrada Major (nodes d'arbre)
Clau null Una de permesa Una de permesa NO permesa
Valors null
Requisit de la clau equals + hashCode equals + hashCode Comparable o Comparator
Quan fer-lo servir Per defecte Ordre reproduïble; memòria cau LRU Rangs, firstKey, ordre permanent

LinkedHashMap

Manté una llista enllaçada que travessa totes les entrades en l'ordre en què es van inserir. El cost és petit (dues referències per entrada) i a canvi el recorregut és determinista:

Map<String, Integer> terminis = new LinkedHashMap<>();
terminis.put("Llibre", 15);
terminis.put("Revista", 7);
terminis.put("DVD", 3);

terminis.forEach((tipus, dies) -> System.out.println(tipus + ": " + dies));
// SEMPRE en aquest ordre: Llibre, Revista, DVD

Fes-lo servir quan l'ordre importi per a la presentació, per als tests o per a la reproduïbilitat. Curiosament, recórrer-lo és fins i tot una mica més ràpid que recórrer un HashMap, perquè segueix la llista enllaçada en lloc d'examinar tots els cubells (inclosos els buits).

TreeMap

Manté les claus ordenades en un arbre equilibrat. Tot costa O(log n) en lloc d'O(1), però a canvi ofereix operacions que cap mapa de hash no pot donar:

TreeMap<String, Material> perReferencia = new TreeMap<>();
perReferencia.put("978-0000000002", patronsDisseny);
perReferencia.put("978-0000000001", javaEficac);
perReferencia.put("978-0000000003", refactoritzacio);

// Sempre ordenat, sense ordenar res a ma
perReferencia.forEach((k, v) -> System.out.println(k + " -> " + v.getTitol()));

System.out.println(perReferencia.firstKey());        // 978-0000000001
System.out.println(perReferencia.lastKey());         // 978-0000000003
System.out.println(perReferencia.firstEntry());      // parell complet

// Navegacio: NavigableMap
System.out.println(perReferencia.floorKey("978-0000000002x"));   // la clau mes gran <= donada
System.out.println(perReferencia.ceilingKey("978-0000000000"));  // la clau mes petita >= donada
System.out.println(perReferencia.higherKey("978-0000000001"));   // estrictament mes gran
System.out.println(perReferencia.lowerKey("978-0000000003"));    // estrictament mes petita

// Vistes per rang, que son VISTES VIVES del mapa original
SortedMap<String, Material> primers = perReferencia.headMap("978-0000000003");  // < donada
SortedMap<String, Material> ultims  = perReferencia.tailMap("978-0000000002");  // >= donada
NavigableMap<String, Material> tram = perReferencia.subMap("978-0000000001", true,
                                                           "978-0000000002", true);

System.out.println(perReferencia.descendingMap().firstKey());    // recorregut invers

I admet un Comparator propi, connectant amb tot el de 04-06:

// Ordenat per longitud de la clau i, a igualtat, alfabeticament
Map<String, Integer> aMida = new TreeMap<>(
    Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));

Quan triar TreeMap: quan necessitis recórrer en ordre amb freqüència, consultar rangs (headMap, subMap), o preguntar "quina és la clau immediatament anterior a aquesta". Si només necessites ordre una vegada al final, surt més barat fer servir un HashMap i ordenar les claus en aquell moment.

Nuls, resumits

Clau null Valors null
HashMap , una (va al cubell 0) Sí, tants com vulguis
LinkedHashMap Sí, una
TreeMap No: NullPointerException
Hashtable No No
Map.of(...) No No

Que HashMap accepti una clau null genera una ambigüitat coneguda: mapa.get(clau) retorna null tant si la clau no hi és com si està associada a null. Per distingir-ho, containsKey. En general, evita les claus i els valors null: compliquen el codi sense aportar res. I Optional (10-04) és la resposta moderna a "potser no hi ha valor".

  1. Una memòria cau LRU en cinc línies

LinkedHashMap té una capacitat poc coneguda i molt elegant. El seu tercer constructor accepta un mode d'ordenació:

new LinkedHashMap<>(capacitatInicial, factorDeCarrega, accessOrder);

Amb accessOrder = true, la llista interna es reordena a cada accés: cada vegada que consultes una entrada amb get, aquella entrada passa al final. És a dir, el principi de la llista és sempre l'entrada usada fa més temps.

Combinat amb el mètode removeEldestEntry, que LinkedHashMap invoca després de cada inserció per preguntar si ha d'expulsar l'entrada més antiga, tens una memòria cau LRU (Least Recently Used) completa:

package com.nexussoftware.bibliotech.servei;

import java.util.LinkedHashMap;
import java.util.Map;

/** Cache LRU: conserva les N fitxes consultades mes recentment. */
public class CacheFitxes<K, V> extends LinkedHashMap<K, V> {

    private final int maxim;

    public CacheFitxes(int maxim) {
        super(16, 0.75f, true);      // accessOrder = true: reordena a cada get
        this.maxim = maxim;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> mesAntiga) {
        return size() > maxim;       // si retorna true, LinkedHashMap l'expulsa
    }
}

Cinc línies útils. En acció:

CacheFitxes<String, String> cache = new CacheFitxes<>(3);

cache.put("978-0000000001", "Java Eficac");
cache.put("978-0000000002", "Patrons de Disseny");
cache.put("978-0000000003", "Refactoritzacio");
System.out.println(cache.keySet());      // [...001, ...002, ...003]

cache.get("978-0000000001");             // consultem el PRIMER: passa al final
System.out.println(cache.keySet());      // [...002, ...003, ...001]

cache.put("978-0000000004", "Codi Net");        // supera el maxim
System.out.println(cache.keySet());      // [...003, ...001, ...004]
// Ha desaparegut ...002: era la menys usada recentment

Fixa't en el detall clau: ...001 va sobreviure perquè el vam consultar, encara que fos el més antic per inserció. Això és exactament la política LRU, i és la que fan servir les memòries cau de bases de dades, navegadors i sistemes operatius.

A BiblioTech serviria per desar en memòria cau les fitxes dels materials més consultats sense que la memòria creixi sense límit.

  1. Hashtable: el llegat

Hashtable és la classe original de Java 1.0, anterior al Framework de Col·leccions. No la facis servir en codi nou.

Hashtable HashMap
Sincronitzada , tots els mètodes No
Rendiment Pitjor (bloqueig a cada operació) Millor
Claus/valors null Prohibits Permesos
Antiguitat Java 1.0 Java 1.2
Iteració Enumeration (obsoleta) Iterator

El seu únic avantatge aparent —estar sincronitzada— resulta ser insuficient per a l'ús concurrent real, perquè bloqueja tota la taula a cada operació i tot i així no fa atòmiques les seqüències compostes del tipus "comprovar i després inserir".

La resposta correcta per a concurrència és ConcurrentHashMap, que permet accés concurrent sense bloquejar el mapa sencer i ofereix operacions atòmiques. És tema del mòdul 8.

El mateix val per a Vector (el Hashtable de les llistes) i per a Stack, que estén Vector i veuràs desaconsellada a 05-08. Totes comparteixen la mateixa història: sincronització global heretada de Java 1.0.

  1. Aplicació a BiblioTech

Ara, la refactorització gran. Dues aplicacions que canvien el projecte d'arrel.

Índex per referència al Cataleg

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import com.nexussoftware.bibliotech.domini.Material;

/** Cataleg amb index per referencia: cerques en O(1). */
public class Cataleg {

    private final List<Material>          materials    = new ArrayList<>();
    private final Map<String, Material>   perReferencia = new HashMap<>();
    private final Map<String, List<Material>> perTipus  = new HashMap<>();

    /** Alta: mante els tres contenidors sincronitzats. */
    public boolean afegir(Material m) {
        if (m == null) { return false; }
        // putIfAbsent retorna el valor existent, o null si ha inserit
        if (perReferencia.putIfAbsent(m.getReferencia(), m) != null) {
            return false;                       // referencia duplicada: alta rebutjada
        }
        materials.add(m);
        perTipus.computeIfAbsent(m.getTipus(), tipus -> new ArrayList<>()).add(m);
        return true;
    }

    /** Cerca per referencia: O(1). Abans era O(n). */
    public Material cercarPerReferencia(String referencia) {
        return perReferencia.get(referencia);
    }

    public boolean estaCatalogat(String referencia) {
        return perReferencia.containsKey(referencia);
    }

    /** Baixa: O(1) per localitzar-lo, O(n) per treure'l de la llista. */
    public boolean eliminar(String referencia) {
        Material m = perReferencia.remove(referencia);
        if (m == null) { return false; }
        materials.remove(m);
        // Si el grup queda buit, l'entrada s'elimina del mapa (funcio que retorna null)
        perTipus.computeIfPresent(m.getTipus(),
                (tipus, llista) -> { llista.remove(m); return llista.isEmpty() ? null : llista; });
        return true;
    }

    /** Tots els materials d'un tipus: O(1). Abans era un recorregut complet. */
    public List<Material> perTipus(String tipus) {
        return List.copyOf(perTipus.getOrDefault(tipus, List.of()));
    }

    /** Informe per tipus: abans eren 20 linies amb bucles imbricats O(n^2). */
    public Map<String, Integer> comptarPerTipus() {
        Map<String, Integer> resum = new HashMap<>();
        for (Material m : materials) {
            resum.merge(m.getTipus(), 1, Integer::sum);
        }
        return resum;
    }

    /** Suma de tarifes diaries agrupada per tipus, tambe amb merge. */
    public Map<String, Double> tarifaTotalPerTipus() {
        Map<String, Double> resum = new HashMap<>();
        for (Material m : materials) {
            resum.merge(m.getTipus(), m.getTarifaDiaria(), Double::sum);
        }
        return resum;
    }

    public List<Material> cercar(Predicate<Material> criteri) {
        List<Material> resultat = new ArrayList<>();
        for (Material m : materials) {
            if (criteri.test(m)) { resultat.add(m); }
        }
        return resultat;
    }

    public void ordenar(Comparator<Material> criteri) { materials.sort(criteri); }
    public List<Material> llistar() { return List.copyOf(materials); }
    public int mida() { return materials.size(); }
}

Compara l'informePerTipus promès al tancament del mòdul 4:

// ABANS: bucles imbricats, O(n * tipus), 20 linies
public void informePerTipus(Material[] cataleg) {
    String[] tipus = { "Llibre", "Revista", "DVD", "Audiollibre" };
    for (int t = 0; t < tipus.length; t++) {
        int quants = 0;
        double tarifa = 0.0;
        for (int i = 0; i < cataleg.length; i++) {
            if (cataleg[i] != null && cataleg[i].getTipus().equals(tipus[t])) {
                quants++;
                tarifa += cataleg[i].getTarifaDiaria();
            }
        }
        if (quants > 0) {
            System.out.printf("%-12s %3d materials, tarifa mitjana %.2f%n",
                              tipus[t], quants, tarifa / quants);
        }
    }
}

// ARA: una passada, O(n), i sense llista de tipus codificada a ma
public void informePerTipus() {
    Map<String, Integer> quants  = comptarPerTipus();
    Map<String, Double>  tarifes = tarifaTotalPerTipus();
    quants.forEach((tipus, n) ->
        System.out.printf("%-12s %3d materials, tarifa mitjana %.2f%n",
                          tipus, n, tarifes.get(tipus) / n));
}

Tres línies de cos, una sola passada pel catàleg, i els tipus ja no estan codificats a mà: si demà apareix l'audiollibre, l'informe l'inclou tot sol.

Préstecs per empleat

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Prestec;

public class RegistrePrestecs {

    private final Map<String, Prestec>          perReferencia = new HashMap<>();
    private final Map<Empleat, List<Prestec>>   perEmpleat    = new HashMap<>();

    /**
     * Empleat com a CLAU: exigeix equals/hashCode correctes i basats en un camp
     * IMMUTABLE. A BiblioTech es basen en 'identificador', que es final.
     */
    public void registrar(Prestec p) {
        if (p == null) { return; }
        perReferencia.put(p.getReferencia(), p);
        perEmpleat.computeIfAbsent(p.getEmpleat(), e -> new ArrayList<>()).add(p);
    }

    /** Devolucio en O(1): abans recorria tota la llista de prestecs. */
    public boolean retornar(String referenciaPrestec, int dia) {
        Prestec p = perReferencia.get(referenciaPrestec);
        if (p == null || p.estaRetornat()) { return false; }
        p.registrarDevolucio(dia);
        return true;
    }

    /** Prestecs d'un empleat en O(1): abans era un recorregut complet. */
    public List<Prestec> de(Empleat empleat) {
        return List.copyOf(perEmpleat.getOrDefault(empleat, List.of()));
    }

    /** Multa acumulada per empleat, amb merge. */
    public Map<Empleat, Double> multesPerEmpleat(int diaActual) {
        Map<Empleat, Double> multes = new HashMap<>();
        for (List<Prestec> llista : perEmpleat.values()) {
            for (Prestec p : llista) {
                if (!p.estaRetornat() && p.estaVencut(diaActual)) {
                    multes.merge(p.getEmpleat(), p.calcularMulta(diaActual), Double::sum);
                }
            }
        }
        return multes;
    }

    /** Quants prestecs actius te cada empleat. */
    public Map<Empleat, Integer> actiusPerEmpleat() {
        Map<Empleat, Integer> actius = new HashMap<>();
        for (Map.Entry<Empleat, List<Prestec>> e : perEmpleat.entrySet()) {
            int n = 0;
            for (Prestec p : e.getValue()) {
                if (!p.estaRetornat()) { n++; }
            }
            if (n > 0) { actius.put(e.getKey(), n); }
        }
        return actius;
    }
}

Ús:

Empleat marta = new Empleat("Marta Ruiz",   "EMP-001");
Empleat diego = new Empleat("Diego Alonso", "EMP-002");

Cataleg cataleg = new Cataleg();
cataleg.afegir(new Llibre("Java Eficac",        "Joshua Bloch",  "978-0000000001", 2018));
cataleg.afegir(new Llibre("Patrons de Disseny", "Erich Gamma",   "978-0000000002", 1994));
cataleg.afegir(new Llibre("Refactoritzacio",    "Martin Fowler", "978-0000000003", 1999));
cataleg.afegir(new Revista("Java Magazine",     "REV-2024-03",   42, "Mensual"));
cataleg.afegir(new Dvd("Refactoritzacio en directe", "DVD-0007", 95));

// Cerca instantania, sense recorrer res
Material m = cataleg.cercarPerReferencia("978-0000000002");
System.out.println("Trobat: " + m.getTitol());

System.out.println("--- Informe per tipus ---");
cataleg.comptarPerTipus().forEach((tipus, n) -> System.out.printf("  %-8s %d%n", tipus, n));

RegistrePrestecs registre = new RegistrePrestecs();
registre.registrar(new Prestec(cataleg.cercarPerReferencia("978-0000000001"), marta, 100));
registre.registrar(new Prestec(cataleg.cercarPerReferencia("978-0000000003"), marta, 102));
registre.registrar(new Prestec(cataleg.cercarPerReferencia("DVD-0007"),       diego, 105));

System.out.println("Prestecs de Marta: " + registre.de(marta).size());
System.out.println("--- Multes el dia 130 ---");
registre.multesPerEmpleat(130).forEach((e, multa) ->
    System.out.printf("  %-14s %6.2f EUR%n", e.getNom(), multa));
Trobat: Patrons de Disseny
--- Informe per tipus ---
  Llibre   3
  Revista  1
  DVD      1
Prestecs de Marta: 2
--- Multes el dia 130 ---
  Marta Ruiz      20,00 EUR
  Diego Alonso    12,50 EUR

Errors Habituals i Consells

Sobreescriure equals sense hashCode. L'error número u. L'objecte es desa però no es troba mai, i es poden crear claus duplicades. Si sobreescrius un, sobreescriu l'altre, sobre els mateixos camps. Deixa que l'IDE els generi tots dos, o fes servir un record.

Fer servir claus mutables. Si el hash de la clau canvia després d'inserir-la, l'entrada queda inabastable per sempre. Claus immutables: String, Integer, enum, record.

Fer servir una col·lecció mutable com a clau. El hashCode d'una llista depèn del seu contingut: afegir-hi un element fa perdre l'entrada. Si necessites una llista com a clau, List.copyOf(...).

Fer servir un array com a clau. El seu hashCode és el d'Object, basat en identitat: no trobaràs mai l'entrada. Fes servir List.of(...).

Dependre de l'ordre de recorregut d'un HashMap. No està garantit i pot canviar en inserir o entre versions de Java. Si necessites ordre, LinkedHashMap o TreeMap. Un test que depengui de l'ordre d'un HashMap fallarà algun dia.

Recórrer amb keySet() i fer get a dins. Duplica la feina. Fes servir entrySet() quan necessitis clau i valor.

Confondre containsKey amb containsValue. El primer és O(1); el segon, O(n). No posis un containsValue dins d'un bucle.

Modificar el mapa durant el recorregut. ConcurrentModificationException, igual que amb qualsevol col·lecció. Fes servir l'Iterator d'entrySet(), values().removeIf(...) o keySet().removeIf(...).

Confondre get que retorna null amb "la clau no existeix". Pot existir amb valor null. Distingeix-ho amb containsKey, o millor, no guardis valors null.

new HashMap<>(n) creient que n són les entrades previstes. És la capacitat: amb factor 0,75, el rehash salta a les 0,75 × n. La fórmula correcta és new HashMap<>((int)(previstes / 0.75f) + 1).

Fer servir Hashtable o Vector en codi nou. Són llegat de Java 1.0. Per a ús normal, HashMap; per a concurrència, ConcurrentHashMap (mòdul 8).

Consell: computeIfAbsent i merge són els teus millors amics. El mapa de llistes i el comptador són els dos patrons més freqüents del món real, i cadascun es resol en una línia. Interioritza'ls.

Consell: els record són claus perfectes. Quan necessitis una clau composta, declara un record d'una línia (04-07) i tindràs equals i hashCode correctes de franc.

Exercicis

Exercici 1: índex invertit de cerca

Escriu IndexCerca que permeti buscar materials per paraules del seu títol:

  • void indexar(Material m): descompon el títol en paraules (minúscules, separades per espais) i desa cada paraula en un Map<String, List<Material>> amb computeIfAbsent.
  • List<Material> cercar(String paraula): els materials que continguin aquella paraula, o llista buida.
  • Map<String, Integer> frequencies(): quantes vegades apareix cada paraula, amb merge.
  • List<String> paraulesMesComunes(int quantes): les N paraules més freqüents.
  • void eliminar(Material m): treu el material de totes les seves paraules, esborrant l'entrada quan la llista quedi buida.

Exercici 2: la demostració d'equals i hashCode

Escriu una classe executable DemostracioHashCode amb un main que demostri, imprimint resultats i explicant-los:

  1. Una classe ClauTrencada amb equals però sense hashCode: posa un objecte en un HashMap i demostra que un objecte igual no el troba, i que es poden crear claus duplicades.
  2. Una classe ClauCorrecta amb tots dos: demostra que funciona.
  3. Una classe ClauMutable: posa-hi un objecte, muta la clau i demostra que l'entrada queda inabastable però continua comptant per al size().
  4. Un record ClauRecord: demostra que funciona sense escriure cap mètode.
  5. Una classe ClauBeneita amb hashCode() que sempre retorna 42: mesura el temps de 50 000 insercions i cerques, i compara'l amb ClauCorrecta.

Exercici 3: estadístiques de la biblioteca

Escriu EstadistiquesBiblioteca que rebi un List<Prestec> i calculi, fent servir exclusivament mètodes de Map (res de Streams):

  • Map<Empleat, Integer> prestecsPerEmpleat().
  • Map<String, Double> multaTotalPerTipus(int diaActual).
  • Map<Gravetat, List<Prestec>> agrupatsPerGravetat(int diaActual).
  • Empleat empleatMesActiu(): el que més préstecs té, null si no n'hi ha cap.
  • Map<String, Integer> topMaterials(int quants): els N materials més prestats, en un LinkedHashMap que conservi l'ordre de major a menor.

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.nexussoftware.bibliotech.domini.Material;

/** Index invertit: de cada paraula als materials que la contenen al titol. */
public class IndexCerca {

    private final Map<String, List<Material>> perParaula = new HashMap<>();
    private final Map<String, Integer>        frequencia = new HashMap<>();

    private static List<String> paraulesDe(Material m) {
        if (m == null || m.getTitol() == null) { return List.of(); }
        List<String> paraules = new ArrayList<>();
        for (String p : m.getTitol().toLowerCase().split("\\s+")) {
            if (p.length() > 2) { paraules.add(p); }   // descartem "de", "el", "la"...
        }
        return paraules;
    }

    public void indexar(Material m) {
        for (String paraula : paraulesDe(m)) {
            // computeIfAbsent: crea la llista NOMES si la paraula es nova,
            // i retorna sempre una llista valida sobre la qual encadenar add
            perParaula.computeIfAbsent(paraula, p -> new ArrayList<>()).add(m);
            // merge: si la paraula es nova guarda 1; si existeix, suma
            frequencia.merge(paraula, 1, Integer::sum);
        }
    }

    /** getOrDefault evita retornar null i les comprovacions de qui crida. */
    public List<Material> cercar(String paraula) {
        if (paraula == null) { return List.of(); }
        return List.copyOf(perParaula.getOrDefault(paraula.toLowerCase(), List.of()));
    }

    public Map<String, Integer> frequencies() {
        return Map.copyOf(frequencia);        // copia immutable
    }

    public List<String> paraulesMesComunes(int quantes) {
        // Aboquem les claus a una llista i l'ordenem per frequencia descendent.
        // Un TreeMap no serviria: ordena per CLAU, no per valor.
        List<String> paraules = new ArrayList<>(frequencia.keySet());
        paraules.sort(Comparator.comparingInt((String p) -> frequencia.get(p)).reversed()
                                .thenComparing(Comparator.naturalOrder()));
        return paraules.subList(0, Math.min(quantes, paraules.size()));
    }

    public void eliminar(Material m) {
        for (String paraula : paraulesDe(m)) {
            // Si la funcio retorna null, computeIfPresent ELIMINA l'entrada del mapa.
            perParaula.computeIfPresent(paraula, (p, llista) -> {
                llista.remove(m);
                return llista.isEmpty() ? null : llista;
            });
            frequencia.computeIfPresent(paraula, (p, n) -> (n <= 1) ? null : n - 1);
        }
    }

    public int paraulesIndexades() { return perParaula.size(); }
}

Prova:

IndexCerca index = new IndexCerca();
index.indexar(new Llibre("Java Eficac",           "Joshua Bloch",  "978-0000000001", 2018));
index.indexar(new Llibre("Patrons de Disseny",    "Erich Gamma",   "978-0000000002", 1994));
index.indexar(new Llibre("Refactoritzacio",       "Martin Fowler", "978-0000000003", 1999));
index.indexar(new Revista("Java Magazine",        "REV-2024-03",   42, "Mensual"));
index.indexar(new Dvd("Refactoritzacio en directe", "DVD-0007",    95));

System.out.println("Amb 'java': " + index.cercar("java").size());               // 2
System.out.println("Amb 'refactoritzacio': " + index.cercar("refactoritzacio").size()); // 2
System.out.println("Mes comunes: " + index.paraulesMesComunes(3));

La clau de l'exercici és el trio computeIfAbsent / merge / computeIfPresent retornant null. Els tres junts permeten mantenir un mapa de llistes i un comptador sense ni una sola comprovació de null explícita. Escrit amb get i put a mà, el mateix codi ocuparia el triple i tindria almenys un NullPointerException esperant el seu moment.

Una observació important: paraulesMesComunes ha d'abocar i ordenar, perquè un mapa ordena per clau, mai per valor. És una limitació estructural dels mapes que convé tenir present.

Solució 2

package com.nexussoftware.bibliotech.presentacio;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class DemostracioHashCode {

    // --- 1. equals SENSE hashCode ---
    static class ClauTrencada {
        final String isbn;
        ClauTrencada(String isbn) { this.isbn = isbn; }
        @Override public boolean equals(Object o) {
            return (o instanceof ClauTrencada c) && Objects.equals(isbn, c.isbn);
        }
        // sense hashCode: s'hereta el d'Object (basat en identitat)
        @Override public String toString() { return "Trencada[" + isbn + "]"; }
    }

    // --- 2. equals I hashCode coherents ---
    static class ClauCorrecta {
        final String isbn;
        ClauCorrecta(String isbn) { this.isbn = isbn; }
        @Override public boolean equals(Object o) {
            return (o instanceof ClauCorrecta c) && Objects.equals(isbn, c.isbn);
        }
        @Override public int hashCode() { return Objects.hash(isbn); }
        @Override public String toString() { return "Correcta[" + isbn + "]"; }
    }

    // --- 3. clau MUTABLE ---
    static class ClauMutable {
        String isbn;                        // no final: el problema
        ClauMutable(String isbn) { this.isbn = isbn; }
        @Override public boolean equals(Object o) {
            return (o instanceof ClauMutable c) && Objects.equals(isbn, c.isbn);
        }
        @Override public int hashCode() { return Objects.hash(isbn); }
        @Override public String toString() { return "Mutable[" + isbn + "]"; }
    }

    // --- 4. record: equals i hashCode DE FRANC ---
    record ClauRecord(String isbn, int exemplar) { }

    // --- 5. hashCode constant: legal pero catastrofic ---
    static class ClauBeneita {
        final String isbn;
        ClauBeneita(String isbn) { this.isbn = isbn; }
        @Override public boolean equals(Object o) {
            return (o instanceof ClauBeneita c) && Objects.equals(isbn, c.isbn);
        }
        @Override public int hashCode() { return 42; }   // TOTES al mateix cubell
    }

    public static void main(String[] args) {
        demostrarTrencada();
        demostrarCorrecta();
        demostrarMutable();
        demostrarRecord();
        demostrarBeneita();
    }

    static void demostrarTrencada() {
        System.out.println("=== 1. equals SENSE hashCode ===");
        ClauTrencada a = new ClauTrencada("978-0000000001");
        ClauTrencada b = new ClauTrencada("978-0000000001");

        System.out.println("a.equals(b): " + a.equals(b));            // true
        System.out.println("hash a: " + a.hashCode());
        System.out.println("hash b: " + b.hashCode() + "   <- DIFERENT");

        Map<ClauTrencada, String> mapa = new HashMap<>();
        mapa.put(a, "Prestatge A-3");
        System.out.println("get(a): " + mapa.get(a));                 // Prestatge A-3
        System.out.println("get(b): " + mapa.get(b) + "   <- s'ha PERDUT");

        mapa.put(b, "Prestatge B-1");
        System.out.println("size despres de posar b: " + mapa.size() + "   <- CLAU DUPLICADA");
        System.out.println("Causa: get(b) calcula el hash de b, va a UN ALTRE cubell,");
        System.out.println("       el troba buit i retorna null. equals no es crida MAI.\n");
    }

    static void demostrarCorrecta() {
        System.out.println("=== 2. equals I hashCode ===");
        ClauCorrecta a = new ClauCorrecta("978-0000000001");
        ClauCorrecta b = new ClauCorrecta("978-0000000001");

        System.out.println("hash a == hash b: " + (a.hashCode() == b.hashCode()));  // true

        Map<ClauCorrecta, String> mapa = new HashMap<>();
        mapa.put(a, "Prestatge A-3");
        System.out.println("get(b): " + mapa.get(b) + "   <- FUNCIONA");
        mapa.put(b, "Prestatge B-1");
        System.out.println("size: " + mapa.size() + "   <- b ha SUBSTITUIT a\n");
    }

    static void demostrarMutable() {
        System.out.println("=== 3. clau MUTABLE ===");
        ClauMutable clau = new ClauMutable("978-0000000001");
        Map<ClauMutable, String> mapa = new HashMap<>();
        mapa.put(clau, "Java Eficac");
        System.out.println("get abans de mutar:   " + mapa.get(clau));

        clau.isbn = "978-0000000002";         // mutem la clau JA inserida

        System.out.println("get despres de mutar: " + mapa.get(clau) + "   <- PERDUDA");
        System.out.println("containsKey:          " + mapa.containsKey(clau));
        System.out.println("size:                 " + mapa.size() + "   <- continua aqui dins");
        System.out.println("contingut:            " + mapa);
        System.out.println("L'entrada es al cubell del hash ANTIC. Ningu no la recolloca:");
        System.out.println("es memoria ocupada i irrecuperable, ni tan sols amb remove.\n");
    }

    static void demostrarRecord() {
        System.out.println("=== 4. record ===");
        Map<ClauRecord, String> mapa = new HashMap<>();
        mapa.put(new ClauRecord("978-0000000001", 1), "Prestatge A-3");
        mapa.put(new ClauRecord("978-0000000001", 2), "Prestatge A-4");

        System.out.println("get(nova instancia igual): "
                           + mapa.get(new ClauRecord("978-0000000001", 2)));
        System.out.println("size: " + mapa.size());
        System.out.println("Zero metodes escrits: el record els genera coherents.\n");
    }

    static void demostrarBeneita() {
        System.out.println("=== 5. hashCode constant ===");
        final int N = 50_000;

        Map<ClauCorrecta, Integer> bona = new HashMap<>();
        long t1 = System.nanoTime();
        for (int i = 0; i < N; i++) { bona.put(new ClauCorrecta("ref-" + i), i); }
        for (int i = 0; i < N; i++) { bona.get(new ClauCorrecta("ref-" + i)); }
        long msBona = (System.nanoTime() - t1) / 1_000_000;

        Map<ClauBeneita, Integer> beneita = new HashMap<>();
        long t2 = System.nanoTime();
        for (int i = 0; i < N; i++) { beneita.put(new ClauBeneita("ref-" + i), i); }
        for (int i = 0; i < N; i++) { beneita.get(new ClauBeneita("ref-" + i)); }
        long msBeneita = (System.nanoTime() - t2) / 1_000_000;

        System.out.printf("hashCode ben repartit:   %5d ms%n", msBona);
        System.out.printf("hashCode constant (42):  %5d ms%n", msBeneita);
        System.out.println("Totes les claus cauen al MATEIX cubell. Des de Java 8 aquest");
        System.out.println("cubell es converteix en arbre, aixi que degrada a O(log n) en");
        System.out.println("lloc d'O(n); sense aquesta xarxa de seguretat seria catastrofic.");
    }
}

Sortida típica:

=== 1. equals SENSE hashCode ===
a.equals(b): true
hash a: 1735600054
hash b: 21685669   <- DIFERENT
get(a): Prestatge A-3
get(b): null   <- s'ha PERDUT
size despres de posar b: 2   <- CLAU DUPLICADA
...
=== 5. hashCode constant ===
hashCode ben repartit:      38 ms
hashCode constant (42):   1420 ms

Aquesta és la demostració que 03-09 va prometre. Els cinc casos junts expliquen la història completa: hashCode tria el cubell i equals tria l'entrada dins d'ell. Si el primer falla, el segon no arriba mai a executar-se; si el segon falla, l'entrada correcta no es reconeix mai; si la clau muta, l'entrada es queda en un cubell que ja ningú no visitarà; i si el hash està mal repartit, el mapa deixa de ser O(1) encara que continuï funcionant.

Solució 3

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.nexussoftware.bibliotech.domini.Empleat;
import com.nexussoftware.bibliotech.domini.Gravetat;
import com.nexussoftware.bibliotech.domini.Prestec;

public class EstadistiquesBiblioteca {

    private final List<Prestec> prestecs;

    public EstadistiquesBiblioteca(List<Prestec> prestecs) {
        this.prestecs = (prestecs == null) ? List.of() : List.copyOf(prestecs);
    }

    /** merge: el patro comptador en una linia. */
    public Map<Empleat, Integer> prestecsPerEmpleat() {
        Map<Empleat, Integer> resultat = new HashMap<>();
        for (Prestec p : prestecs) {
            resultat.merge(p.getEmpleat(), 1, Integer::sum);
        }
        return resultat;
    }

    /** merge amb Double::sum: acumulador, mateix patro. */
    public Map<String, Double> multaTotalPerTipus(int diaActual) {
        Map<String, Double> resultat = new HashMap<>();
        for (Prestec p : prestecs) {
            double multa = p.calcularMulta(diaActual);
            if (multa > 0) {
                resultat.merge(p.getMaterial().getTipus(), multa, Double::sum);
            }
        }
        return resultat;
    }

    /** computeIfAbsent: el patro mapa de llistes. */
    public Map<Gravetat, List<Prestec>> agrupatsPerGravetat(int diaActual) {
        Map<Gravetat, List<Prestec>> resultat = new HashMap<>();
        for (Prestec p : prestecs) {
            Gravetat g = p.getMaterial().classificarGravetat(diaActual);
            resultat.computeIfAbsent(g, clau -> new ArrayList<>()).add(p);
        }
        return resultat;
        // Nota: amb un enum com a clau, EnumMap seria encara mes eficient.
        // A 10-04 aixo sera Collectors.groupingBy en una sola expressio.
    }

    /** Recorregut d'entrySet per quedar-nos amb el maxim. */
    public Empleat empleatMesActiu() {
        Empleat millor = null;
        int maxim = 0;
        for (Map.Entry<Empleat, Integer> e : prestecsPerEmpleat().entrySet()) {
            if (e.getValue() > maxim) {
                maxim  = e.getValue();
                millor = e.getKey();
            }
        }
        return millor;     // null si no hi ha prestecs
    }

    /**
     * Els N materials mes prestats, EN ORDRE.
     * Un HashMap no conserva ordre, aixi que cal abocar, ordenar i
     * reconstruir en un LinkedHashMap, que si que el conserva.
     */
    public Map<String, Integer> topMaterials(int quants) {
        Map<String, Integer> recompte = new HashMap<>();
        for (Prestec p : prestecs) {
            recompte.merge(p.getMaterial().getTitol(), 1, Integer::sum);
        }

        List<Map.Entry<String, Integer>> entrades = new ArrayList<>(recompte.entrySet());
        entrades.sort(Map.Entry.<String, Integer>comparingByValue().reversed()
                               .thenComparing(Map.Entry.comparingByKey()));

        Map<String, Integer> top = new LinkedHashMap<>();     // conserva l'ordre d'insercio
        int n = 0;
        for (Map.Entry<String, Integer> e : entrades) {
            if (n++ >= quants) { break; }
            top.put(e.getKey(), e.getValue());
        }
        return top;
    }

    public void informe(int diaActual) {
        System.out.println("=== Estadistiques de BiblioTech (dia " + diaActual + ") ===");

        System.out.println("Prestecs per empleat:");
        prestecsPerEmpleat().forEach((e, n) ->
            System.out.printf("  %-16s %d%n", e.getNom(), n));

        System.out.println("Multes per tipus de material:");
        multaTotalPerTipus(diaActual).forEach((tipus, multa) ->
            System.out.printf("  %-10s %6.2f EUR%n", tipus, multa));

        System.out.println("Prestecs per gravetat:");
        agrupatsPerGravetat(diaActual).forEach((g, llista) ->
            System.out.printf("  %-12s %d prestecs%n", g, llista.size()));

        Empleat actiu = empleatMesActiu();
        System.out.println("Empleat mes actiu: "
                           + (actiu == null ? "(cap)" : actiu.getNom()));

        System.out.println("Materials mes prestats:");
        topMaterials(3).forEach((titol, n) ->
            System.out.printf("  %-26s %d vegades%n", titol, n));
    }
}

Tres patrons es repeteixen a tota la classe, i són els tres que resolen la majoria dels informes que escriuràs a la teva vida professional:

  1. Comptador: mapa.merge(clau, 1, Integer::sum).
  2. Acumulador: mapa.merge(clau, valor, Double::sum).
  3. Agrupador: mapa.computeIfAbsent(clau, k -> new ArrayList<>()).add(element).

I una limitació important que l'exercici deixa veure: un mapa no es pot ordenar per valor. TreeMap ordena per clau. Per a un "top N" cal abocar entrySet() a una llista, ordenar-la amb Map.Entry.comparingByValue() i reconstruir en un LinkedHashMap si vols conservar l'ordre. És un idioma que convé tenir memoritzat.

Conclusió

Has creuat la frontera més important d'aquest mòdul. Saps que un mapa associa claus úniques a valors i troba un valor per la seva clau en O(1): un cost que no creix amb la mida, enfront dels O(n) de recórrer una llista. Amb un milió d'elements, la diferència és entre una operació i cinc-centes mil.

Domines la interfície Map completa: put amb el seu valor anterior retornat, get i getOrDefault, containsKey en O(1) enfront de containsValue en O(n), remove en les seves dues formes, i les tres vistes keySet/values/entrySet —vives, no còpies—, sabent que quan necessites clau i valor la resposta és sempre entrySet. I manegues els set mètodes de Java 8 que eliminen la meitat del codi repetitiu, amb els tres patrons que resolen la majoria dels informes reals: computeIfAbsent per a mapes de llistes, merge per a comptadors i acumuladors, i una funció que retorna null per eliminar entrades sobre la marxa.

Entens la maquinària: hashCode redueix la clau a un índex de cubell mitjançant una barreja de bits i un AND amb la capacitat —sempre potència de dos—, i equals distingeix les entrades dins d'aquell cubell. Saps què és una col·lisió, com s'encadenen les entrades, quan un cubell es converteix en arbre roig-negre (8 elements, taula d'almenys 64) i per què aquesta xarxa de seguretat es va introduir per motius de seguretat. Coneixes el factor de càrrega 0,75, el rehash que duplica els cubells i ho recol·loca tot en O(n), i per això saps que put és O(1) amortitzat, igual que l'add d'ArrayList. I saps dimensionar un mapa gran amb previstes / 0.75 + 1.

I sobretot, s'ha complert la promesa del mòdul 3. Has vist amb codi executable què passa quan sobreescrius equals sense hashCode: l'objecte desapareix, perquè get busca en un altre cubell i equals no arriba mai a executar-se; i a sobre es poden crear claus duplicades en un mapa que garanteix claus úniques. Has vist què passa en mutar una clau ja inserida: l'entrada queda òrfena al cubell del hash antic, compta per al size(), es veu en imprimir el mapa i és irrecuperable. I has vist què passa amb un hashCode constant: legal, i quaranta vegades més lent. D'aquí surten els requisits d'una bona clau —immutable, amb equals i hashCode sobre els mateixos camps, ben repartida i barata— i els millors candidats: String, embolcalls, enum i, per a claus compostes, un record d'una línia.

Coneixes les tres implementacions i quan fer servir cadascuna: HashMap per defecte, LinkedHashMap quan l'ordre hagi de ser reproduïble —i amb accessOrder més removeEldestEntry, una memòria cau LRU completa en cinc línies—, i TreeMap quan necessitis claus ordenades, rangs amb headMap/subMap o navegació amb floorKey/ceilingKey. I saps que Hashtable és llegat de Java 1.0 i que la resposta a la concurrència és ConcurrentHashMap, al mòdul 8.

BiblioTech ha canviat de categoria. El Cataleg manté un Map<String, Material> per referència i un Map<String, List<Material>> per tipus, tots dos actualitzats amb putIfAbsent i computeIfAbsent: buscar un material és instantani i aquell informePerTipus de vint línies amb bucles imbricats s'ha quedat, literalment, en tres —i ja no porta els tipus codificats a mà—. El RegistrePrestecs indexa per referència de préstec i per empleat, amb les multes acumulades mitjançant merge.

A la lliçó següent, HashSet, veuràs l'altra cara de la mateixa moneda. Un HashSet és literalment un HashMap amb valors ficticis, així que hereta tota la maquinària que acabes d'entendre —i tots els seus requisits sobre equals i hashCode—. Aprendràs la interfície Set, les operacions de conjunt (unió, intersecció, diferència, subconjunt) amb addAll, retainAll, removeAll i containsAll, com el boolean que retorna add resol la detecció de duplicats en una línia, la comparació entre HashSet, LinkedHashSet i TreeSet amb la seva navegació per rangs, el perill de mutar un element ja desat, i per què contains en un Set és incomparablement més ràpid que en una List. A BiblioTech, el Set<String> d'ISBN catalogats deixarà de ser un apedaçament i es convertirà en una peça de disseny.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats