De totes les classes del Framework de Col·leccions, ArrayList és la que més escriuràs. En qualsevol projecte Java real, entre el 70 % i el 90 % de les col·leccions són ArrayList, i amb raó: combina l'accés instantani per posició d'un array amb el creixement automàtic d'una col·lecció, i el seu recorregut és el més ràpid de totes les implementacions de List. És la resposta per defecte quan necessites "una llista de coses", i només l'hauries de canviar quan tinguis un motiu concret i mesurat.

Precisament perquè la faràs servir tant, val la pena entendre-la per dins. En aquesta lliçó veuràs que un ArrayList és literalment el CatalegArray que vas escriure a 05-01 —un array intern més un comptador—, però escrit pels enginyers del JDK i afinat durant vint-i-cinc anys. Entendre aquest array intern ho explica gairebé tot de cop: per què get(i) és instantani, per què remove(0) és car, per què existeix un constructor que demana la capacitat, per què add és O(1) "amortitzat" encara que de vegades copiï un milió d'elements, i per què subList et pot sorprendre. En acabar, el catàleg de BiblioTech haurà deixat de ser un array per sempre.

Contingut

  1. Què és un ArrayList per dins
  2. Capacitat enfront de mida
  3. El redimensionament i el cost amortitzat
  4. El constructor amb capacitat inicial
  5. Crear i omplir una llista
  6. L'API completa, mètode a mètode
  7. remove(int) enfront de remove(Object): el parany clàssic
  8. subList és una vista
  9. Taula de complexitat de cada operació
  10. Recorregut i esborrat segur
  11. ArrayList enfront d'array
  12. Conversió array ↔ llista
  13. Llistes de llistes
  14. equals i hashCode d'una llista
  15. Refactorització de BiblioTech
  16. Errors Habituals i Consells
  17. Exercicis

  1. Què és un ArrayList per dins

Si obres el codi font de java.util.ArrayList al JDK, el primer que hi trobes és això (simplificat):

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess {

    private static final int DEFAULT_CAPACITY = 10;

    transient Object[] elementData;   // l'array intern on viuen els elements
    private int size;                 // quants elements hi ha REALMENT
}

Dos camps. Un array i un enter. És exactament el CatalegArray de 05-01: un Material[] materials i un int n. La diferència no és en la idea, sinó que aquí tota la gestió de capacitat, creixement i desplaçaments està escrita, provada i optimitzada per altres.

flowchart TB
    subgraph AL["objecte ArrayList"]
        S["size = 4"]
        E["elementData →"]
    end
    subgraph ARR["Object[] elementData (capacitat 10)"]
        direction LR
        C0["[0]"]
        C1["[1]"]
        C2["[2]"]
        C3["[3]"]
        C4["[4] null"]
        C5["[5] null"]
        C6["[6] null"]
        C7["[7] null"]
        C8["[8] null"]
        C9["[9] null"]
    end
    E --> ARR
    C0 --> L1["Llibre<br/>Java Eficac"]
    C1 --> L2["Llibre<br/>Patrons de Disseny"]
    C2 --> L3["Llibre<br/>Refactoritzacio"]
    C3 --> L4["Revista<br/>Java Magazine"]

D'aquesta estructura se'n dedueix tota la resta:

  • get(i) és O(1): és un accés directe elementData[i], igual que en un array.
  • add(e) al final és O(1): escriure a elementData[size] i incrementar size... llevat de quan l'array s'omple.
  • add(0, e) i remove(0) són O(n): cal desplaçar tots els elements següents.
  • contains(e) i indexOf(e) són O(n): cal recórrer comparant amb equals.
  • Recórrer és ràpidíssim: memòria contigua, la millor localitat de memòria cau possible (05-01).

Fixa't també en implements RandomAccess. És una interfície marcadora: no declara cap mètode, només diu "aquesta llista permet accés per índex en temps constant". Alguns algorismes del JDK, com ara Collections.binarySearch, consulten aquesta marca per triar estratègia. LinkedList no la porta.

  1. Capacitat enfront de mida

Aquesta distinció és l'origen de la meitat dels malentesos amb ArrayList, i ja la coneixes del CatalegArray:

Concepte Què és Com es consulta
Mida (size) Quants elements hi ha realment llista.size()
Capacitat Quants hi caben abans d'haver de créixer No es pot consultar des de l'API pública

Al diagrama anterior, la mida és 4 i la capacitat és 10: hi ha sis cel·les lliures, invisibles des de fora. I aquí hi ha la gran millora respecte als arrays: la capacitat és un detall intern que no et concerneix. Amb Material[] cataleg = new Material[10], cataleg.length valia 10 encara que només n'haguessis omplert 4, i era feina teva portar el compte. Amb ArrayList, size() diu sempre la veritat.

List<String> noms = new ArrayList<>();
System.out.println(noms.size());          // 0  (capacitat interna: 10 en quant afegeixis el primer)
noms.add("Marta Ruiz");
noms.add("Diego Alonso");
System.out.println(noms.size());          // 2

Un detall d'eficiència que va introduir Java 7: un new ArrayList<>() no reserva res en construir-se; apunta a un array compartit buit i només reserva les 10 posicions quan hi afegeixes el primer element. Així, crear milers de llistes que potser no es faran servir mai no costa memòria.

Existeix un mètode per retornar la capacitat sobrant al sistema:

ArrayList<Material> llista = new ArrayList<>(1000);
// ... nomes se n'omplen 30 ...
llista.trimToSize();     // redueix la capacitat interna a 30

Compte: trimToSize() només existeix a ArrayList, no a la interfície List, així que per cridar-lo hauries de declarar la variable com a ArrayList. És una de les poquíssimes excepcions legítimes a la regla d'or de 05-02, i només té sentit en llistes enormes que ja no creixeran.

  1. El redimensionament i el cost amortitzat

Què passa quan afegeixes l'element número 11 a una llista de capacitat 10? Exactament el que feia el teu CatalegArray:

// dins d'ArrayList, simplificat:
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);   // capacitat * 1.5
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

oldCapacity >> 1 és un desplaçament de bits a la dreta, és a dir, dividir entre dos. Així que la nova capacitat és 1,5 vegades l'anterior. I Arrays.copyOf crea un array nou i copia tots els elements: una operació O(n).

flowchart LR
    A["Array de capacitat 10<br/>size = 10, PLE"] --> B["add(element 11)"]
    B --> C["new Object[15]"]
    C --> D["copiar els 10 elements: O(n)"]
    D --> E["elementData apunta a l'array nou"]
    E --> F["l'array vell queda per al recollidor"]
    F --> G["escriure l'element 11<br/>size = 11"]

