Arribes al tancament del mòdul amb totes les peces sobre la taula. Saps guardar elements en llistes, garantir unicitat amb conjunts, indexar per clau amb mapes i modelar polítiques de procés amb cues i piles. Falta l'operació que travessa totes elles i que apareix pràcticament en qualsevol programa: posar les coses en ordre i trobar-les.

Ordenar i cercar no són temes menors. L'ordenació és probablement l'operació algorísmica més estudiada de la història de la informàtica, i l'elecció entre cerca lineal, cerca binària i accés per clau és una de les decisions que més impacte té en el rendiment d'un sistema real. En aquesta lliçó veuràs el contracte complet de Comparable —inclòs l'error de restar enters que desborda silenciosament—, tot l'arsenal de Comparator reprès des de 04-06, les quatre estratègies d'ordenació amb el seu criteri d'elecció, què significa que una ordenació sigui estable i per què això és el que permet ordenar per criteris successius, i quins algorismes fa servir Java realment per sota.

Després, la cerca: lineal, binària i per clau, amb la taula de complexitats que decideix entre elles i la interpretació exacta del valor negatiu que retorna binarySearch. I al final, el balanç del mòdul: BiblioTech amb catàleg indexat i ordenable, informes, cua de reserves i pila de desfer... i la llista honesta del que continua sent fràgil, que és exactament el temari del mòdul 6.

Contingut

  1. Ordre natural amb Comparable
  2. El contracte de compareTo
  3. L'error de restar enters
  4. Coherència amb equals i què es trenca sense ella
  5. Comparator: ordre extern i múltiple
  6. El catàleg de mètodes de Comparator
  7. Les quatre estratègies d'ordenació
  8. Estabilitat i ordenació per criteris successius
  9. Quin algorisme fa servir Java realment
  10. Cerca: lineal, binària i per clau
  11. binarySearch i el seu valor negatiu
  12. Utilitats de Collections
  13. Rendiment: ordenar una vegada, cercar moltes
  14. Tancament del mòdul: l'estat de BiblioTech
  15. Errors Habituals i Consells
  16. Exercicis

  1. Ordre natural amb Comparable

Una classe declara el seu ordre natural implementant la interfície Comparable:

public interface Comparable<T> {
    int compareTo(T altre);
}

Un sol mètode, que retorna un int el signe del qual —no el seu valor— és el que importa:

Resultat Significat
Negatiu this va abans que altre
Zero Són equivalents quant a l'ordre
Positiu this va després que altre

Ja vas fer servir Comparable a 04-07 en fer que Fitxa s'ordenés per títol. Aquí tens la implementació completa i correcta:

package com.nexussoftware.bibliotech.domini;

/** Fitxa bibliografica amb ordre natural per titol. */
public record Fitxa(String titol, String autor, int any) implements Comparable<Fitxa> {

    public Fitxa {
        if (titol == null || titol.isBlank()) { titol = "Sense titol"; }
        if (autor == null || autor.isBlank()) { autor = "Desconegut"; }
        if (any < 1450 || any > 2100)         { any   = 0; }
    }

    /** Ordre natural: per titol alfabetic. */
    @Override
    public int compareTo(Fitxa altra) {
        return this.titol.compareTo(altra.titol);    // String ja implementa Comparable
    }
}

I així es fa servir, sense dir a ningú com ordenar:

List<Fitxa> fitxes = new ArrayList<>(List.of(
    new Fitxa("Refactoritzacio",    "Martin Fowler", 1999),
    new Fitxa("Java Eficac",        "Joshua Bloch",  2018),
    new Fitxa("Patrons de Disseny", "Erich Gamma",   1994)));

fitxes.sort(null);                        // null = ordre natural
Collections.sort(fitxes);                 // equivalent
System.out.println(fitxes.get(0).titol()); // Java Eficac

TreeSet<Fitxa> ordenades = new TreeSet<>(fitxes);        // el TreeSet usa compareTo
Fitxa minima = Collections.min(fitxes);                  // tambe