La seqüència de capacitats partint d'una llista buida és:

En afegir l'element núm. Capacitat abans Capacitat després Hi va haver còpia? Elements copiats
1 0 10 Reserva inicial 0
11 10 15 10
16 15 22 15
23 22 33 22
34 33 49 33
50 49 73 49
74 73 109 73

Per què add continua sent O(1)

Si algunes crides a add costen O(n), com pot dir la taula de 05-02 que add és O(1)? Perquè és O(1) amortitzat, i val la pena entendre la idea perquè apareix en moltes estructures de dades.

Amortitzat significa cost mitjà per operació al llarg d'una seqüència llarga, no cost d'una operació aïllada. Fes el compte: per arribar a 1000 elements, el nombre total d'elements copiats en tots els redimensionaments és d'aproximadament 2000 (la suma d'una sèrie geomètrica de raó 1,5 convergeix a unes dues vegades la mida final). És a dir, unes 2 còpies per element afegit, de mitjana, sense importar si n'afegeixes mil o un milió. Dues operacions per element és una constant, i una constant és O(1).

La clau és que l'array creix multiplicativament (×1,5), no additivament. Si creixés de 10 en 10, per arribar a 1000 elements hi hauria 100 redimensionaments que copiarien 10+20+30+...+990 ≈ 50 000 elements: 50 per element afegit, i creixent. Això sí que seria un problema, i seria O(n) amortitzat.

L'analogia útil: mudar-se de casa és caríssim, però si només et mudes quan dobles les teves pertinences, el cost mitjà per objecte que acumules és constant.

  1. El constructor amb capacitat inicial

Hi ha tres constructors, i saber quan fer servir el segon és un detall de professional:

List<Material> a = new ArrayList<>();                    // capacitat per defecte (10 en omplir)
List<Material> b = new ArrayList<>(500);                 // capacitat inicial 500
List<Material> c = new ArrayList<>(altraColleccio);      // copia amb la mida justa

Quan importa la capacitat inicial? Quan saps per endavant, encara que sigui aproximadament, quants elements hi posaràs i són molts.

// Carregar el cataleg complet: sabem que son uns 5000 materials
List<Material> cataleg = new ArrayList<>(5000);
for (String linia : liniesDelFitxer) {
    cataleg.add(convertir(linia));
}

Sense la capacitat inicial, arribar a 5000 elements hauria provocat uns 20 redimensionaments i unes 10 000 còpies de referències. Amb ella, zero. Per a 5000 elements la diferència és de mil·lisegons; per a uns quants milions, notable.

Quan NO importa? Gairebé sempre. Per a llistes de desenes o centenars d'elements, new ArrayList<>() és perfecte i afegir-hi un número màgic només embruta el codi. No caiguis en la microoptimització: fes servir el constructor amb capacitat quan la mida sigui gran i previsible, i el buit la resta de vegades.

El tercer constructor és el que faràs servir per a còpies defensives (03-07) i per convertir entre col·leccions:

List<Material> copiaIndependent = new ArrayList<>(cataleg);   // copia superficial modificable
List<String>   dUnSet           = new ArrayList<>(conjuntISBN);
List<String>   dUnaLlistaFixa   = new ArrayList<>(List.of("a", "b", "c"));  // ara si es modificable

  1. Crear i omplir una llista

import java.util.ArrayList;
import java.util.List;

List<Material> cataleg = new ArrayList<>();

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

I les formes abreujades quan ja tens els elements:

// Llista MODIFICABLE a partir d'elements solts
List<String> empleats = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal"));

// Llista IMMUTABLE (compte: add llancara UnsupportedOperationException)
List<String> fixos = List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal");

// Afegir-ne diversos de cop a una llista existent
cataleg.addAll(altresMaterials);
Collections.addAll(cataleg, material1, material2, material3);

L'idioma new ArrayList<>(List.of(...)) és el més pràctic per inicialitzar una llista modificable amb contingut: diu el que fa i ocupa una línia.

  1. L'API completa, mètode a mètode

Afegir

List<String> l = new ArrayList<>(List.of("A", "B", "C"));

l.add("D");                 // [A, B, C, D]   al final, O(1) amortitzat
l.add(1, "X");              // [A, X, B, C, D] a la posicio 1, O(n): desplaca la resta
l.addAll(List.of("E","F")); // [A, X, B, C, D, E, F]
l.addAll(0, List.of("Z"));  // [Z, A, X, B, C, D, E, F]

add(int, E) accepta índexs de 0 a size() (tots dos inclosos: size() significa "al final"). Fora d'aquest rang, IndexOutOfBoundsException — com tractar-la és el mòdul 6.

Llegir i escriure

String primer = l.get(0);           // O(1)
String ultim  = l.get(l.size()-1);  // l'idioma per a l'ultim element

String anterior = l.set(1, "Y");    // SUBSTITUEIX i retorna el que hi havia. O(1)

Confondre add(1, "Y") amb set(1, "Y") és un error freqüent: el primer insereix (la llista creix), el segon reemplaça (la llista manté la seva mida).

Eliminar

l.remove(0);                  // per INDEX, retorna l'element eliminat
l.remove("B");                // per OBJECTE, retorna boolean. Usa equals
l.removeIf(s -> s.isEmpty()); // per CRITERI, retorna boolean
l.clear();                    // buida la llista

Els tres primers són O(n) en el cas general: eliminar a la posició i obliga a desplaçar els size - i - 1 elements posteriors amb un System.arraycopy intern, exactament com feia el teu CatalegArray. Només remove(size()-1) és O(1).

Cercar

boolean conte = l.contains("C");     // O(n), usa equals
int pos       = l.indexOf("C");      // primera aparicio, o -1 si no hi es. O(n)
int ultima    = l.lastIndexOf("C");  // ultima aparicio

contains i indexOf depenen per complet d'equals. Això connecta directament amb 03-09: si la teva classe no el sobreescriu, hereta el d'Object, que compara referències, i un objecte "igual però diferent" no es trobarà mai:

class MaterialSenseEquals { /* no sobreescriu equals */ }

List<Fitxa> fitxes = new ArrayList<>();
fitxes.add(new Fitxa("Java Eficac", "Joshua Bloch", 2018));

// Fitxa es un record: equals generat, compara components
System.out.println(fitxes.contains(new Fitxa("Java Eficac", "Joshua Bloch", 2018)));  // true

// Si Fitxa fos una classe normal sense equals:
// System.out.println(...) -> false, encara que les dades siguin identiques

Regla pràctica: si una classe viurà en una col·lecció i s'hi buscarà, necessita equals (i hashCode). A 05-05 veuràs per què tots dos, sempre junts.

Transformar i ordenar

l.replaceAll(String::toUpperCase);              // aplica un UnaryOperator a cada element
l.sort(Comparator.naturalOrder());              // ordena al lloc
l.sort(Comparator.comparing(Material::getTitol));
l.forEach(System.out::println);                 // recorre aplicant un Consumer

Tots quatre van arribar a Java 8 i fan servir les interfícies funcionals de 04-06. replaceAll és especialment útil i poc conegut: modifica cada element al lloc, sense crear una altra llista.

List<Material> cataleg = ...;
cataleg.sort(Comparator.comparing(Material::getTipus)
                       .thenComparing(Material::getTitol));

Prefereix llista.sort(comparador) a Collections.sort(llista, comparador): és el mètode propi de la interfície i no necessita cap classe d'utilitats.

Consultar

int n         = l.size();
boolean buida = l.isEmpty();       // preferible a size() == 0: mes llegible

  1. remove(int) enfront de remove(Object): el parany clàssic

List té dos mètodes remove sobrecarregats:

E       remove(int index);       // elimina per POSICIO
boolean remove(Object o);        // elimina per VALOR

Amb la majoria dels tipus no hi ha ambigüitat, però amb List<Integer> n'hi ha, i produeix el bug més famós del Framework:

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

nombres.remove(1);                  // que elimina?
System.out.println(nombres);        // [10, 30, 40]  <- he eliminat la POSICIO 1, no el valor 1

La resolució de sobrecàrrega (03-03) prefereix sempre la coincidència exacta sense conversions: 1 és un int, així que crida remove(int index) sense autoboxing. Si la teva intenció era eliminar el valor 20, el resultat és correcte per casualitat; si volies eliminar el valor 1 d'una llista que comença per 10, has esborrat l'element equivocat en silenci.

Encara pitjor:

List<Integer> ids = new ArrayList<>(List.of(100, 200, 300));
ids.remove(100);         // IndexOutOfBoundsException: Index 100 out of bounds for length 3

Buscaves el valor 100 i has demanat la posició 100.

Les solucions

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

nombres.remove(Integer.valueOf(20));    // 1. explicit: per VALOR
nombres.remove((Integer) 30);           // 2. casting: per VALOR
nombres.remove(1);                      // 3. per POSICIO (queda clar que es el que vols)

Integer.valueOf(20) és la forma preferida per llegibilitat. I una regla general molt útil: desconfia de les sobrecàrregues quan facis servir col·leccions d'Integer. El mateix problema no passa amb List<String> ni amb List<Material>, perquè remove("B") no es pot confondre amb un índex.

Crida sobre List<Integer> Es resol com Efecte
llista.remove(2) remove(int) Elimina l'element de la posició 2
llista.remove(Integer.valueOf(2)) remove(Object) Elimina el valor 2
llista.remove((Integer) 2) remove(Object) Elimina el valor 2
llista.indexOf(2) Només hi ha una versió Busca el valor 2 (autoboxing)
llista.contains(2) Només hi ha una versió Busca el valor 2 (autoboxing)

Fixa't en les dues últimes files: contains i indexOf només accepten Object, així que allà l'autoboxing sí que passa i tot funciona com esperes. L'ambigüitat és exclusiva de remove.

  1. subList és una vista

subList(inici, fi) retorna la porció de la llista entre aquests índexs (inici inclòs, fi exclòs). Però no és una còpia: és una vista sobre la llista original, igual que Arrays.asList era una vista sobre l'array.

List<String> l = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> centre = l.subList(1, 4);     // [B, C, D]

centre.set(0, "X");
System.out.println(l);         // [A, X, C, D, E]   <- ha canviat l'ORIGINAL

centre.clear();
System.out.println(l);         // [A, E]            <- s'han esborrat de l'ORIGINAL

Aquest comportament és intencionat i molt potent —clear() sobre una subllista és l'idioma estàndard per eliminar un rang—, però cal conèixer-lo, perquè les sorpreses són de dos tipus:

1. Els canvis es propaguen en tots dos sentits. Modificar la vista modifica l'original i a l'inrevés.

2. Modificar l'original estructuralment invalida la vista. Si afegeixes o treus elements de la llista original directament, qualsevol ús posterior de la subllista llança ConcurrentModificationException, pel mateix mecanisme de modCount que vas veure a 05-02:

List<String> l = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> centre = l.subList(1, 4);
l.add("F");                          // modificacio estructural de l'original
System.out.println(centre);          // ConcurrentModificationException

Si el que vols és una còpia independent, embolcalla-la:

List<String> copiaDelCentre = new ArrayList<>(l.subList(1, 4));   // ara si que es independent

Usos legítims de subList tal qual:

// Paginar resultats sense copiar la llista sencera
int inici = pagina * perPagina;
int fi    = Math.min(inici + perPagina, resultats.size());
List<Material> paginaActual = resultats.subList(inici, fi);

// Esborrar un rang complet
cataleg.subList(0, 10).clear();           // elimina els 10 primers

// Ordenar nomes un tram
cataleg.subList(0, 5).sort(Comparator.comparing(Material::getTitol));

  1. Taula de complexitat de cada operació

Aquesta taula és la que et permetrà decidir amb criteri a 05-04:

Operació Complexitat Per què
get(i) O(1) Accés directe elementData[i]
set(i, e) O(1) Escriptura directa
add(e) (al final) O(1) amortitzat Escriure i sumar 1; ocasionalment copiar-ho tot
add(0, e) (al principi) O(n) Desplaçar tots els elements a la dreta
add(i, e) (al mig) O(n) Desplaçar els n - i posteriors
remove(size()-1) (últim) O(1) Posar a null i restar 1
remove(0) (primer) O(n) Desplaçar tots els posteriors a l'esquerra
remove(i) O(n) Desplaçar els n - i - 1 posteriors
remove(Object) O(n) Cercar (O(n)) + desplaçar (O(n))
contains(e) / indexOf(e) O(n) Recórrer comparant amb equals
size() / isEmpty() O(1) És un camp, no es compta res
clear() O(n) Posa totes les cel·les a null per no filtrar memòria
Recórrer amb for-each O(n), molt ràpid Memòria contigua: localitat de memòria cau òptima
sort(cmp) O(n log n) TimSort (05-09)
contains després de sort + binarySearch O(log n) Només si la mantens ordenada (05-09)

El missatge que cal endur-se: ArrayList és excel·lent pels índexs i pel final de la llista, i mediocre pel principi i pel mig. Si el teu programa fa llista.add(0, x) o llista.remove(0) dins d'un bucle sobre milers d'elements, estàs fent O(n²) sense adonar-te'n, i és el moment de mirar un ArrayDeque (05-07).

  1. Recorregut i esborrat segur

Les tres formes de recórrer, amb el seu criteri:

// 1. for-each: per defecte, quan nomes llegeixes
for (Material m : cataleg) {
    System.out.println(m.descriure());
}

// 2. for classic amb index: quan necessites la posicio o escrius amb set
for (int i = 0; i < cataleg.size(); i++) {
    System.out.printf("%2d. %s%n", i + 1, cataleg.get(i).getTitol());
}

// 3. forEach amb Consumer: quan ja tens l'accio com a referencia a metode
cataleg.forEach(System.out::println);

Un avís sobre l'opció 2: cataleg.get(i) és O(1) a ArrayList, però serà O(n) a LinkedList. Un bucle indexat sobre una LinkedList és O(n²) i és un dels errors de rendiment més habituals. El for-each és correcte en totes dues.

I l'esborrat durant el recorregut, reprenent el de 05-02 amb els idiomes concrets:

// CORRECTE I PREFERIT: una linia
cataleg.removeIf(m -> !m.estaDisponible());

// CORRECTE: iterador explicit, quan la logica es complexa
Iterator<Material> it = cataleg.iterator();
while (it.hasNext()) {
    Material m = it.next();
    if (!m.estaDisponible()) {
        registrarBaixa(m);       // efecte collateral abans d'esborrar
        it.remove();
    }
}

// CORRECTE: for classic CAP ENRERE, quan necessites l'index
for (int i = cataleg.size() - 1; i >= 0; i--) {
    if (!cataleg.get(i).estaDisponible()) {
        System.out.println("Baixa a la posicio " + i);
        cataleg.remove(i);
    }
}

// INCORRECTE: ConcurrentModificationException
for (Material m : cataleg) {
    if (!m.estaDisponible()) { cataleg.remove(m); }
}

Un detall d'eficiència poc conegut: removeIf sobre un ArrayList està implementat en una sola passada que marca els elements a eliminar i després compacta l'array de cop, així que és O(n). Un bucle que cridés remove(Object) n vegades seria O(n²). Una altra raó per preferir removeIf.

  1. ArrayList enfront d'array

Aspecte Material[] List<Material>
Mida Fixa en crear Creix i minva sola
Afegir / eliminar A mà amb copyOf / arraycopy add / remove
Mida real length és la capacitat; cal portar un comptador size() és la veritat
Accés per índex a[i] — el més ràpid possible get(i) — O(1), amb una crida de mètode
Primitius int[] sense embolcalls List<Integer> amb autoboxing
Memòria (1 M d'int) ~4 MB ~20 MB
Recorregut El més ràpid Molt ràpid (mateix array per dins)
Cercar per criteri Bucle a mà contains, indexOf, removeIf
Ordenar Arrays.sort llista.sort
Multidimensional int[][] natural List<List<Integer>>, més verbós
Seguretat de tipus Covariant: ArrayStoreException en execució Genèrics: error en compilació
API disponible La classe Arrays Tota la interfície List i Collections

Quan l'array continua sent millor:

  1. Primitius en quantitat. int[], double[], byte[]. Una memòria intermèdia de bytes d'E/S (mòdul 7) sempre és byte[].
  2. Mida fixa per naturalesa. Una matriu 3×12, un tauler d'escacs, els 256 valors d'una taula de conversió.
  3. Màxim rendiment en càlcul numèric, on la localitat de memòria cau i l'absència d'embolcalls marquen la diferència.
  4. Implementar estructures de dades, com fan ArrayList, HashMap i ArrayDeque per dins.
  5. APIs que l'exigeixen: main(String[] args), String.split, toArray.

Per a tota la resta, ArrayList.

  1. Conversió array ↔ llista

És una operació quotidiana i cada direcció té el seu parany.

De llista a array

List<Material> llista = new ArrayList<>(...);

Material[] array   = llista.toArray(new Material[0]);   // L'IDIOMA CORRECTE
Object[]   dolent  = llista.toArray();                  // perd el tipus: gairebe mai util

El new Material[0] no es malbarata: indica al mètode el tipus d'array que ha de crear. Com que l'array passat és massa petit, toArray en crea internament un de la mida correcta i retorna aquest. Sorprenentment, new Material[0] sol ser més ràpid que new Material[llista.size()] a les JVM modernes, perquè evita omplir de zeros un array que se sobreescriurà sencer. Fes servir sempre new T[0].

L'array resultant és independent: afegir a la llista després no el canvia. Però és una còpia superficial: els objectes són els mateixos.

D'array a llista

Tres formes, amb comportaments molt diferents:

Material[] array = { llibre1, llibre2, llibre3 };

// 1. VISTA de mida fixa recolzada en l'array (05-01)
List<Material> vista = Arrays.asList(array);
vista.set(0, altre);       // OK, i modifica array[0]
// vista.add(altre);       // UnsupportedOperationException

// 2. Llista MODIFICABLE i INDEPENDENT  <- el que vols gairebe sempre
List<Material> modificable = new ArrayList<>(Arrays.asList(array));
modificable.add(altre);    // OK, i no toca 'array'

// 3. Llista IMMUTABLE i independent (Java 9+)
List<Material> immutable = List.of(array);
// immutable.set(0, altre); // UnsupportedOperationException
Forma Modificable Reflecteix canvis de l'array? Quan fer-la servir
Arrays.asList(a) Només set Sí, en tots dos sentits Embolcallar un array per passar-lo a una API que demana List
new ArrayList<>(Arrays.asList(a)) Totalment No L'opció per defecte
List.of(a) No No Constants; rebutja null

I els dos paranys que ja coneixes de 05-01, que aquí tornen a mossegar:

int[] primitius = { 1, 2, 3 };
List<int[]> malament = Arrays.asList(primitius);  // UNA llista d'UN element (l'array sencer)
System.out.println(malament.size());              // 1

Integer[] embolcallats = { 1, 2, 3 };
List<Integer> be = Arrays.asList(embolcallats);   // 3 elements, com esperaves
System.out.println(be.size());                    // 3

Els genèrics només funcionen amb tipus referència (10-01), així que un int[] es pren com un únic objecte. Per passar d'int[] a List<Integer> sense Streams, el bucle explícit és la via:

List<Integer> llista = new ArrayList<>(primitius.length);
for (int n : primitius) { llista.add(n); }        // autoboxing a cada add

Al mòdul 10 veuràs que Arrays.stream(primitius).boxed().toList() fa el mateix en una expressió.

  1. Llistes de llistes

Com que una List pot contenir qualsevol tipus, pot contenir altres llistes. És l'equivalent flexible de l'array bidimensional de 05-01:

List<List<Material>> perTipus = new ArrayList<>();

List<Material> llibres = new ArrayList<>();
llibres.add(new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018));
llibres.add(new Llibre("Refactoritzacio", "Martin Fowler", "978-0000000003", 1999));

List<Material> revistes = new ArrayList<>();
revistes.add(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"));

perTipus.add(llibres);
perTipus.add(revistes);
perTipus.add(new ArrayList<>());     // els DVD, encara sense cap

// Acces: dos indexs, igual que en un array bidimensional
System.out.println(perTipus.get(0).get(1).getTitol());   // Refactoritzacio

// Recorregut imbricat
for (List<Material> grup : perTipus) {
    System.out.println("Grup de " + grup.size() + " materials:");
    for (Material m : grup) {
        System.out.println("   " + m.getTitol());
    }
}

L'avantatge enfront de Material[][] és que cada subllista creix pel seu compte, sense dimensionar res per endavant.

Però fixa't en la debilitat: l'índex 0 significa "llibres" per conveni, i aquest conveni no està escrit enlloc. És exactament el problema que resol un Map<String, List<Material>> de 05-05, on la clau diu explícitament què conté cada grup.

I un advertiment sobre la inicialització:

List<List<String>> matriu = new ArrayList<>();
for (int i = 0; i < 3; i++) {
    matriu.add(new ArrayList<>());       // cada fila necessita LA SEVA PROPIA llista
}
matriu.get(0).add("dada");               // ara si

Si t'oblides de crear cada subllista, matriu.get(0) retorna null i l'add llança NullPointerException. I si afegeixes tres vegades la mateixa llista (List<String> fila = new ArrayList<>(); matriu.add(fila); matriu.add(fila);), les tres files seran la mateixa per aliasing (03-02), i escriure en una les canvia totes.

  1. equals i hashCode d'una llista

AbstractList sí que sobreescriu equals i hashCode, a diferència dels arrays. Dues llistes són iguals si tenen els mateixos elements en el mateix ordre:

List<String> a = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
List<String> b = new LinkedList<>(List.of("Marta Ruiz", "Diego Alonso"));
List<String> c = new ArrayList<>(List.of("Diego Alonso", "Marta Ruiz"));

System.out.println(a.equals(b));      // true  <- la implementacio NO importa
System.out.println(a.equals(c));      // false <- l'ORDRE si que importa
System.out.println(a.hashCode() == b.hashCode());   // true

Que a.equals(b) sigui true sent b una LinkedList és deliberat: el contracte de List.equals compara contingut, no classe. És una diferència important amb els arrays, on equals compara referències i calia Arrays.equals.

El hashCode es calcula així, i explica que dues llistes iguals el tinguin igual:

int hash = 1;
for (E e : llista) {
    hash = 31 * hash + (e == null ? 0 : e.hashCode());
}

Conseqüència pràctica: el hashCode d'una llista depèn dels seus elements, així que canvia si la llista canvia. Fer servir una llista mutable com a clau d'un HashMap o com a element d'un HashSet és una recepta per al desastre; ho veuràs demostrat a 05-05. Si necessites una llista com a clau, fes servir-ne una d'immutable (List.of(...) o List.copyOf(...)).

I compte: perquè l'equals de la llista funcioni, els elements han de tenir equals correcte. Una List<Fitxa> compara bé perquè Fitxa és un record; una llista d'objectes sense equals compararia per referència element a element.

  1. Refactorització de BiblioTech

És el moment de convertir el catàleg definitivament. Compara el mètode cercarPerTipus en les seves dues versions.

Abans: amb arrays

/** Versio del modul 4, amb Material[] i comptador manual. */
public Material[] cercarPerTipus(String tipus) {
    Material[] resultat = new Material[n];       // cal reservar el MAXIM possible
    int trobats = 0;
    for (int i = 0; i < n; i++) {                // fins a n, no fins a materials.length
        if (materials[i] != null && materials[i].getTipus().equals(tipus)) {
            resultat[trobats] = materials[i];
            trobats++;
        }
    }
    return Arrays.copyOf(resultat, trobats);     // i retallar al final
}

Quatre problemes en onze línies: reservar de més, portar un comptador a part, recórrer només fins a n sense oblidar-ho, i retallar en retornar.

Després: amb List

/** Versio del modul 5. */
public List<Material> cercarPerTipus(String tipus) {
    List<Material> resultat = new ArrayList<>();
    for (Material m : materials) {
        if (m.getTipus().equals(tipus)) { resultat.add(m); }
    }
    return resultat;
}

I amb el Predicate de 04-06, un sol mètode serveix per a qualsevol criteri:

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;
}

El Cataleg complet

package com.nexussoftware.bibliotech.servei;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import com.nexussoftware.bibliotech.domini.Material;

/** Cataleg de BiblioTech sobre ArrayList. */
public class Cataleg {

    private final List<Material> materials;
    private final Set<String>    referencies = new HashSet<>();   // unicitat en O(1)

    public Cataleg() {
        this.materials = new ArrayList<>();
    }

    /** Capacitat inicial: util nomes quan carreguem un cataleg gran de cop. */
    public Cataleg(int materialsPrevistos) {
        this.materials = new ArrayList<>(Math.max(materialsPrevistos, 10));
    }

    public boolean afegir(Material m) {
        if (m == null) { return false; }
        if (!referencies.add(m.getReferencia())) {
            return false;                             // referencia duplicada
        }
        return materials.add(m);                      // sempre true en una List
    }

    /** Insereix en una posicio concreta. O(n): desplaca la resta. */
    public boolean inserir(int posicio, Material m) {
        if (m == null || posicio < 0 || posicio > materials.size()) { return false; }
        if (!referencies.add(m.getReferencia())) { return false; }
        materials.add(posicio, m);
        return true;
    }

    public boolean eliminar(String referencia) {
        if (!referencies.remove(referencia)) { return false; }
        return materials.removeIf(m -> m.getReferencia().equals(referencia));
    }

    /** Substitueix el material d'una posicio conservant la mida. */
    public Material reemplacar(int posicio, Material nou) {
        Material anterior = materials.set(posicio, nou);   // retorna el que hi havia
        referencies.remove(anterior.getReferencia());
        referencies.add(nou.getReferencia());
        return anterior;
    }

    public Material obtenir(int posicio)   { return materials.get(posicio); }    // O(1)
    public boolean  conte(Material m)      { return materials.contains(m); }     // O(n), usa equals
    public int      posicioDe(Material m)  { return materials.indexOf(m); }      // O(n)

    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 int comptar(Predicate<Material> criteri) {
        int n = 0;
        for (Material m : materials) {
            if (criteri.test(m)) { n++; }
        }
        return n;
    }

    public void ordenar(Comparator<Material> criteri) { materials.sort(criteri); }

    /** Pagina de resultats fent servir subList, amb els limits ben controlats. */
    public List<Material> pagina(int numeroPagina, int perPagina) {
        int inici = numeroPagina * perPagina;
        if (inici >= materials.size() || inici < 0) { return List.of(); }
        int fi = Math.min(inici + perPagina, materials.size());
        return new ArrayList<>(materials.subList(inici, fi));   // COPIA, no vista
    }

    /** Copia immutable: ningu de fora no pot alterar el cataleg. */
    public List<Material> llistar()  { return List.copyOf(materials); }

    /** Quan una API antiga demana un array. */
    public Material[] comArray()     { return materials.toArray(new Material[0]); }

    public int     mida()   { return materials.size(); }
    public boolean buit()   { return materials.isEmpty(); }
    public void    buidar() { materials.clear(); referencies.clear(); }
}

Ús complet:

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));

cataleg.ordenar(Comparator.comparing(Material::getTipus)
                          .thenComparing(Material::getTitol));

System.out.println("--- Cataleg (" + cataleg.mida() + " materials) ---");
cataleg.llistar().forEach(m -> System.out.println("  " + m.descriure()));

List<Material> barats = cataleg.cercar(m -> m.getTarifaDiaria() < 0.30);
System.out.println("Tarifa inferior a 0,30 EUR/dia: " + barats.size());

System.out.println("Pagina 0 (2 per pagina):");
cataleg.pagina(0, 2).forEach(m -> System.out.println("  " + m.getTitol()));
--- Cataleg (5 materials) ---
  [DVD] Refactoritzacio en directe (DVD-0007)
  [Llibre] Java Eficac - Joshua Bloch (978-0000000001)
  [Llibre] Patrons de Disseny - Erich Gamma (978-0000000002)
  [Llibre] Refactoritzacio - Martin Fowler (978-0000000003)
  [Revista] Java Magazine num. 42 (REV-2024-03)
Tarifa inferior a 0,30 EUR/dia: 4
Pagina 0 (2 per pagina):
  Refactoritzacio en directe
  Java Eficac

Comparat amb el CatalegArray de 05-01, ha desaparegut tota la gestió de memòria i han aparegut operacions que abans eren impensables: paginació, inserció posicional, reemplaçament, unicitat garantida. I continua quedant una debilitat, deliberada: eliminar i qualsevol cerca per referència recorren la llista sencera. Amb 5 materials tant se val; amb 50 000, no. Aquesta és la feina de 05-05.