Moltes classes del JDK ja el implementen: String (ordre lexicogràfic), tots els embolcalls numèrics, Character, Boolean, els enum (pel seu ordinal, és a dir, per l'ordre de declaració) i les classes de java.time (10-05).

Un Comparable ben implementat obre les portes a tot el Framework: sort sense arguments, TreeSet, TreeMap, PriorityQueue, Collections.max, Collections.min i binarySearch.

Ara bé, Comparable té una limitació important: només pots definir un ordre natural per classe. Si necessites ordenar materials per títol, per tarifa i per tipus, necessites Comparator. I hi ha una pregunta que convé fer-se abans d'implementar Comparable: existeix realment un ordre que sigui el natural per a aquest tipus? Per a un String o un número, sí. Per a un Material, discutible. Per a un Prestec, probablement no. Si dubtes, no implementis Comparable: fes servir comparadors.

  1. El contracte de compareTo

compareTo té un contracte tan estricte com el d'equals de 03-09, i trencar-lo produeix comportaments impredictibles a les col·leccions ordenades.

1. Antisimetria. sgn(a.compareTo(b)) == -sgn(b.compareTo(a)) per a tots els a i b. Si A va abans que B, B va després que A. I si a.compareTo(b) llança una excepció, b.compareTo(a) també l'ha de llançar.

2. Transitivitat. Si a.compareTo(b) > 0 i b.compareTo(c) > 0, llavors a.compareTo(c) > 0. Si A va després de B, i B després de C, A va després de C. Sense aquesta propietat, l'ordenació pot entrar en bucle o llançar IllegalArgumentException: Comparison method violates its general contract!, un error que surt de TimSort en detectar la incoherència.

3. Consistència amb la igualtat d'ordre. Si a.compareTo(b) == 0, llavors sgn(a.compareTo(c)) == sgn(b.compareTo(c)) per a tot c. Els elements "empatats" s'han de comportar igual davant de tercers.

4. Coherència amb equals (recomanada, no obligatòria). a.compareTo(b) == 0 hauria d'implicar a.equals(b). És l'única que no exigeix el compilador, i la que més problemes causa quan s'ignora. Té el seu apartat propi.

5. null no hi participa. a.compareTo(null) ha de llançar NullPointerException, no retornar un valor. null no té posició en un ordre.

Exemples de violacions que compilen perfectament:

// VIOLA l'antisimetria: sempre diu "vaig abans"
@Override public int compareTo(Fitxa altra) { return -1; }

// VIOLA la transitivitat: l'ordre depen d'alguna cosa que canvia
@Override public int compareTo(Fitxa altra) {
    return Double.compare(Math.random(), 0.5);
}

// VIOLA la consistencia: compara camps diferents segons el cas
@Override public int compareTo(Fitxa altra) {
    if (this.any > 2000) { return this.titol.compareTo(altra.titol); }
    return Integer.compare(this.any, altra.any);
}

La tercera és la més perillosa perquè sembla raonable. En ordenar una llista amb TimSort, l'algorisme assumeix transitivitat per descartar comparacions; si el criteri canvia segons l'element, el resultat pot ser un ordre incorrecte o directament una excepció enmig del sort.

  1. L'error de restar enters

Aquest és l'error clàssic de compareTo, i apareix en moltíssim codi en producció:

// MALAMENT: sembla correcte i funciona... fins que no
@Override
public int compareTo(Fitxa altra) {
    return this.any - altra.any;
}

El raonament és temptador: si this.any és més gran, la resta és positiva; si és més petit, negativa; si són iguals, zero. I funciona amb anys entre 1450 i 2100.

El problema és el desbordament (overflow). Un int va de −2 147 483 648 a 2 147 483 647. Si la resta se surt d'aquest rang, el resultat fa la volta i canvia de signe:

int a = 2_000_000_000;
int b = -2_000_000_000;

System.out.println(a - b);                 // -294967296   <- NEGATIU, i a > b !
System.out.println(Integer.compare(a, b)); // 1            <- correcte

a és clarament més gran que b, però la resta desborda i retorna un negatiu, així que compareTo afirma el contrari. El resultat és una ordenació silenciosament incorrecta, un TreeSet que perd elements i un binarySearch que no troba el que hi ha.

La solució és no restar mai. Fes servir els mètodes estàtics de comparació, que estan escrits precisament per a això:

Integer.compare(a, b)      // per a int
Long.compare(a, b)         // per a long
Double.compare(a, b)       // per a double: a mes tracta be NaN i -0.0
Boolean.compare(a, b)      // false < true
Character.compare(a, b)
// BE
@Override
public int compareTo(Fitxa altra) {
    return Integer.compare(this.any, altra.any);
}

El cas de Double mereix menció a part. Restar dobles té un problema addicional al desbordament: la resta de dos dobles molt propers pot donar 0.0 sense que siguin iguals, i a més Double.compare gestiona correctament NaN i la distinció entre 0.0 i -0.0, cosa que una resta no fa.

// MALAMENT
return (int) (this.tarifa - altra.tarifa);   // 0.25 - 0.10 = 0.15 -> (int) 0 -> "iguals"!

// BE
return Double.compare(this.tarifa, altra.tarifa);

Aquest exemple és especialment insidiós: el casting a int trunca qualsevol diferència menor que 1, així que totes les tarifes de BiblioTech es considerarien iguals.

Regla sense excepcions: no restis mai per comparar. Fes servir sempre Tipus.compare(a, b).

  1. Coherència amb equals i què es trenca sense ella

Ja ho vas veure de passada a 05-06 amb TreeSet; aquí tens el problema complet.

Es diu que un ordre és coherent amb equals quan a.compareTo(b) == 0 si i només si a.equals(b). És a dir: dos elements empaten en l'ordre exactament quan són iguals.

Quan aquesta coherència es trenca, les col·leccions ordenades es comporten de manera inesperada, perquè TreeSet i TreeMap fan servir compareTo, no equals, per decidir la unicitat.

package com.nexussoftware.bibliotech.domini;

/** Fitxa amb ordre natural per ANY: incoherent amb equals. */
record FitxaPerAny(String titol, String autor, int any)
        implements Comparable<FitxaPerAny> {

    @Override
    public int compareTo(FitxaPerAny altra) {
        return Integer.compare(this.any, altra.any);    // nomes l'any
    }
}
FitxaPerAny a = new FitxaPerAny("Java Eficac",        "Joshua Bloch", 2018);
FitxaPerAny b = new FitxaPerAny("Patrons de Disseny", "Erich Gamma",  2018);

System.out.println(a.equals(b));        // false: son fitxes DIFERENTS
System.out.println(a.compareTo(b));     // 0:     pero EMPATEN en l'ordre

// En una List no passa res
List<FitxaPerAny> llista = new ArrayList<>(List.of(a, b));
System.out.println(llista.size());      // 2   correcte

// En un TreeSet, si
TreeSet<FitxaPerAny> conjunt = new TreeSet<>(List.of(a, b));
System.out.println(conjunt.size());     // 1   <-- S'HA PERDUT UNA FITXA

// I contains menteix
System.out.println(conjunt.contains(b));   // true, encara que b no hi sigui

S'ha perdut un element en silenci. El TreeSet va preguntar compareTo, va obtenir 0 i va concloure que b ja hi era.

El mateix amb TreeMap:

TreeMap<FitxaPerAny, String> mapa = new TreeMap<>();
mapa.put(a, "Prestatge A-3");
mapa.put(b, "Prestatge B-1");
System.out.println(mapa.size());      // 1: b ha SUBSTITUIT a
System.out.println(mapa.get(a));      // Prestatge B-1  <-- valor equivocat

Compara amb el que passa a les estructures basades en hash:

Col·lecció Decideix la unicitat amb Fitxa per any incoherent
List Res: admet duplicats 2 elements, correcte
HashSet / HashMap equals + hashCode 2 elements, correcte
TreeSet / TreeMap compareTo / compare 1 element: se'n perd un
PriorityQueue Res: admet duplicats 2 elements, però ordre arbitrari entre ells

La solució és sempre la mateixa: afegir criteris de desempat fins que l'ordre sigui total, és a dir, fins que només empatin els elements realment iguals.

@Override
public int compareTo(FitxaPerAny altra) {
    int perAny = Integer.compare(this.any, altra.any);
    if (perAny != 0) { return perAny; }
    int perTitol = this.titol.compareTo(altra.titol);
    if (perTitol != 0) { return perTitol; }
    return this.autor.compareTo(altra.autor);   // els tres camps: coherent amb equals
}

O, molt més llegible, amb Comparator (apartat següent):

private static final Comparator<FitxaPerAny> ORDRE =
    Comparator.comparingInt(FitxaPerAny::any)
              .thenComparing(FitxaPerAny::titol)
              .thenComparing(FitxaPerAny::autor);

@Override
public int compareTo(FitxaPerAny altra) { return ORDRE.compare(this, altra); }

I el consell de documentació: si el teu ordre natural no és coherent amb equals a propòsit, digues-ho al Javadoc. BigDecimal és l'exemple canònic del JDK: new BigDecimal("1.0").equals(new BigDecimal("1.00")) és false però compareTo retorna 0, i per això un TreeSet<BigDecimal> es comporta diferent d'un HashSet<BigDecimal>.

  1. Comparator: ordre extern i múltiple

Comparator defineix un ordre des de fora de la classe, i resol les dues limitacions de Comparable: en pots tenir tants com vulguis, i funcionen amb classes que no controles.

public interface Comparator<T> {
    int compare(T a, T b);
}

És una interfície funcional (04-06), així que admet les tres formes que ja coneixes:

// 1. Classe anonima: la forma classica (04-04)
Comparator<Material> perTitol = new Comparator<Material>() {
    @Override public int compare(Material a, Material b) {
        return a.getTitol().compareTo(b.getTitol());
    }
};

// 2. Lambda (04-05)
Comparator<Material> perTitol2 = (a, b) -> a.getTitol().compareTo(b.getTitol());

// 3. Comparator.comparing amb referencia a metode: la forma moderna (04-06)
Comparator<Material> perTitol3 = Comparator.comparing(Material::getTitol);

Les tres fan el mateix. Fes servir sempre la tercera: és més curta, més llegible i menys propensa a errors, perquè no escrius la comparació a mà.

  1. El catàleg de mètodes de Comparator

Reprenem i completem el de 04-06.

Construcció

Comparator.comparing(Material::getTitol)                     // per una clau Comparable
Comparator.comparingInt(Prestec::getDiaPrestec)              // clau int: SENSE autoboxing
Comparator.comparingLong(Registre::getMarcaTemps)            // clau long
Comparator.comparingDouble(Material::getTarifaDiaria)        // clau double
Comparator.naturalOrder()                                    // l'ordre natural del tipus
Comparator.reverseOrder()                                    // l'ordre natural invertit

Per què comparingInt i no comparing. Comparator.comparing(Material::getDiesPrestec) funciona, però l'extractor retorna int i la signatura espera un Comparable, així que cada crida fa autoboxing: crea un Integer per comparació. Ordenar un milió d'elements implica de l'ordre de vint milions de comparacions, i per tant quaranta milions d'objectes temporals. comparingInt accepta una ToIntFunction i compara primitius directament.

Regla: si la clau és int, long o double, fes servir la variant especialitzada.

Composició

Comparator<Material> criteri =
    Comparator.comparing(Material::getTipus)                  // primer per tipus
              .thenComparing(Material::getTitol)              // a igualtat, per titol
              .thenComparingDouble(Material::getTarifaDiaria); // i despres per tarifa

thenComparing només es consulta quan l'anterior retorna 0. És exactament el mecanisme del desempat de l'apartat 4, escrit de manera declarativa.

I té variants especialitzades per la mateixa raó que comparing: thenComparingInt, thenComparingLong, thenComparingDouble.

Inversió

Comparator<Material> carPrimer = Comparator.comparingDouble(Material::getTarifaDiaria)
                                           .reversed();

Compte amb on col·loques reversed(): inverteix tot el que s'ha compost fins a aquell punt, no només l'últim.

// Inverteix TOTS DOS criteris: tipus descendent i, a igualtat, titol descendent
Comparator.comparing(Material::getTipus)
          .thenComparing(Material::getTitol)
          .reversed();

// Inverteix NOMES el titol: tipus ascendent, titol descendent
Comparator.comparing(Material::getTipus)
          .thenComparing(Material::getTitol, Comparator.reverseOrder());

La segona forma —thenComparing(extractor, comparadorDeLaClau)— és la que necessites quan cada criteri porta la seva pròpia direcció. És un error molt freqüent i silenciós.

Nuls

Comparator<Material> segur  = Comparator.nullsFirst(Comparator.comparing(Material::getTitol));
Comparator<Material> alFinal = Comparator.nullsLast(Comparator.comparing(Material::getTitol));

nullsFirst i nullsLast embolcallen un comparador perquè accepti elements null, col·locant-los al principi o al final. Sense ells, un sol null a la llista llança NullPointerException enmig del sort.

I si el que pot ser null és la clau, no l'element:

Comparator<Material> perAutor = Comparator.comparing(
    m -> ((Llibre) m).getAutor(),
    Comparator.nullsLast(Comparator.naturalOrder()));

Taula resum

Mètode Què fa
comparing(f) Ordena per la clau que extreu f
comparingInt/Long/Double(f) Igual, sense autoboxing. Fes-los servir si la clau és primitiva
thenComparing(f) Desempat per una altra clau
thenComparing(f, cmp) Desempat amb la seva pròpia direcció
reversed() Inverteix tot l'acumulat fins allà
naturalOrder() / reverseOrder() L'ordre natural del tipus, directe o invertit
nullsFirst(cmp) / nullsLast(cmp) Tolera elements null

Els comparadors d'ús freqüent convé declarar-los com a constants:

public final class OrdresMaterial {
    public static final Comparator<Material> PER_TITOL =
        Comparator.comparing(Material::getTitol);
    public static final Comparator<Material> PER_TIPUS_I_TITOL =
        Comparator.comparing(Material::getTipus).thenComparing(Material::getTitol);
    public static final Comparator<Material> PER_TARIFA_DESC =
        Comparator.comparingDouble(Material::getTarifaDiaria).reversed()
                  .thenComparing(Material::getReferencia);   // desempat per a ordre total
    private OrdresMaterial() { }
}

cataleg.ordenar(OrdresMaterial.PER_TIPUS_I_TITOL);

A més de llegible, evita crear un comparador nou a cada crida.

  1. Les quatre estratègies d'ordenació

Estratègia Com Cost Quan
List.sort(cmp) Al lloc, sobre la llista O(n log n) Per defecte amb llistes
Collections.sort(llista) Al lloc, ordre natural O(n log n) Compatibilitat; prefereix List.sort
Arrays.sort(array) Al lloc, sobre un array O(n log n) Quan treballes amb arrays
Col·lecció ordenada Es manté sola O(log n) per inserció Quan l'ordre ha d'estar sempre
// 1. List.sort: el metode propi de la interficie. L'OPCIO PER DEFECTE
cataleg.sort(Comparator.comparing(Material::getTitol));
cataleg.sort(null);                                     // ordre natural

// 2. Collections.sort: anterior a Java 8, fa el mateix
Collections.sort(fitxes);                               // ordre natural
Collections.sort(cataleg, Comparator.comparing(Material::getTitol));

// 3. Arrays.sort
Material[] array = cataleg.toArray(new Material[0]);
Arrays.sort(array, Comparator.comparing(Material::getTitol));
Arrays.sort(array, 0, 10, comparador);                  // nomes un rang

// 4. Colleccions que es mantenen ordenades
TreeSet<Fitxa> sempreOrdenades = new TreeSet<>();        // per compareTo
TreeMap<String, Material> perClau = new TreeMap<>();     // claus ordenades
PriorityQueue<Prestec> perUrgencia = new PriorityQueue<>(comparador);  // nomes el cap

El criteri d'elecció

La pregunta decisiva és quantes vegades necessitaràs l'ordre:

Situació Estratègia
Ordenar una vegada per mostrar List.sort
Ordenar per criteris diferents segons el moment List.sort amb diversos comparadors
Les dades han d'estar sempre ordenades i es consulten sovint TreeSet / TreeMap
Consultes per rang (headSet, subMap, floorKey) TreeSet / TreeMap
Només necessites "el següent més prioritari", mai la llista completa PriorityQueue
Insereixes moltes vegades i ordenes poques ArrayList + sort al final
Insereixes poques vegades i consultes ordenat moltes TreeSet

Una anàlisi de costos per a n insercions:

  • ArrayList + sort al final: n insercions O(1) + una ordenació O(n log n) = O(n log n) total.
  • TreeSet: n insercions O(log n) = O(n log n) total.

Són la mateixa complexitat, però les constants afavoreixen clarament l'ArrayList: un sort sobre memòria contigua és molt més ràpid que n insercions en un arbre amb nodes dispersos. Si només necessites l'ordre al final, ArrayList + sort guanya. El TreeSet guanya quan l'ordre ha d'estar disponible entre insercions, o quan necessites les seves operacions de rang.

  1. Estabilitat i ordenació per criteris successius

Una ordenació és estable si els elements que empaten conserven el seu ordre relatiu original.

List<Fitxa> fitxes = new ArrayList<>(List.of(
    new Fitxa("Refactoritzacio",    "Martin Fowler", 1999),
    new Fitxa("Java Eficac",        "Joshua Bloch",  2018),
    new Fitxa("Patrons de Disseny", "Erich Gamma",   1994),
    new Fitxa("Codi Net",           "Robert Martin", 2008)));

// Primer per autor
fitxes.sort(Comparator.comparing(Fitxa::autor));
// Despres per any
fitxes.sort(Comparator.comparingInt(Fitxa::any));

Amb una ordenació estable, després del segon sort les fitxes del mateix any continuen ordenades per autor. Amb una d'inestable, aquest ordre s'hauria perdut.

Java garanteix que Collections.sort, List.sort i Arrays.sort sobre objectes són estables. Sobre primitius (int[], double[]) no, i tant se val: dos int iguals són indistingibles, així que l'estabilitat no té sentit.

Aquesta garantia permet la tècnica d'ordenació per passades successives: ordenar pel criteri menys important primer i pel més important al final.

// Estrategia A: passades successives (del criteri MENYS important al MES important)
cataleg.sort(Comparator.comparing(Material::getTitol));       // secundari
cataleg.sort(Comparator.comparing(Material::getTipus));       // primari

// Estrategia B: un comparador compost
cataleg.sort(Comparator.comparing(Material::getTipus)
                       .thenComparing(Material::getTitol));

Totes dues donen el mateix resultat, però prefereix sempre la B: fa una sola passada en lloc de dues, expressa la intenció de manera directa i no depèn que el lector recordi que l'ordre de les passades és invers a la prioritat.

L'estratègia A continua sent útil en un cas: interfícies interactives. Quan l'usuari prem "ordena per tipus" sobre una taula ja ordenada per títol, l'estabilitat fa que el resultat sigui "per tipus i, dins de cada tipus, per títol" —just el que espera— sense que el programa hagi de recordar el criteri anterior.

  1. Quin algorisme fa servir Java realment

Java fa servir dos algorismes diferents, i l'elecció revela una decisió de disseny interessant.

TimSort, per a objectes

Collections.sort, List.sort i Arrays.sort(Object[]) fan servir TimSort, un algorisme híbrid creat per Tim Peters per a Python i adoptat per Java 7.

La seva idea central: les dades reals rarament estan completament desordenades. Solen contenir trams ja ordenats —llistes parcialment actualitzades, dades que arriben gairebé en ordre, resultats d'una ordenació prèvia—. TimSort detecta aquests trams, anomenats runs, els estén i els fusiona.

  1. Recorre l'array buscant trams ja ordenats (ascendents o descendents; els descendents els inverteix).
  2. Si un tram és curt, l'estén amb ordenació per inserció, que és molt ràpida en trams petits.
  3. Fusiona els trams per parelles, com en l'ordenació per mescla (merge sort).
Cas Complexitat
Millor cas (ja ordenat) O(n)
Cas mitjà O(n log n)
Pitjor cas O(n log n)
Memòria addicional O(n)
Estable

Aquest O(n) en el millor cas és la seva gran virtut: reordenar una llista gairebé ordenada és pràcticament gratis.

TimSort és també qui llança el famós IllegalArgumentException: Comparison method violates its general contract!. No és cap caprici: en fusionar trams, l'algorisme detecta que les comparacions són incoherents i prefereix fallar a produir un resultat incorrecte. Si veus aquest error, el teu comparador viola el contracte de l'apartat 2.

Quicksort de doble pivot, per a primitius

Arrays.sort(int[]), Arrays.sort(double[]) i altres fan servir dual-pivot quicksort, una variant de quicksort amb dos pivots que divideix l'array en tres parts en lloc de dues.

Cas Complexitat
Millor i mitjà O(n log n)
Pitjor cas O(n²) (amb entrades adverses, molt improbable)
Memòria addicional O(log n)
Estable No

Per què dos algorismes

Objectes (TimSort) Primitius (quicksort)
Cost de comparar Alt: crida a compareTo/compare Baix: una instrucció de CPU
Cost de moure Alt: moure referències, tocar memòria cau Baix: moure un valor
Importa l'estabilitat? : dos objectes "iguals" són distingibles No: dos int iguals són idèntics
Memòria addicional Acceptable Es prefereix mínima
Elecció TimSort: minimitza comparacions, estable Quicksort: al lloc, mínima memòria

Amb objectes, cada comparació pot ser cara i l'estabilitat importa: TimSort minimitza comparacions aprofitant l'ordre preexistent. Amb primitius, comparar és trivial, l'estabilitat no significa res i el que interessa és no gastar memòria: quicksort ordena al lloc.

A la pràctica no necessites triar: Java ho fa per tu. Però conèixer-ho explica dues coses que sí que veuràs: per què reordenar una llista gairebé ordenada és tan ràpid, i d'on surt aquesta IllegalArgumentException.

  1. Cerca: lineal, binària i per clau

Tres estratègies, tres complexitats i un criteri clar.

Cerca lineal

Recórrer comparant fins a trobar. És el que fan contains, indexOf i qualsevol bucle propi.

boolean conte = cataleg.contains(material);          // O(n), usa equals
int posicio = cataleg.indexOf(material);             // O(n)

Material trobat = null;                              // cerca per criteri
for (Material m : cataleg) {
    if (m.getReferencia().equals("978-0000000001")) { trobat = m; break; }
}
Avantatges Inconvenients
No requereix ordre previ O(n)
Funciona amb qualsevol criteri Es degrada amb la mida
Sense memòria addicional

Cerca binària

Sobre una col·lecció ordenada: mirar l'element central, descartar la meitat que no el pot contenir i repetir.

List<Fitxa> fitxes = new ArrayList<>(...);
fitxes.sort(null);                                   // OBLIGATORI ordenar primer

int pos = Collections.binarySearch(fitxes, buscada);
int pos2 = Collections.binarySearch(cataleg, sonda,
                                    Comparator.comparing(Material::getReferencia));

int pos3 = Arrays.binarySearch(array, buscat);

Amb un milió d'elements, una vintena de comparacions en lloc d'un milió.

Avantatges Inconvenients
O(log n) Exigeix ordre previ pel mateix criteri
Retorna el punt d'inserció si no hi és Ordenar costa O(n log n)
Sense memòria addicional Sobre LinkedList és O(n) per l'accés indexat

Aquest últim punt mereix atenció: Collections.binarySearch comprova si la llista implementa RandomAccess (05-03). Si no —cas de LinkedList—, canvia a una estratègia amb iterador, i el resultat és pitjor que una cerca lineal. Cerca binària només sobre ArrayList o arrays.

Cerca per clau

Un Map o un Set basat en hash, com vas veure a 05-05 i 05-06.

Map<String, Material> index = new HashMap<>();
Material m = index.get("978-0000000001");            // O(1)
boolean existeix = referencies.contains("978-0000000001");  // O(1)
Avantatges Inconvenients
O(1) Memòria addicional per a l'índex
No requereix ordre Requereix equals/hashCode correctes
Escala perfectament Només serveix per a la clau indexada

Taula de decisió

Situació Estratègia Cost
Cerques una vegada, col·lecció petita Lineal O(n)
Cerques per un criteri arbitrari i canviant Lineal O(n)
Cerques moltes vegades per la mateixa clau Map/Set O(1)
Necessites ordre i cerques TreeMap/TreeSet O(log n)
La llista ja està ordenada per aquest criteri binarySearch O(log n)
Cerques rangs ("tots entre A i B") TreeSet.subSet O(log n) + mida del rang

I la regla pràctica: si cercaràs més d'unes poques vegades per la mateixa clau, construeix un índex. El cost de crear el HashMap (O(n), una vegada) s'amortitza gairebé immediatament, com vas veure a 05-06.

  1. binarySearch i el seu valor negatiu

Collections.binarySearch i Arrays.binarySearch retornen:

  • Si el troben: l'índex de l'element, un valor ≥ 0.
  • Si no el troben: -(punt d'inserció) - 1, un valor negatiu.

El punt d'inserció és la posició on caldria inserir l'element per mantenir l'ordre.

List<Integer> ordenada = new ArrayList<>(List.of(10, 20, 30, 40, 50));

System.out.println(Collections.binarySearch(ordenada, 30));   //  2  (es a l'index 2)
System.out.println(Collections.binarySearch(ordenada, 35));   // -4  (aniria a l'index 3)
System.out.println(Collections.binarySearch(ordenada, 5));    // -1  (aniria a l'index 0)
System.out.println(Collections.binarySearch(ordenada, 99));   // -6  (aniria a l'index 5)

Per què aquesta fórmula tan estranya

Perquè l'índex 0 és un resultat vàlid de "trobat", i -0 és 0: no hi hauria manera de distingir "és a la posició 0" de "aniria a la posició 0". Restar u desplaça tots els negatius i elimina l'ambigüitat.

Per recuperar el punt d'inserció:

int resultat = Collections.binarySearch(ordenada, 35);
if (resultat >= 0) {
    System.out.println("Trobat a la posicio " + resultat);
} else {
    int puntInsercio = -resultat - 1;                    // -(-4) - 1 = 3
    System.out.println("No hi es; aniria a la posicio " + puntInsercio);
    ordenada.add(puntInsercio, 35);                      // insercio ordenada
}

Aquest idioma —cercar, i si no hi és inserir al punt retornat— és la forma estàndard de mantenir una llista ordenada sense reordenar-la cada vegada.

Les dues condicions imprescindibles

1. La col·lecció ha d'estar ordenada. Sobre una de desordenada, el resultat és brossa, sense cap avís.

List<Integer> desordenada = new ArrayList<>(List.of(30, 10, 50, 20, 40));
System.out.println(Collections.binarySearch(desordenada, 50));   // -3, i el 50 hi es

2. Ha d'estar ordenada pel MATEIX criteri amb què cerques. Si ordenes per títol i cerques amb un comparador per referència, el resultat no té sentit.

cataleg.sort(Comparator.comparing(Material::getTitol));

// MALAMENT: ordenat per titol, buscat per referencia
Collections.binarySearch(cataleg, sonda, Comparator.comparing(Material::getReferencia));

// BE: el mateix comparador en tots dos
Comparator<Material> perTitol = Comparator.comparing(Material::getTitol);
cataleg.sort(perTitol);
Collections.binarySearch(cataleg, sonda, perTitol);

Un detall pràctic: binarySearch necessita un element amb què comparar, no un valor de clau solt. Per cercar "el material amb referència X" cal un element sonda, una instància fictícia amb aquella referència. És incòmode, i és una raó més per preferir un Map quan cerques per clau.

I un advertiment final: si hi ha elements duplicats segons el criteri, binarySearch en retorna un qualsevol, no necessàriament el primer.

  1. Utilitats de Collections

java.util.Collections és la classe d'utilitats del Framework, germana d'Arrays. Aquestes són les que queden per conèixer:

List<Fitxa> fitxes = new ArrayList<>(...);

// Extrems, amb ordre natural o amb Comparator
Fitxa primera = Collections.min(fitxes);
Fitxa ultima  = Collections.max(fitxes);
Material car  = Collections.max(cataleg, Comparator.comparingDouble(Material::getTarifaDiaria));

// Comptar aparicions (usa equals)
int quants = Collections.frequency(cataleg, javaEficac);

// Modificar la llista al lloc
Collections.reverse(fitxes);           // inverteix l'ordre. O(n)
Collections.shuffle(fitxes);           // barreja aleatoriament. O(n)
Collections.swap(fitxes, 0, 3);        // intercanvia dues posicions. O(1)
Collections.rotate(fitxes, 2);         // desplaca 2 posicions circularment
Collections.fill(fitxes, plantilla);   // omple tot amb el mateix element

// Crear
List<String> repes = Collections.nCopies(3, "pendent");     // llista IMMUTABLE de 3 iguals
Collections.addAll(cataleg, m1, m2, m3);                    // afegir-ne diversos de cop

// Consultar
boolean senseRelacio = Collections.disjoint(ambPrestec, ambRetard);  // sense elements comuns

// Vistes i colleccions especials (05-02)
List<Material> nomesLectura = Collections.unmodifiableList(cataleg);
List<Material> buida = Collections.emptyList();
List<Material> un = Collections.singletonList(javaEficac);
Mètode Què fa Cost
min(c) / max(c) Extrems segons ordre natural o Comparator O(n)
frequency(c, o) Quantes vegades apareix, fent servir equals O(n)
reverse(l) Inverteix la llista al lloc O(n)
shuffle(l) Barreja aleatòriament O(n)
swap(l, i, j) Intercanvia dues posicions O(1)
rotate(l, d) Desplaça circularment O(n)
nCopies(n, o) Llista immutable de n còpies O(1)
disjoint(a, b) No comparteixen cap element? O(n)
addAll(c, e...) Afegeix diversos elements solts O(n)

Dues observacions útils. min i max són O(n) i no requereixen ordre previ: per trobar el màxim una sola vegada, són millors que ordenar. I shuffle accepta un Random amb llavor (Collections.shuffle(llista, new Random(42))), cosa que fa la barreja reproduïble, imprescindible per a tests.

  1. Rendiment: ordenar una vegada, cercar moltes

El compromís central d'aquesta lliçó, amb números.

Suposem un catàleg de 100 000 materials sobre el qual fem 10 000 cerques per referència.

Estratègia Cost de preparació Cost per cerca Total (operacions)
Lineal, sense preparar 0 50 000 de mitjana 500 000 000
Ordenar + binària ~1 700 000 (n log n) ~17 (log n) ~1 870 000
Índex HashMap 100 000 (n) 1 110 000

La lliçó és doble. Preparar les dades compensa moltíssim tan bon punt hi hagi diverses cerques: ordenar costa el mateix que 1,7 cerques lineals i estalvia la resta. I l'índex hash guanya la cerca binària quan cerques per igualtat exacta.

Quan triar llavors la cerca binària sobre un HashMap?

  • Quan necessites l'ordre a més de la cerca.
  • Quan cerques per rang, no per valor exacte ("tots els publicats entre 1990 i 2000").
  • Quan la memòria és crítica: un ArrayList ordenat ocupa molt menys que un HashMap.
  • Quan la clau no té un bon hashCode.

I el punt d'equilibri, per orientar-se:

Cerques previstes Estratègia recomanada
1-2 Lineal: preparar no compensa
3-100 Ordenar + binària, o índex
Més de 100 Índex HashMap
Qualsevol nombre, però també necessites ordre TreeMap

I l'advertiment de sempre: aquestes xifres són d'operacions, no de temps. Per a 200 elements, totes les estratègies són instantànies i has de triar per claredat. L'optimització comença a importar a partir de desenes de milers.

  1. Tancament del mòdul: l'estat de BiblioTech

Reunim el projecte complet amb tot el que hem après al mòdul.

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import com.nexussoftware.bibliotech.domini.*;

/** BiblioTech al tancament del modul 5: totes les estructures al seu lloc. */
public class BiblioTech {

    // --- Cataleg: llista per a l'ordre, mapes per als indexs ---
    private final List<Material>              cataleg       = new ArrayList<>();
    private final Map<String, Material>       perReferencia = new HashMap<>();
    private final Map<String, List<Material>> perTipus      = new HashMap<>();
    private final Set<String>                 isbnVistos    = new HashSet<>();

    // --- Prestecs ---
    private final Map<String, Prestec>          prestecs   = new HashMap<>();
    private final Map<Empleat, List<Prestec>>   perEmpleat = new HashMap<>();

    // --- Reserves: una cua FIFO per material ---
    private final Map<String, Deque<Reserva>> reserves = new HashMap<>();

    // --- Historial reversible ---
    private final Deque<OperacioCataleg> desfer = new ArrayDeque<>();

    // --- Comparadors reutilitzables ---
    public static final Comparator<Material> PER_TITOL =
        Comparator.comparing(Material::getTitol);
    public static final Comparator<Material> PER_TIPUS_I_TITOL =
        Comparator.comparing(Material::getTipus).thenComparing(Material::getTitol);
    public static final Comparator<Material> PER_TARIFA_DESC =
        Comparator.comparingDouble(Material::getTarifaDiaria).reversed()
                  .thenComparing(Material::getReferencia);      // desempat: ordre TOTAL

    // ---------- Cataleg ----------

    public boolean donarDeAlta(Material m, int dia) {
        if (m == null || !isbnVistos.add(m.getReferencia())) { return false; }
        cataleg.add(m);
        perReferencia.put(m.getReferencia(), m);
        perTipus.computeIfAbsent(m.getTipus(), t -> new ArrayList<>()).add(m);
        desfer.push(new OperacioCataleg(OperacioCataleg.Tipus.ALTA, m, dia));
        return true;
    }

    public boolean donarDeBaixa(String referencia, int dia) {
        Material m = perReferencia.remove(referencia);
        if (m == null) { return false; }
        isbnVistos.remove(referencia);
        cataleg.remove(m);
        perTipus.computeIfPresent(m.getTipus(),
                (t, llista) -> { llista.remove(m); return llista.isEmpty() ? null : llista; });
        desfer.push(new OperacioCataleg(OperacioCataleg.Tipus.BAIXA, m, dia));
        return true;
    }

    /** Cerca per clau: O(1). */
    public Material cercar(String referencia) { return perReferencia.get(referencia); }

    /** Cerca per criteri arbitrari: O(n), inevitable. */
    public List<Material> cercar(Predicate<Material> criteri) {
        List<Material> resultat = new ArrayList<>();
        for (Material m : cataleg) {
            if (criteri.test(m)) { resultat.add(m); }
        }
        return resultat;
    }

    public List<Material> llistar(Comparator<Material> criteri) {
        List<Material> copia = new ArrayList<>(cataleg);
        copia.sort(criteri);                        // TimSort, O(n log n), estable
        return copia;
    }

    // ---------- Prestecs ----------

    public boolean prestar(String referencia, Empleat empleat, int dia) {
        Material m = perReferencia.get(referencia);
        if (m == null || !m.estaDisponible() || !empleat.potPrendrePrestat()) {
            return false;
        }
        Prestec p = new Prestec(m, empleat, dia);
        prestecs.put(p.getReferencia(), p);
        perEmpleat.computeIfAbsent(empleat, e -> new ArrayList<>()).add(p);
        m.prestar();
        empleat.registrarPrestec();
        return true;
    }

    public boolean retornar(String referenciaPrestec, int dia) {
        Prestec p = prestecs.get(referenciaPrestec);          // O(1)
        if (p == null || p.estaRetornat()) { return false; }
        p.registrarDevolucio(dia);
        p.getEmpleat().registrarDevolucio();

        // Atendre la primera reserva del material, si n'hi ha
        Deque<Reserva> cua = reserves.get(p.getMaterial().getReferencia());
        if (cua != null) {
            Reserva seguent = cua.pollFirst();
            if (seguent != null) {
                seguent.marcarAtesa();
                System.out.printf("AVIS: %s pot recollir '%s'%n",
                        seguent.getEmpleat().getNom(), p.getMaterial().getTitol());
            }
        }
        return true;
    }

    public void reservar(String referencia, Empleat empleat, int dia) {
        Material m = perReferencia.get(referencia);
        if (m == null) { return; }
        reserves.computeIfAbsent(referencia, r -> new ArrayDeque<>())
                .offerLast(new Reserva(empleat, m, dia));
    }

    // ---------- Informes ----------

    /** Comptadors per tipus, en un TreeMap perque surtin sempre ordenats. */
    public Map<String, Integer> informePerTipus() {
        Map<String, Integer> resum = new TreeMap<>();
        for (Material m : cataleg) { resum.merge(m.getTipus(), 1, Integer::sum); }
        return resum;
    }

    /** Rànquing d'empleats per multa acumulada, de major a menor. */
    public Map<Empleat, Double> rankingMultes(int diaActual) {
        Map<Empleat, Double> multes = new HashMap<>();
        for (List<Prestec> llista : perEmpleat.values()) {
            for (Prestec p : llista) {
                double multa = p.calcularMulta(diaActual);
                if (multa > 0) { multes.merge(p.getEmpleat(), multa, Double::sum); }
            }
        }
        // Un mapa NO s'ordena per valor: cal abocar, ordenar i reconstruir
        List<Map.Entry<Empleat, Double>> entrades = new ArrayList<>(multes.entrySet());
        entrades.sort(Map.Entry.<Empleat, Double>comparingByValue().reversed());

        Map<Empleat, Double> ordenat = new LinkedHashMap<>();   // conserva l'ordre
        for (Map.Entry<Empleat, Double> e : entrades) {
            ordenat.put(e.getKey(), e.getValue());
        }
        return ordenat;
    }

    /** Els N prestecs mes urgents, amb PriorityQueue. */
    public List<Prestec> mesUrgents(int quants, int diaActual) {
        Queue<Prestec> cua = new PriorityQueue<>(
            Comparator.comparingDouble((Prestec p) -> p.calcularMulta(diaActual)).reversed()
                      .thenComparing(Prestec::getReferencia));
        for (Prestec p : prestecs.values()) {
            if (!p.estaRetornat() && p.estaVencut(diaActual)) { cua.offer(p); }
        }
        List<Prestec> resultat = new ArrayList<>();
        for (int i = 0; i < quants && !cua.isEmpty(); i++) {
            resultat.add(cua.poll());                // poll: l'UNIC ordre garantit
        }
        return resultat;
    }

    /** El material mes car: min/max son O(n), no cal ordenar. */
    public Material mesCar() {
        return cataleg.isEmpty() ? null
             : Collections.max(cataleg, Comparator.comparingDouble(Material::getTarifaDiaria));
    }

    public OperacioCataleg ultimaOperacio() { return desfer.peek(); }
    public int midaCataleg() { return cataleg.size(); }
}

I una sessió completa:

BiblioTech app = new BiblioTech();

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

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

System.out.println("=== Cataleg per tipus i titol ===");
app.llistar(BiblioTech.PER_TIPUS_I_TITOL)
   .forEach(m -> System.out.printf("  %-8s %-26s %.2f EUR/dia%n",
           m.getTipus(), m.getTitol(), m.getTarifaDiaria()));

app.prestar("978-0000000001", marta, 100);
app.prestar("978-0000000003", marta, 102);
app.prestar("DVD-0007",       diego, 105);
app.reservar("978-0000000001", nuria, 106);       // Java Eficac esta prestat

System.out.println("\n=== Informe per tipus ===");
app.informePerTipus().forEach((t, n) -> System.out.printf("  %-8s %d%n", t, n));

System.out.println("\n=== Prestecs mes urgents (dia 140) ===");
app.mesUrgents(3, 140).forEach(p -> System.out.printf("  %-16s %-26s %6.2f EUR%n",
        p.getEmpleat().getNom(), p.getMaterial().getTitol(), p.calcularMulta(140)));

System.out.println("\n=== Ranquing de multes (dia 140) ===");
app.rankingMultes(140).forEach((e, multa) ->
        System.out.printf("  %-16s %6.2f EUR%n", e.getNom(), multa));

System.out.println("\n=== Devolucio amb reserva pendent ===");
app.retornar("PR-0001", 140);

System.out.println("\nMaterial mes car: " + app.mesCar().getTitol());
System.out.println("Ultima operacio:  " + app.ultimaOperacio());
=== Cataleg per tipus i titol ===
  DVD      Refactoritzacio en directe 0,50 EUR/dia
  Llibre   Java Eficac                0,25 EUR/dia
  Llibre   Patrons de Disseny         0,25 EUR/dia
  Llibre   Refactoritzacio            0,25 EUR/dia
  Revista  Java Magazine              0,10 EUR/dia

=== Informe per tipus ===
  DVD      1
  Llibre   3
  Revista  1

=== Prestecs mes urgents (dia 140) ===
  Diego Alonso     Refactoritzacio en directe  16,00 EUR
  Marta Ruiz       Java Eficac                  6,25 EUR
  Marta Ruiz       Refactoritzacio              5,75 EUR

=== Ranquing de multes (dia 140) ===
  Diego Alonso      16,00 EUR
  Marta Ruiz        12,00 EUR

=== Devolucio amb reserva pendent ===
AVIS: Nuria Vidal pot recollir 'Java Eficac'

Material mes car: Refactoritzacio en directe
Ultima operacio: Alta de material de 'Refactoritzacio en directe' (DVD-0007) el dia 101

El balanç: què sap fer BiblioTech

Necessitat Estructura Cost
Catàleg ordenable i recorrible List<Material> (ArrayList) Recorregut O(n), accés O(1)
Cerca per referència Map<String, Material> O(1)
Agrupació per tipus Map<String, List<Material>> amb computeIfAbsent O(1)
Rebuig d'ISBN duplicats Set<String> amb el boolean d'add O(1)
Préstecs per referència Map<String, Prestec> O(1)
Préstecs per empleat Map<Empleat, List<Prestec>> O(1)
Cua de reserves per material Map<String, Deque<Reserva>> (ArrayDeque) O(1) als extrems
Avisos per urgència PriorityQueue<Prestec> O(log n)
Historial de desfer Deque<OperacioCataleg> (ArrayDeque) O(1)
Informes ordenats TreeMap / LinkedHashMap O(log n) / O(1)
Ordenació per qualsevol criteri Comparator compostos O(n log n)

I aquell informePerTipus de vint línies amb bucles imbricats del mòdul 4 s'ha quedat, literalment, en tres.

Què continua sent fràgil

I ara la part honesta. Mira qualsevol mètode del projecte amb ull crític i hi veuràs la mateixa debilitat:

1. Qualsevol dada invàlida trenca el programa. Integer.parseInt("abc") llança NumberFormatException i l'aplicació acaba. perReferencia.get(null) pot donar NullPointerException. cataleg.get(99) dona IndexOutOfBoundsException. pila.pop() sobre una pila buida, NoSuchElementException. 1 / 0, ArithmeticException. A tot el mòdul hem evitat acuradament aquestes situacions comprovant abans, però comprovar abans no sempre és possible ni suficient.

2. Els errors es comuniquen retornant null o false. cercar(referencia) retorna null si no hi és: és que no existeix, o és que la referència era invàlida? donarDeAlta retorna false: per duplicat, per material nul, per referència buida? El valor de retorn no pot transportar el motiu de la fallada, així que qui crida no pot reaccionar de manera diferent segons el cas.

3. Els avisos s'imprimeixen per consola. Tot el projecte és ple de System.out.println("AVIS: ..."). Això no és gestió d'errors: és un missatge que ningú no pot capturar, registrar ni tractar. Un servei que s'executi sense consola perd tota la informació.

4. Una fallada a mitges deixa el sistema incoherent. Si donarDeAlta afegeix al Set d'ISBN i falla abans d'afegir a la llista, l'ISBN queda marcat com a catalogat sense que existeixi el material. No hi ha manera de desfer parcialment una operació.

5. Res no es desa en sortir. Tot viu a la memòria. Tancar el programa esborra el catàleg, els préstecs, les reserves i l'historial. La propera execució comença de zero.

Els quatre primers punts són exactament el temari del mòdul 6. El cinquè, el del mòdul 7.

Errors Habituals i Consells

Restar per comparar. a - b desborda amb enters grans i trunca amb dobles. Fes servir Integer.compare, Double.compare, etc. Sense excepcions.

(int) (dobleA - dobleB). Totes les diferències menors que 1 es trunquen a 0: elements diferents es declaren iguals. Double.compare sempre.

Ordre no coherent amb equals en un TreeSet o TreeMap. Els elements que empaten segons el comparador es consideren el mateix, i el segon es descarta en silenci. Afegeix criteris de desempat fins que l'ordre sigui total.

Col·locar malament reversed(). Inverteix tot el que s'ha compost fins a aquell punt, no només l'últim criteri. Si cada criteri porta la seva direcció, fes servir thenComparing(extractor, Comparator.reverseOrder()).

Fer servir comparing amb claus primitives. Provoca autoboxing a cada comparació. comparingInt, comparingLong i comparingDouble existeixen precisament per a això.

binarySearch sobre una col·lecció desordenada. Retorna resultats sense sentit, sense cap avís. I ha d'estar ordenada pel mateix criteri amb què cerques.

Interpretar el negatiu de binarySearch com a "-1, no hi és". És -(punt d'inserció) - 1. Per obtenir el punt: -resultat - 1.

binarySearch sobre una LinkedList. L'accés indexat és O(n), així que la cerca "binària" acaba sent pitjor que la lineal. Només sobre ArrayList o arrays.

Ordenar per trobar el màxim. Collections.max és O(n); ordenar és O(n log n). Si només necessites l'extrem, no ordenis.

Ordenar una llista dins d'un bucle. És l'error de rendiment clàssic: ordena una vegada a fora, o fes servir un TreeSet si l'ordre ha d'estar sempre.

Ignorar IllegalArgumentException: Comparison method violates its general contract! No és una fallada de Java: és TimSort avisant-te que el teu comparador és incoherent. Repassa la transitivitat i l'antisimetria.

Consell: declara els comparadors freqüents com a constants. Són immutables i reutilitzables; crear-ne un de nou a cada crida és soroll.

Consell: per al "top N", aboca entrySet(), ordena i reconstrueix en un LinkedHashMap. Un mapa mai no s'ordena per valor, i només LinkedHashMap conserva l'ordre en què insereixes.

Consell: si cerques més d'unes poques vegades per la mateixa clau, construeix un índex. El HashMap s'amortitza gairebé immediatament.

Exercicis

Exercici 1: catàleg ordenable amb múltiples criteris

Escriu CatalegOrdenable amb un List<Material> intern i:

  • Una classe imbricada Ordres amb constants Comparator<Material> per a: per títol, per tipus i títol, per tarifa descendent, per disponibilitat i després títol, i per tipus i tarifa descendent.
  • List<Material> ordenatPer(Comparator<Material>): retorna una còpia ordenada, sense tocar l'original.
  • void ordenarAlLloc(Comparator<Material>).
  • Material maxim(Comparator<Material>) i Material minim(Comparator<Material>) amb Collections.
  • List<Material> topN(int n, Comparator<Material>).
  • int cercarBinari(String titol): ordena per títol si cal i interpreta correctament el valor negatiu, informant del punt d'inserció.

Tots els comparadors han de produir un ordre total (amb desempat per referència).

Exercici 2: demostració dels contractes trencats

Escriu DemostracioOrdre amb un main que demostri, imprimint i explicant:

  1. Que restar enters desborda: compara a - b amb Integer.compare(a, b) per a valors extrems.
  2. Que (int)(dobleA - dobleB) trunca les tarifes de BiblioTech i les declara totes iguals.
  3. Que un ordre incoherent amb equals fa que un TreeSet perdi elements, mentre que un HashSet no.
  4. Que l'ordenació de Java és estable: ordena per autor, després per any, i comprova que dins de cada any es conserva l'ordre per autor.
  5. Que reversed() inverteix tot l'acumulat, amb la forma correcta d'invertir un sol criteri.
  6. Que binarySearch sobre una llista desordenada retorna brossa.

Exercici 3: comparativa d'estratègies de cerca

Escriu ComparativaCerques que, amb 100 000 materials i 10 000 cerques per referència, mesuri:

  1. Cerca lineal amb for sobre un ArrayList.
  2. Ordenar una vegada + Collections.binarySearch amb element sonda.
  3. Construir un HashMap una vegada + get.
  4. Construir un TreeMap una vegada + get.

Imprimeix una taula amb el temps de preparació, el de les cerques i el total, i inclou l'advertiment sobre JMH. Escriu una conclusió raonada indicant quan triaries cada estratègia.

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;

public class CatalegOrdenable {

    private final List<Material> materials = new ArrayList<>();
    private Comparator<Material> ordreActual = null;   // recorda com esta ordenat

    /** Comparadors reutilitzables. TOTS acaben amb un desempat per referencia. */
    public static final class Ordres {
        private Ordres() { }

        public static final Comparator<Material> PER_TITOL =
            Comparator.comparing(Material::getTitol)
                      .thenComparing(Material::getReferencia);

        public static final Comparator<Material> PER_TIPUS_I_TITOL =
            Comparator.comparing(Material::getTipus)
                      .thenComparing(Material::getTitol)
                      .thenComparing(Material::getReferencia);

        // comparingDouble: sense autoboxing. reversed() abans del desempat,
        // perque la referencia continui ordenant-se ASCENDENT.
        public static final Comparator<Material> PER_TARIFA_DESC =
            Comparator.comparingDouble(Material::getTarifaDiaria).reversed()
                      .thenComparing(Material::getReferencia);

        // Els disponibles primer: false < true, aixi que cal invertir el boolea
        public static final Comparator<Material> PER_DISPONIBILITAT =
            Comparator.comparing((Material m) -> !m.estaDisponible())
                      .thenComparing(Material::getTitol)
                      .thenComparing(Material::getReferencia);

        // Tipus ASCENDENT, tarifa DESCENDENT: cada criteri amb la seva direccio.
        // Un .reversed() al final invertiria TAMBE el tipus: error classic.
        public static final Comparator<Material> PER_TIPUS_I_TARIFA_DESC =
            Comparator.comparing(Material::getTipus)
                      .thenComparing(Material::getTarifaDiaria, Comparator.reverseOrder())
                      .thenComparing(Material::getReferencia);
    }

    public void afegir(Material m) {
        if (m != null) { materials.add(m); ordreActual = null; }    // es perd l'ordre
    }

    /** Copia ordenada: el cataleg original conserva el seu ordre. */
    public List<Material> ordenatPer(Comparator<Material> criteri) {
        List<Material> copia = new ArrayList<>(materials);
        copia.sort(criteri);
        return copia;
    }

    public void ordenarAlLloc(Comparator<Material> criteri) {
        materials.sort(criteri);
        ordreActual = criteri;
    }

    /** max/min son O(n): molt millor que ordenar (O(n log n)) per a un sol extrem. */
    public Material maxim(Comparator<Material> criteri) {
        return materials.isEmpty() ? null : Collections.max(materials, criteri);
    }

    public Material minim(Comparator<Material> criteri) {
        return materials.isEmpty() ? null : Collections.min(materials, criteri);
    }

    public List<Material> topN(int n, Comparator<Material> criteri) {
        List<Material> ordenat = ordenatPer(criteri);
        return new ArrayList<>(ordenat.subList(0, Math.min(n, ordenat.size())));
    }

    /**
     * Cerca binaria per titol. Ordena nomes si cal i fa servir
     * el MATEIX comparador per ordenar i per cercar: requisit imprescindible.
     */
    public int cercarBinari(String titol) {
        if (titol == null) { return -1; }

        Comparator<Material> criteri = Ordres.PER_TITOL;
        if (ordreActual != criteri) {
            ordenarAlLloc(criteri);           // O(n log n), pero nomes la primera vegada
        }

        // binarySearch necessita un ELEMENT, no una clau solta: cal una sonda.
        // Es un inconvenient real, i una rao mes per preferir un Map (05-05).
        Material sonda = new Llibre(titol, "", "", 2000);
        int resultat = Collections.binarySearch(materials, sonda,
                Comparator.comparing(Material::getTitol));    // nomes el titol: la sonda
                                                              // no te referencia valida

        if (resultat >= 0) {
            System.out.printf("'%s' trobat a la posicio %d%n", titol, resultat);
        } else {
            int puntInsercio = -resultat - 1;         // la formula: -(pos) - 1
            System.out.printf("'%s' no hi es; aniria a la posicio %d%n", titol, puntInsercio);
        }
        return resultat;
    }

    public int mida() { return materials.size(); }
}

Prova:

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

System.out.println("--- Per tipus i tarifa descendent ---");
c.ordenatPer(CatalegOrdenable.Ordres.PER_TIPUS_I_TARIFA_DESC)
 .forEach(m -> System.out.printf("  %-8s %-26s %.2f%n",
         m.getTipus(), m.getTitol(), m.getTarifaDiaria()));

System.out.println("Mes car: " + c.maxim(
        Comparator.comparingDouble(Material::getTarifaDiaria)).getTitol());

System.out.println("--- Top 2 per tarifa ---");
c.topN(2, CatalegOrdenable.Ordres.PER_TARIFA_DESC)
 .forEach(m -> System.out.println("  " + m.getTitol()));

c.cercarBinari("Java Eficac");
c.cercarBinari("Codi Net");

Tres decisions importants. Tots els comparadors acaben en thenComparing(Material::getReferencia), que és única, de manera que l'ordre és total i aquests comparadors es poden fer servir sense risc en un TreeSet. A PER_TIPUS_I_TARIFA_DESC la direcció s'aplica a cada criteri per separat amb thenComparing(extractor, reverseOrder()), perquè un .reversed() al final hauria invertit també el tipus. I cercarBinari recorda l'ordre actual per no reordenar a cada crida, encara que el detall de la sonda deixa clar per què, per cercar per clau, un Map és gairebé sempre millor.

Solució 2

package com.nexussoftware.bibliotech.presentacio;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class DemostracioOrdre {

    record FitxaSimple(String titol, String autor, int any) { }

    /** Ordre natural INCOHERENT amb equals: nomes mira l'any. */
    record FitxaPerAny(String titol, String autor, int any)
            implements Comparable<FitxaPerAny> {
        @Override public int compareTo(FitxaPerAny o) { return Integer.compare(any, o.any); }
    }

    public static void main(String[] args) {
        desbordament();
        truncament();
        incoherencia();
        estabilitat();
        reversedMalCollocat();
        binarySearchDesordenat();
    }

    static void desbordament() {
        System.out.println("=== 1. Restar enters DESBORDA ===");
        int a = 2_000_000_000, b = -2_000_000_000;

        System.out.println("a = " + a + ", b = " + b + "  -> a es clarament MES GRAN");
        System.out.println("a - b               = " + (a - b) + "   <- NEGATIU: diu a < b");
        System.out.println("Integer.compare(a,b)= " + Integer.compare(a, b) + "   <- correcte");
        System.out.println("Causa: 4.000.000.000 no cap en un int (max 2.147.483.647),");
        System.out.println("el valor fa la volta i canvia de signe. No restis mai.\n");
    }

    static void truncament() {
        System.out.println("=== 2. Restar dobles i fer casting TRUNCA ===");
        double llibre = 0.25, revista = 0.10, dvd = 0.50;

        System.out.println("(int)(dvd - revista)      = " + (int)(dvd - revista)
                           + "   <- 0.40 truncat a 0: 'son iguals'");
        System.out.println("(int)(llibre - revista)   = " + (int)(llibre - revista) + "   <- idem");
        System.out.println("Double.compare(dvd,revista)= " + Double.compare(dvd, revista));
        System.out.println("Amb aquest bug, TOTES les tarifes de BiblioTech serien iguals");
        System.out.println("i el cataleg no s'ordenaria en absolut.\n");
    }

    static void incoherencia() {
        System.out.println("=== 3. Ordre incoherent amb equals ===");
        FitxaPerAny a = new FitxaPerAny("Java Eficac",        "Joshua Bloch", 2018);
        FitxaPerAny b = new FitxaPerAny("Patrons de Disseny", "Erich Gamma",  2018);

        System.out.println("a.equals(b):    " + a.equals(b)    + "   <- son DIFERENTS");
        System.out.println("a.compareTo(b): " + a.compareTo(b) + "   <- pero EMPATEN");

        System.out.println("En List:     " + new ArrayList<>(List.of(a, b)).size() + " elements");
        System.out.println("En HashSet:  " + new HashSet<>(List.of(a, b)).size()
                           + " elements   <- usa equals/hashCode: correcte");
        System.out.println("En TreeSet:  " + new TreeSet<>(List.of(a, b)).size()
                           + " elements   <- usa compareTo: SE'N PERD UNA");

        Set<FitxaPerAny> arreglat = new TreeSet<>(
            Comparator.comparingInt(FitxaPerAny::any)
                      .thenComparing(FitxaPerAny::titol));     // desempat
        arreglat.addAll(List.of(a, b));
        System.out.println("Amb desempat: " + arreglat.size() + " elements   <- arreglat\n");
    }

    static void estabilitat() {
        System.out.println("=== 4. L'ordenacio de Java es ESTABLE ===");
        List<FitxaSimple> fitxes = new ArrayList<>(List.of(
            new FitxaSimple("Refactoritzacio",    "Martin Fowler", 1999),
            new FitxaSimple("Codi Net",           "Robert Martin", 2008),
            new FitxaSimple("Java Eficac",        "Joshua Bloch",  2018),
            new FitxaSimple("Java Concurrent",    "Brian Goetz",   2018),
            new FitxaSimple("Patrons de Disseny", "Erich Gamma",   1999)));

        fitxes.sort(Comparator.comparing(FitxaSimple::autor));       // criteri SECUNDARI
        System.out.println("Despres d'ordenar per autor:");
        fitxes.forEach(f -> System.out.printf("  %-16s %d  %s%n", f.autor(), f.any(), f.titol()));

        fitxes.sort(Comparator.comparingInt(FitxaSimple::any));      // criteri PRIMARI
        System.out.println("Despres d'ordenar per any (dins de cada any, continua per autor):");
        fitxes.forEach(f -> System.out.printf("  %-16s %d  %s%n", f.autor(), f.any(), f.titol()));

        System.out.println("Prefereix el comparador compost: UNA passada i mes clar:");
        fitxes.sort(Comparator.comparingInt(FitxaSimple::any)
                              .thenComparing(FitxaSimple::autor));
        System.out.println();
    }

    static void reversedMalCollocat() {
        System.out.println("=== 5. On collocar reversed() ===");
        List<FitxaSimple> fitxes = new ArrayList<>(List.of(
            new FitxaSimple("Java Eficac",      "Joshua Bloch", 2018),
            new FitxaSimple("Java Concurrent",  "Brian Goetz",  2018),
            new FitxaSimple("Refactoritzacio",  "Martin Fowler", 1999)));

        List<FitxaSimple> malament = new ArrayList<>(fitxes);
        malament.sort(Comparator.comparingInt(FitxaSimple::any)
                                .thenComparing(FitxaSimple::autor)
                                .reversed());     // inverteix ANY I AUTOR
        System.out.println("Amb .reversed() al final (inverteix TOTS DOS):");
        malament.forEach(f -> System.out.printf("  %d %s%n", f.any(), f.autor()));

        List<FitxaSimple> be = new ArrayList<>(fitxes);
        be.sort(Comparator.comparingInt(FitxaSimple::any).reversed()
                          .thenComparing(FitxaSimple::autor));    // nomes l'any
        System.out.println("Amb .reversed() despres de l'any (nomes inverteix l'any):");
        be.forEach(f -> System.out.printf("  %d %s%n", f.any(), f.autor()));
        System.out.println();
    }

    static void binarySearchDesordenat() {
        System.out.println("=== 6. binarySearch sobre llista DESORDENADA ===");
        List<Integer> desordenada = new ArrayList<>(List.of(30, 10, 50, 20, 40));
        System.out.println("Llista: " + desordenada);
        System.out.println("binarySearch(50): " + java.util.Collections.binarySearch(desordenada, 50)
                           + "   <- el 50 ES a l'index 2, pero retorna un negatiu");

        List<Integer> ordenada = new ArrayList<>(desordenada);
        java.util.Collections.sort(ordenada);
        System.out.println("Ordenada: " + ordenada);
        System.out.println("binarySearch(50): " + java.util.Collections.binarySearch(ordenada, 50)
                           + "   <- correcte");
        System.out.println("binarySearch(35): " + java.util.Collections.binarySearch(ordenada, 35)
                           + "   <- -(punt d'insercio) - 1 = -(3) - 1");
        System.out.println("Punt d'insercio: "
                           + (-java.util.Collections.binarySearch(ordenada, 35) - 1));
    }
}

Els sis apartats comparteixen un tret: cap no llança una excepció. El desbordament retorna un número, el truncament retorna zero, el TreeSet perd un element sense protestar, el reversed() mal col·locat ordena "alguna cosa" i binarySearch retorna un negatiu plausible. Els errors d'ordenació i comparació són silenciosos, i per això convé conèixer els contractes: no hi ha compilador ni excepció que t'avisi.

Solució 3

package com.nexussoftware.bibliotech.presentacio;

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

/**
 * Compara quatre estrategies de cerca per referencia.
 *
 * ADVERTIMENT: no es un benchmark rigoros. El JIT compila sobre la marxa,
 * el recollidor pot intervenir i el compilador pot eliminar codi el resultat
 * del qual no es faci servir. Per mesurar de debo, JMH (05-04). Aquestes xifres
 * nomes serveixen per veure ORDRES DE MAGNITUD.
 */
public class ComparativaCerques {

    private static final int N       = 100_000;
    private static final int CERQUES = 10_000;
    private static long escut = 0;       // evita que el JIT elimini els bucles

    public static void main(String[] args) {
        List<Material> cataleg = generar(N);
        List<String>   claus   = clausAleatories(CERQUES);

        System.out.println("Escalfant la JVM...");
        for (int i = 0; i < 3; i++) {
            lineal(cataleg.subList(0, 1000), claus.subList(0, 100));
            ambHashMap(cataleg.subList(0, 1000), claus.subList(0, 100));
        }

        System.out.printf("%n=== %d materials, %d cerques ===%n", N, CERQUES);
        System.out.printf("%-24s %12s %12s %12s%n",
                          "Estrategia", "Preparar", "Cercar", "TOTAL");
        System.out.println("-".repeat(64));

        long[] r1 = lineal(cataleg, claus);
        long[] r2 = ambBinaria(cataleg, claus);
        long[] r3 = ambHashMap(cataleg, claus);
        long[] r4 = ambTreeMap(cataleg, claus);

        fila("1. Lineal (for + equals)",  r1);
        fila("2. Ordenar + binarySearch", r2);
        fila("3. HashMap",                r3);
        fila("4. TreeMap",                r4);

        System.out.println("\n(escut = " + escut + ", ignora'l)");
        conclusio();
    }

    private static void fila(String etiqueta, long[] t) {
        System.out.printf("%-24s %9d ms %9d ms %9d ms%n", etiqueta, t[0], t[1], t[0] + t[1]);
    }

    /** 1. Lineal: sense preparacio, O(n) per cerca. */
    private static long[] lineal(List<Material> cataleg, List<String> claus) {
        long t = System.nanoTime();
        int trobats = 0;
        for (String clau : claus) {
            for (Material m : cataleg) {
                if (m.getReferencia().equals(clau)) { trobats++; break; }
            }
        }
        escut += trobats;
        return new long[]{ 0, ms(t) };
    }

    /** 2. Ordenar una vegada O(n log n), despres O(log n) per cerca. */
    private static long[] ambBinaria(List<Material> cataleg, List<String> claus) {
        Comparator<Material> perReferencia = Comparator.comparing(Material::getReferencia);

        long t1 = System.nanoTime();
        List<Material> ordenat = new ArrayList<>(cataleg);
        ordenat.sort(perReferencia);
        long preparar = ms(t1);

        long t2 = System.nanoTime();
        int trobats = 0;
        for (String clau : claus) {
            // binarySearch necessita un ELEMENT: cal fabricar una sonda per cerca.
            // Aquest cost extra es real i penalitza aquesta estrategia.
            Material sonda = new Llibre("", "", clau, 2000);
            if (Collections.binarySearch(ordenat, sonda, perReferencia) >= 0) { trobats++; }
        }
        escut += trobats;
        return new long[]{ preparar, ms(t2) };
    }

    /** 3. Index hash: O(n) en construir, O(1) per cerca. */
    private static long[] ambHashMap(List<Material> cataleg, List<String> claus) {
        long t1 = System.nanoTime();
        Map<String, Material> index = new HashMap<>((int)(cataleg.size() / 0.75f) + 1);
        for (Material m : cataleg) { index.put(m.getReferencia(), m); }
        long preparar = ms(t1);

        long t2 = System.nanoTime();
        int trobats = 0;
        for (String clau : claus) {
            if (index.get(clau) != null) { trobats++; }
        }
        escut += trobats;
        return new long[]{ preparar, ms(t2) };
    }

    /** 4. Arbre: O(n log n) en construir, O(log n) per cerca, pero ORDENAT. */
    private static long[] ambTreeMap(List<Material> cataleg, List<String> claus) {
        long t1 = System.nanoTime();
        Map<String, Material> index = new TreeMap<>();
        for (Material m : cataleg) { index.put(m.getReferencia(), m); }
        long preparar = ms(t1);

        long t2 = System.nanoTime();
        int trobats = 0;
        for (String clau : claus) {
            if (index.get(clau) != null) { trobats++; }
        }
        escut += trobats;
        return new long[]{ preparar, ms(t2) };
    }

    private static List<Material> generar(int n) {
        List<Material> llista = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            llista.add(new Llibre("Titol " + i, "Autor " + (i % 500),
                                  String.format("978-%010d", i), 1990 + (i % 35)));
        }
        return llista;
    }

    private static List<String> clausAleatories(int quantes) {
        List<String> claus = new ArrayList<>(quantes);
        java.util.Random atzar = new java.util.Random(42);   // llavor fixa: reproduible
        for (int i = 0; i < quantes; i++) {
            claus.add(String.format("978-%010d", atzar.nextInt(N)));
        }
        return claus;
    }

    private static long ms(long iniciNano) {
        return (System.nanoTime() - iniciNano) / 1_000_000;
    }

    private static void conclusio() {
        System.out.println("""

            === Conclusio ===
            1. LINEAL: zero preparacio, pero cada cerca recorre mitja llista.
               Amb 10.000 cerques sobre 100.000 elements son uns 500 milions
               de comparacions. Nomes val per a 1-2 cerques o llistes petites.

            2. ORDENAR + BINARIA: la preparacio es la mes cara (O(n log n)) i cada
               cerca es O(log n), unes 17 comparacions. A mes cal fabricar
               un element SONDA per cerca, cosa que penalitza forca.
               Val la pena quan a mes necessites la llista ordenada.

            3. HASHMAP: preparacio O(n) i cerca O(1). Es la mes rapida amb
               diferencia i la resposta per defecte per cercar per clau exacta.
               Preu: memoria extra i necessitar equals/hashCode correctes.

            4. TREEMAP: preparacio O(n log n) i cerca O(log n). Mes lent que
               el HashMap, pero a canvi mante les claus ORDENADES i ofereix
               firstKey, headMap, floorKey i consultes per rang.

            CRITERI:
              - Cerques 1-2 vegades  ............. lineal
              - Cerques per clau exacta, moltes .. HashMap
              - Necessites ordre o rangs ......... TreeMap
              - La llista ja esta ordenada ....... binarySearch
            """);
    }
}

Sortida típica:

=== 100000 materials, 10000 cerques ===
Estrategia                   Preparar       Cercar        TOTAL
----------------------------------------------------------------
1. Lineal (for + equals)         0 ms      2840 ms      2840 ms
2. Ordenar + binarySearch        95 ms        42 ms       137 ms
3. HashMap                      18 ms         2 ms        20 ms
4. TreeMap                      78 ms         9 ms        87 ms

Els números confirmen l'anàlisi de l'apartat 13, amb una lliçó addicional: la preparació és gairebé sempre rendible. La lineal triga 2840 ms sense preparar res; el HashMap triga 20 ms incloent-hi la construcció de l'índex. Preparar costa 18 ms i n'estalvia 2820.

I hi ha un matís que les taules teòriques no mostren: l'estratègia 2 surt pitjor del que la seva O(log n) suggeriria, perquè fabrica un objecte sonda a cada cerca. Aquest cost no apareix a l'anàlisi asimptòtica i tanmateix domina el temps real. És un recordatori que l'O() descriu com escala una operació, no quant costa executar-la: per decidir de debò, cal mesurar.

Conclusió

Has tancat el mòdul amb les operacions que travessen totes les col·leccions. Saps que Comparable defineix l'ordre natural d'una classe mitjançant compareTo, el signe del qual —negatiu, zero, positiu— és l'única cosa que importa, i coneixes el seu contracte complet: antisimetria, transitivitat, consistència, coherència amb equals i el rebuig de null. Saps que trencar-lo produeix ordenacions incorrectes o l'IllegalArgumentException: Comparison method violates its general contract! que TimSort llança en detectar la incoherència.

Tens gravat l'error que més codi en producció conté: no restis mai per comparar. a - b desborda amb enters grans i retorna el signe contrari; (int)(dobleA - dobleB) trunca qualsevol diferència menor que 1 i declara iguals elements diferents. La resposta és sempre Integer.compare, Double.compare i els seus germans. I entens per què la coherència amb equals importa tant: TreeSet i TreeMap decideixen la unicitat amb compareTo, no amb equals, així que un ordre parcial perd elements en silenci. La solució és afegir desempats fins que l'ordre sigui total.

Domines Comparator en la seva forma moderna: comparing, les variants comparingInt/Long/Double que eviten l'autoboxing, thenComparing per als desempats, reversed() —amb l'advertiment que inverteix tot l'acumulat, i thenComparing(extractor, reverseOrder()) quan cada criteri necessita la seva direcció— i nullsFirst/nullsLast per tolerar elements absents. I saps declarar-los com a constants reutilitzables.

Coneixes les quatre estratègies d'ordenació i el criteri que decideix entre elles —quantes vegades necessites l'ordre—, amb la conclusió pràctica que ArrayList + sort guanya quan l'ordre només cal al final i TreeSet/TreeMap guanyen quan ha d'estar disponible entre insercions o hi ha consultes per rang. Entens l'estabilitat i per què permet ordenar per criteris successius —encara que un comparador compost sigui preferible—, i saps què hi ha a sota: TimSort per a objectes, híbrid, estable i O(n) quan les dades ja estan gairebé ordenades; quicksort de doble pivot per a primitius, al lloc i amb memòria mínima. I saps per què són dos algorismes diferents.

En cerca, tens les tres estratègies amb les seves complexitats i el seu criteri: lineal quan cerques poc o per criteris canviants; binària quan la col·lecció ja està ordenada pel mateix criteri —amb la interpretació exacta del negatiu com a -(punt d'inserció) - 1 i l'advertiment de no fer-la servir sobre LinkedList—; i per clau en un Map o Set quan cerques moltes vegades pel mateix. Has vist amb números que preparar les dades gairebé sempre compensa: construir un HashMap costa el mateix que unes poques cerques lineals i estalvia totes les altres. I coneixes les utilitats de Collections que quedaven: max, min, frequency, reverse, shuffle, swap, rotate, nCopies, disjoint i addAll.

BiblioTech, en tancar el mòdul 5, és un sistema de gestió de debò. El seu catàleg és una List<Material> ordenable per qualsevol Comparator, recolzada per un Map<String, Material> que troba qualsevol material per la seva referència en temps constant, un Map<String, List<Material>> que agrupa per tipus construït amb computeIfAbsent, i un Set<String> que rebutja ISBN duplicats recolzant-se només en el boolean que retorna add. Els préstecs estan indexats per referència i per empleat, les multes s'acumulen amb merge, les reserves de cada material esperen en un ArrayDeque FIFO que s'atén sol en retornar, els avisos surten d'una PriorityQueue que serveix primer el més urgent, i una Deque<OperacioCataleg> permet anul·lar l'última alta o baixa. Els informes per tipus i el rànquing de multes es generen en una passada. I aquell informePerTipus de vint línies amb bucles imbricats que es va prometre reduir a tres, s'ha quedat en tres. No queda ni una sola línia de gestió manual de memòria a tot el projecte.

I tanmateix, el projecte és fràgil d'una manera que ja no es pot continuar ignorant. Tot el mòdul ha esquivat acuradament el problema: cada mètode comprova abans d'actuar, retorna null o false quan alguna cosa va malament i imprimeix System.out.println("AVIS: ...") per a la resta. Però comprovar abans no sempre és possible. Un Integer.parseInt("vint") llança NumberFormatException i l'aplicació acaba. Un índex fora de rang, un pop() sobre una pila buida, una divisió per zero, un fitxer que no existeix: qualsevol d'ells atura el programa en sec. Un false retornat no diu per què ha fallat, així que qui crida no pot reaccionar de manera diferent segons el motiu. Un missatge imprès per consola no es pot capturar, registrar ni tractar. I si una operació falla a mitges —l'ISBN ja és al Set però el material encara no és a la llista—, el sistema queda incoherent sense manera de tornar enrere. A més, res d'això no sobreviu al tancament del programa: tot viu a la memòria i la propera execució comença de zero.

Al mòdul 6, Maneig d'excepcions, es resol el primer. Veuràs què és realment una excepció i com viatja per la pila de crides que vas conèixer a 05-08; la jerarquia ThrowableError/ExceptionRuntimeException i la distinció entre comprovades i no comprovades; el bloc try-catch amb múltiples captures i el seu ordre obligatori; throw per assenyalar una fallada i throws per declarar-la; excepcions personalitzades que diguin exactament què ha anat malament —MaterialNoTrobatException, ReferenciaDuplicadaException, LimitPrestecsExceditException— i transportin les dades de l'error en lloc d'un false mut; el bloc finally i el try-with-resources que allibera recursos per tu; i les estratègies professionals de maneig d'errors i logging, perquè els avisos de BiblioTech deixin de ser System.out.println i passin a ser registres que es puguin filtrar, arxivar i analitzar. En acabar-lo, BiblioTech deixarà de caure davant la primera dada inesperada i començarà a comportar-se com el programari que es posa en producció.

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