Errors Habituals i Consells

llista.length en lloc de llista.size(). length és d'arrays, length() de String, size() de col·leccions.

Confondre add(i, e) amb set(i, e). El primer insereix i la llista creix; el segon reemplaça i la mida no canvia. Si la teva llista creix quan no tocava, mira aquí.

remove(int) en una List<Integer>. llista.remove(2) elimina la posició 2, no el valor 2. Fes servir remove(Integer.valueOf(2)) per eliminar per valor.

Modificar la llista dins d'un for-each. ConcurrentModificationException. Fes servir removeIf, Iterator.remove() o recorre cap enrere amb índexs.

Bucle indexat sobre una llista que podria no ser ArrayList. for (int i = 0; i < llista.size(); i++) llista.get(i) és O(n) a ArrayList i O(n²) a LinkedList. Si el paràmetre és List, recorre amb for-each.

llista.add(0, x) o llista.remove(0) dins d'un bucle. Cada crida desplaça tota la llista: O(n²) en total. Si necessites treballar pel principi, fes servir un ArrayDeque (05-07).

Creure que subList és una còpia. És una vista: modificar-la modifica l'original, i modificar l'original estructuralment la invalida amb ConcurrentModificationException. Si vols una còpia, new ArrayList<>(llista.subList(a, b)).

Esperar que Arrays.asList retorni una llista modificable. És de mida fixa: set sí, add/remove llancen UnsupportedOperationException. Fes servir new ArrayList<>(Arrays.asList(...)).

Arrays.asList sobre un int[]. Retorna una llista d'un element. Necessites Integer[] o un bucle explícit.

Cercar amb contains en una classe sense equals. No hi trobarà mai res, encara que les dades coincideixin. Repassa 03-09.

Retornar la llista interna des d'un getter. Qui cridi la podrà buidar. Retorna List.copyOf(materials) o, com a mínim, Collections.unmodifiableList(...) sabent que és una vista (05-02).

Consell: isEmpty() en lloc de size() == 0. Es llegeix millor i en algunes implementacions és més ràpid.

Consell: capacitat inicial només quan importi. Amb milers o milions d'elements previstos, new ArrayList<>(n) estalvia redimensionaments. Amb vint, és soroll.

Exercicis

Exercici 1: gestor de préstecs amb ArrayList

Escriu GestorPrestecs amb un List<Prestec> intern i aquests mètodes:

  • void registrar(Prestec p).
  • boolean retornar(String referencia, int dia): busca el préstec per la seva referència (PR-0001), invoca registrarDevolucio(dia) i retorna true si l'ha trobat.
  • List<Prestec> vencuts(int diaActual): els que estiguin vençuts i sense retornar.
  • int purgarRetornats(): elimina els ja retornats amb removeIf i retorna quants n'ha tret.
  • List<Prestec> ultims(int quants): els quants últims registrats, fent servir subList amb els límits ben controlats i retornant una còpia.
  • Prestec mesAntic(): el de diaPrestec més petit, o null si no n'hi ha cap.

Exercici 2: el parany de remove documentat

Escriu una classe DemostracioRemove amb un mètode main que:

  1. Creï un List<Integer> amb els valors 10, 20, 30, 40, 50.
  2. Mostri què passa amb remove(1), remove(Integer.valueOf(20)) i remove((Integer) 30), imprimint la llista després de cada operació.
  3. Mostri per què remove(100) llançaria IndexOutOfBoundsException (sense executar-lo: comenta-ho i explica-ho).
  4. Creï un List<String> equivalent i expliqui per què allà l'ambigüitat no existeix.
  5. Escrigui un mètode boolean eliminarValor(List<Integer> llista, int valor) segur i reutilitzable.

Exercici 3: conversions i els seus paranys

Escriu ConversorColleccions amb mètodes estàtics que demostrin, cadascun amb la seva sortida per consola:

  • void demostrarAsList(): que Arrays.asList és una vista de mida fixa recolzada en l'array (modifica per tots dos costats i mostra quines operacions fallen).
  • void demostrarCopiaIndependent(): que new ArrayList<>(Arrays.asList(a)) és independent.
  • void demostrarToArray(): la conversió de tornada amb toArray(new String[0]).
  • void demostrarPrimitius(): el parany d'Arrays.asList(int[]) enfront d'Arrays.asList(Integer[]).
  • List<Integer> aLlista(int[] primitius): la conversió correcta amb un bucle.

Solucions

Solució 1

package com.nexussoftware.bibliotech.servei;

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

public class GestorPrestecs {

    private final List<Prestec> prestecs = new ArrayList<>();

    public void registrar(Prestec p) {
        if (p != null) { prestecs.add(p); }         // add al final: O(1) amortitzat
    }

    /**
     * Cerca lineal O(n) per referencia. Amb molts prestecs caldria
     * indexar per referencia en un Map<String, Prestec>: aixo es 05-05.
     */
    public boolean retornar(String referencia, int dia) {
        if (referencia == null) { return false; }
        for (Prestec p : prestecs) {                // for-each: nomes llegim
            if (referencia.equals(p.getReferencia())) {
                p.registrarDevolucio(dia);          // modifiquem l'OBJECTE, no la llista: legal
                return true;
            }
        }
        return false;
    }

    public List<Prestec> vencuts(int diaActual) {
        List<Prestec> resultat = new ArrayList<>();
        for (Prestec p : prestecs) {
            if (!p.estaRetornat() && p.estaVencut(diaActual)) {
                resultat.add(p);
            }
        }
        return resultat;               // llista buida si no n'hi ha cap, MAI null
    }

    /**
     * removeIf fa una sola passada O(n). Un bucle amb remove(Object)
     * seria O(n^2) i a mes donaria ConcurrentModificationException.
     */
    public int purgarRetornats() {
        int abans = prestecs.size();
        prestecs.removeIf(Prestec::estaRetornat);   // referencia a metode (04-06)
        return abans - prestecs.size();
    }

    public List<Prestec> ultims(int quants) {
        if (quants <= 0 || prestecs.isEmpty()) { return List.of(); }
        int inici = Math.max(0, prestecs.size() - quants);   // proteccio del limit inferior
        // subList retorna una VISTA: la copiem perque sigui independent
        return new ArrayList<>(prestecs.subList(inici, prestecs.size()));
    }

    public Prestec mesAntic() {
        Prestec millor = null;
        for (Prestec p : prestecs) {
            if (millor == null || p.getDiaPrestec() < millor.getDiaPrestec()) {
                millor = p;
            }
        }
        return millor;     // null si la llista es buida; documenta-ho al javadoc
    }

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

Punts de disseny que cal retenir. retornar modifica l'objecte dins d'un for-each, cosa que és perfectament legal: modCount només compta canvis estructurals de la llista. purgarRetornats fa servir removeIf en lloc d'un bucle amb remove, passant de O(n²) a O(n) i evitant l'excepció. ultims protegeix el límit inferior amb Math.max i copia la vista de subList. I tots els mètodes que retornen col·leccions retornen una llista buida en lloc de null.

A 05-09 veuràs que mesAntic() s'escriu en una línia amb Collections.min(prestecs, Comparator.comparingInt(Prestec::getDiaPrestec)).

Solució 2

package com.nexussoftware.bibliotech.presentacio;

import java.util.ArrayList;
import java.util.List;

public class DemostracioRemove {

    public static void main(String[] args) {
        List<Integer> nombres = new ArrayList<>(List.of(10, 20, 30, 40, 50));
        System.out.println("Inicial:                          " + nombres);

        // 1) remove(int): el literal 1 es un int -> es tria remove(int index)
        //    sense autoboxing, perque la sobrecarrega prefereix la coincidencia exacta.
        nombres.remove(1);
        System.out.println("Despres de remove(1):             " + nombres + "  <- he esborrat la POSICIO 1");

        // 2) remove(Object): Integer.valueOf(30) es un Integer -> remove(Object)
        nombres.remove(Integer.valueOf(30));
        System.out.println("Despres de remove(valueOf(30)):   " + nombres + "  <- he esborrat el VALOR 30");

        // 3) El casting produeix el mateix efecte que valueOf, pero es llegeix pitjor
        nombres.remove((Integer) 40);
        System.out.println("Despres de remove((Integer) 40):  " + nombres + "  <- he esborrat el VALOR 40");

        // 4) La crida perillosa, comentada a proposit:
        // nombres.remove(100);
        //    -> IndexOutOfBoundsException: Index 100 out of bounds for length 2
        //    El programador volia esborrar el VALOR 100 i ha demanat la POSICIO 100.
        //    L'error es descobreix en execucio, no en compilar.

        // 5) Amb String no hi ha ambiguitat: "Marta Ruiz" no es un int,
        //    aixi que nomes es pot resoldre com a remove(Object).
        List<String> empleats = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
        empleats.remove("Marta Ruiz");
        System.out.println("Empleats:                         " + empleats);

        // 6) Metode segur i reutilitzable
        List<Integer> ids = new ArrayList<>(List.of(101, 205, 307));
        System.out.println("eliminarValor(205): " + eliminarValor(ids, 205) + " -> " + ids);
        System.out.println("eliminarValor(999): " + eliminarValor(ids, 999) + " -> " + ids);
    }

    /**
     * Elimina per VALOR sense cap ambiguitat possible. El parametre es int per comoditat
     * de qui crida, i l'embolcall explicit garanteix que s'invoqui
     * remove(Object) i no remove(int).
     */
    public static boolean eliminarValor(List<Integer> llista, int valor) {
        return llista.remove(Integer.valueOf(valor));
    }
}
Inicial:                          [10, 20, 30, 40, 50]
Despres de remove(1):             [10, 30, 40, 50]  <- he esborrat la POSICIO 1
Despres de remove(valueOf(30)):   [10, 40, 50]  <- he esborrat el VALOR 30
Despres de remove((Integer) 40):  [10, 50]  <- he esborrat el VALOR 40
Empleats:                         [Diego Alonso]
eliminarValor(205): true -> [101, 307]
eliminarValor(999): false -> [101, 307]

La lliçó de fons va més enllà de remove: quan dues sobrecàrregues es distingeixen només per primitiu enfront d'embolcall, la que guanya és la del primitiu, perquè la resolució de sobrecàrrega prefereix no aplicar boxing. És la mateixa regla de 03-03, ara amb conseqüències visibles.

Solució 3

package com.nexussoftware.bibliotech.presentacio;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public final class ConversorColleccions {

    private ConversorColleccions() { }

    public static void demostrarAsList() {
        System.out.println("--- Arrays.asList es una VISTA de mida fixa ---");
        String[] array = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
        List<String> vista = Arrays.asList(array);

        vista.set(0, "Marta R.");                        // permes
        System.out.println("array[0] despres de vista.set: " + array[0]);   // Marta R.

        array[1] = "Diego A.";                            // per l'altre costat
        System.out.println("vista despres de array[1]=:    " + vista);      // Diego A.

        // vista.add("Nou");      -> UnsupportedOperationException: mida FIXA
        // vista.remove(0);       -> UnsupportedOperationException
        System.out.println("add i remove llancarien UnsupportedOperationException");
    }

    public static void demostrarCopiaIndependent() {
        System.out.println("--- new ArrayList<>(Arrays.asList(a)) es INDEPENDENT ---");
        String[] array = { "Marta Ruiz", "Diego Alonso" };
        List<String> copia = new ArrayList<>(Arrays.asList(array));

        copia.add("Nuria Vidal");        // ara si que es pot
        copia.set(0, "Marta R.");

        System.out.println("copia:   " + copia);            // [Marta R., Diego Alonso, Nuria Vidal]
        System.out.println("array:   " + Arrays.toString(array));   // [Marta Ruiz, Diego Alonso]
        System.out.println("L'array original NO s'ha vist afectat");
    }

    public static void demostrarToArray() {
        System.out.println("--- De llista a array ---");
        List<String> llista = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));

        // new String[0] indica el TIPUS. toArray en crea internament un de la mida justa.
        String[] array = llista.toArray(new String[0]);
        System.out.println("array: " + Arrays.toString(array) + " (length " + array.length + ")");

        llista.add("Nuria Vidal");       // l'array ja generat no canvia
        System.out.println("despres d'afegir a la llista, array continua amb " + array.length);

        Object[] senseTipus = llista.toArray();    // perd el tipus: gairebe mai util
        System.out.println("toArray() sense argument retorna Object[]: " + senseTipus.length);
    }

    public static void demostrarPrimitius() {
        System.out.println("--- El parany dels primitius ---");
        int[] primitius = { 1, 2, 3 };
        List<int[]> malament = Arrays.asList(primitius);
        System.out.println("Arrays.asList(int[]).size()     = " + malament.size()
                           + "   <- UNA llista d'UN element: l'array sencer");

        Integer[] embolcallats = { 1, 2, 3 };
        List<Integer> be = Arrays.asList(embolcallats);
        System.out.println("Arrays.asList(Integer[]).size() = " + be.size()
                           + "   <- tres elements, com esperaves");
        System.out.println("Causa: els generics nomes admeten tipus referencia (10-01)");
    }

    /** Conversio correcta d'int[] a List<Integer>. */
    public static List<Integer> aLlista(int[] primitius) {
        if (primitius == null) { return List.of(); }
        // capacitat inicial exacta: sabem quants n'entraran
        List<Integer> llista = new ArrayList<>(primitius.length);
        for (int n : primitius) {
            llista.add(n);                // autoboxing: int -> Integer a cada add
        }
        return llista;
    }

    public static void main(String[] args) {
        demostrarAsList();
        demostrarCopiaIndependent();
        demostrarToArray();
        demostrarPrimitius();
        System.out.println("aLlista(new int[]{7,8,9}) = " + aLlista(new int[] { 7, 8, 9 }));
    }
}

La conclusió operativa de l'exercici cap en una regla: Arrays.asList només per embolcallar un array de només lectura que cal passar a una API que demana List; new ArrayList<>(Arrays.asList(...)) per a tota la resta; toArray(new T[0]) per a la tornada. I amb primitius, sempre un bucle o —a partir del mòdul 10— Arrays.stream(...).boxed().

Conclusió

Ja coneixes per dins la col·lecció que més faràs servir. Saps que un ArrayList és un array intern més un int size: exactament el CatalegArray que vas escriure tu, però afinat pel JDK. D'aquesta estructura se'n deriva tot el seu perfil de rendiment: get i set en O(1), add al final en O(1) amortitzat, i O(n) per inserir o eliminar en qualsevol altre lloc perquè cal desplaçar la resta amb un System.arraycopy intern.

Distingeixes capacitat de mida, i saps que la capacitat és un detall intern que no pots consultar i que gairebé mai no has de gestionar. Entens el redimensionament: l'array creix a ×1,5 amb una còpia O(n), i precisament perquè creix de manera multiplicativa el cost mitjà per element es manté constant — això és el cost amortitzat, una idea que reapareixerà amb HashMap i ArrayDeque. I saps quan el constructor amb capacitat inicial val la pena: amb milers o milions d'elements previstos, no amb vint.

Domines l'API completa: add en les seves dues formes, get, set enfront d'add(i, e), remove en les seves tres variants, indexOf, contains i la seva dependència total d'equals —el fil que ve de 03-09—, sort, replaceAll, removeIf i forEach amb les interfícies funcionals de 04-06. Coneixes el parany de remove(int) enfront de remove(Object) en una List<Integer> i les tres formes de desactivar-lo, i saps que subList és una vista: modificar-la canvia l'original, modificar l'original la invalida, i per a una còpia cal embolcallar-la en un new ArrayList<>(...).

Saps recórrer i esborrar amb seguretat —removeIf per defecte, Iterator.remove() per a lògica complexa, for cap enrere si necessites l'índex—, tens la taula que compara ArrayList amb array i els cinc casos en què l'array continua guanyant, i manegues les conversions en tots dos sentits amb els paranys d'Arrays.asList i dels primitius. Coneixes les llistes de llistes i la seva debilitat —l'índex que significa alguna cosa per conveni no escrit— i saps que dues llistes són iguals si tenen els mateixos elements en el mateix ordre, sigui quina sigui la seva implementació, amb l'advertiment que el seu hashCode canvia quan canvia el seu contingut.

El Cataleg de BiblioTech ja és una classe professional: alta amb control de duplicats, inserció posicional, reemplaçament, baixa, cerca per qualsevol Predicate, ordenació per qualsevol Comparator, paginació amb subList i vista immutable cap a l'exterior. Zero línies de gestió de memòria.

A la lliçó següent, LinkedList, veuràs l'altra implementació de List: una cadena de nodes enllaçats en tots dos sentits, sense array i sense capacitat. Entendràs què implica això per a la memòria i per a la memòria cau del processador, i sobretot desmuntaràs el mite que gairebé tothom repeteix malament: que "LinkedList insereix en O(1)". És cert només si ja tens la posició, i arribar-hi costa O(n). Veuràs la comparació honesta operació per operació, per què a la pràctica ArrayList guanya gairebé sempre, per què LinkedList sobreviu sobretot com a Deque, com inserir correctament mentre recorres amb un ListIterator, i l'únic cas de BiblioTech en què LinkedList és realment l'elecció encertada: la cua de reserves que es consumeix pel principi i creix pel final.

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