En tancar la lliçó anterior va quedar pendent una peça pobra del projecte: les incidències d'un Prestec són un String[] amb textos solts com ara "Pagina 42 subratllada". Una incidència real té tres dades —el dia en què es va detectar, el motiu i la seva gravetat— i una cadena no les pot portar sense degenerar en un format fràgil del tipus "12|Pagina subratllada|LLEU" que caldria partir cada vegada. Necessites un tipus. Però, una classe pública de primer nivell, en el seu propi fitxer, per a una cosa que només existeix dins d'un préstec i que ningú no farà servir pel seu compte?

Java ofereix una resposta millor: les classes imbricades. Són classes declarades dins d'una altra classe, i permeten que un tipus auxiliar visqui exactament on té sentit, amb la visibilitat justa. Són quatre variants amb regles diferents, i triar malament té conseqüències reals: una d'elles és la causa clàssica de fuites de memòria en aplicacions Java de producció. Aquesta lliçó t'ensenya a distingir-les, a triar per defecte la correcta —que gairebé sempre és la mateixa— i a reconèixer el patró quan el vegis a la biblioteca estàndard, on és pertot arreu: Map.Entry, Thread.State, els nodes interns de LinkedList.

Contingut

  1. Les quatre classes imbricades: panorama i taula comparativa
  2. Classe interna de membre (no estàtica)
  3. La referència oculta Externa.this i el seu cost
  4. Fuites de memòria: el cas real
  5. Classe imbricada estàtica: l'opció correcta gairebé sempre
  6. Classe local: una classe dins d'un mètode
  7. Per què s'exigeix que les variables capturades siguin efectivament finals
  8. Visibilitat: la classe imbricada private com a detall ocult
  9. Els fitxers .class generats i què revelen
  10. Casos d'ús reals
  11. BiblioTech: Prestec.Incidencia
  12. BiblioTech: un comparador imbricat del catàleg
  13. Errors Habituals i Consells
  14. Exercicis

  1. Les quatre classes imbricades: panorama i taula comparativa

Una classe imbricada és una classe declarada dins del cos d'una altra classe o d'un mètode. Java ofereix quatre variants:

public class Externa {

    private int camp = 10;

    // 1. Classe interna de membre (no estatica)
    class Interna { }

    // 2. Classe imbricada estatica
    static class Imbricada { }

    public void metode() {
        // 3. Classe local
        class Local { }

        // 4. Classe anonima (04-04)
        Runnable r = new Runnable() {
            @Override public void run() { }
        };
    }
}
Característica Interna (membre) Imbricada estàtica Local Anònima
On es declara Cos de la classe Cos de la classe Dins d'un mètode En una expressió
Paraula static No No aplica No aplica
Té nom Sí (local) No
Referència a la instància externa (Externa.this) No Sí, si el mètode no és estàtic Sí, si el context no és estàtic
Accedeix a camps d'instància de l'externa Només als static
Accedeix a variables locals Sí, si són efect. finals Sí, si són efect. finals
Pot tenir membres static Sí (des de Java 16) Sí (des de Java 16) Només constants
S'instancia amb externa.new Interna() new Externa.Imbricada() new Local() dins del mètode new Tipus() { ... }
Modificadors d'accés Els quatre Els quatre Cap Cap
Risc de fuita de memòria No Baix Sí, si es desa
Freqüència real d'ús Baixa Molt alta Baixa Mitjana

La conclusió pràctica és a les dues últimes files: la imbricada estàtica és l'opció per defecte, i la interna no estàtica només es justifica quan de debò necessites accedir a l'estat de l'objecte extern. Vegem per què.

  1. Classe interna de membre (no estàtica)

És una classe declarada com a membre d'una altra, sense static. El seu tret definitori: cada instància seva està lligada a una instància de la classe externa, i pot accedir a tots els seus membres, fins i tot els private.

public class Prestec {

    private final String referencia;
    private int          diesTranscorreguts;

    public Prestec(String referencia, int diesTranscorreguts) {
        this.referencia         = referencia;
        this.diesTranscorreguts = diesTranscorreguts;
    }

    /** Classe interna NO estatica: cada avis pertany a UN prestec concret. */
    public class AvisIntern {

        private final String text;

        public AvisIntern(String text) {
            this.text = text;
        }

        public String formatar() {
            // Accedeix DIRECTAMENT als camps privats del prestec extern:
            return "[" + referencia + " / dia " + diesTranscorreguts + "] " + text;
        }
    }
}

Fixa't en formatar(): fa servir referencia i diesTranscorreguts sense cap prefix, com si fossin seus, i són camps private de Prestec. Aquesta és la capacitat que distingeix la classe interna.

I aquí ve la sintaxi que sorprèn tothom: per crear un AvisIntern necessites primer un Prestec.

Prestec p = new Prestec("PR-0001", 12);

// Sintaxi correcta: instancia externa PUNT new
Prestec.AvisIntern avis = p.new AvisIntern("Retard detectat");
System.out.println(avis.formatar());

// new Prestec.AvisIntern("...");   // NO COMPILA: falta la instancia externa
[PR-0001 / dia 12] Retard detectat

Aquest p.new AvisIntern(...) és probablement la sintaxi més estranya de Java, i la seva raresa és informativa: t'està dient que la classe interna no existeix sense el seu objecte extern. Dins de la mateixa classe externa no cal escriure-la, perquè this està implícit:

public void anotar(String text) {
    AvisIntern avis = new AvisIntern(text);   // equival a this.new AvisIntern(text)
    System.out.println(avis.formatar());
}

  1. La referència oculta Externa.this i el seu cost

Com pot AvisIntern llegir els camps de Prestec? Perquè el compilador li afegeix un camp ocult amb una referència a la instància externa. El codi que realment es genera equival a això:

// Versio "desensucrada" del que fa el compilador:
public class Prestec$AvisIntern {
    final Prestec this$0;                        // CAMP OCULT
    private final String text;

    Prestec$AvisIntern(Prestec externa, String text) {
        this.this$0 = externa;                   // es desa en construir
        this.text   = text;
    }

    public String formatar() {
        return "[" + this$0.referencia + " / dia " + this$0.diesTranscorreguts + "] " + text;
    }
}

Aquest camp s'anomena this$0 al bytecode i tu t'hi pots referir amb la sintaxi Externa.this, que resulta imprescindible quan hi ha noms que xoquen:

public class Prestec {

    private String estat = "ACTIU";

    public class AvisIntern {

        private String estat = "PENDENT";       // mateix nom

        public String descriure() {
            String estat = "LOCAL";             // i una variable local tambe

            return "local=" + estat                    // LOCAL
                 + ", avis=" + this.estat              // PENDENT
                 + ", prestec=" + Prestec.this.estat;  // ACTIU
        }
    }
}
local=LOCAL, avis=PENDENT, prestec=ACTIU

La regla de resolució és de dins cap a fora: variable local → camp de la interna (this) → camp de l'externa (Externa.this).

Però aquest camp ocult té un cost real, i és el punt central de la lliçó:

Cost Conseqüència
Memòria Cada instància interna pesa una referència més
Construcció Sempre cal una instància externa
Serialització Serialitzar la interna arrossega l'externa sencera (mòdul 7)
Vida de l'objecte Mentre visqui la interna, l'externa NO pot ser recol·lectada

L'última fila és la perillosa.

  1. Fuites de memòria: el cas real

Una fuita de memòria en Java no és memòria que es perd: és memòria que el recol·lector de brossa no pot alliberar perquè alguna cosa hi continua apuntant. I una classe interna no estàtica apunta al seu objecte extern sempre, encara que no faci servir ni un sol dels seus camps.

Vegem l'escenari concret. Imagina't un CatalegPesat que ocupa molta memòria i del qual només volem conservar un petit identificador:

public class CatalegPesat {

    private final Material[] materials = new Material[100_000];   // MOLT gran
    private final String     codi;

    public CatalegPesat(String codi) { this.codi = codi; }

    /** Classe interna NO estatica: compte. */
    public class Etiqueta {
        private final String text;
        public Etiqueta(String text) { this.text = text; }
        public String getText() { return text; }
        // No fa servir RES de CatalegPesat... pero el rete igualment
    }

    public Etiqueta crearEtiqueta() {
        return new Etiqueta("Cataleg " + codi);
    }
}
public class FugaDemo {

    private static CatalegPesat.Etiqueta etiquetaDesada;   // viu tot el programa

    public static void main(String[] args) {
        CatalegPesat cataleg = new CatalegPesat("CAT-2026");
        etiquetaDesada = cataleg.crearEtiqueta();

        cataleg = null;       // "ja no necessito el cataleg"
        System.gc();          // suggeriment al GC

        // L'array de 100.000 posicions SEGUEIX EN MEMORIA:
        // etiquetaDesada -> this$0 -> cataleg -> materials
        System.out.println(etiquetaDesada.getText());
    }
}
flowchart LR
    A["etiquetaDesada (camp static, viu)"] --> B["Etiqueta"]
    B -->|"this$0 ocult"| C["CatalegPesat"]
    C --> D["Material[100000]"]
    E["variable cataleg = null"] -.->|"ja no hi apunta"| C

Posar cataleg = null no serveix de res: hi continua havent un camí viu fins a l'array gegant, passant per la referència oculta. En una aplicació que crea milers d'etiquetes i les desa en una memòria cau, això és una fuita que creix fins a l'OutOfMemoryError, i és de les més difícils de diagnosticar perquè el codi d'Etiqueta no menciona el catàleg enlloc.

La solució és d'una paraula:

    /** Classe imbricada ESTATICA: sense referencia oculta, sense fuita. */
    public static class Etiqueta {
        private final String text;
        public Etiqueta(String text) { this.text = text; }
        public String getText() { return text; }
    }

Amb static, l'Etiqueta és un objecte independent. Quan cataleg deixa d'estar referenciat, el recol·lector s'emporta l'array de cent mil posicions i l'etiqueta es queda sola, pesant el que pesa una cadena. Els detalls del recol·lector i de com diagnosticar aquests casos s'estudien a la lliçó 10-07.

  1. Classe imbricada estàtica: l'opció correcta gairebé sempre

Una classe imbricada estàtica és un membre static de la classe externa. Conceptualment no és una classe interna: és una classe de primer nivell normal i corrent que, per comoditat, viu dins de l'espai de noms d'una altra.

public class Prestec {

    /** Classe imbricada ESTATICA: independent del prestec concret. */
    public static class Incidencia {

        private final int    dia;
        private final String motiu;

        public Incidencia(int dia, String motiu) {
            this.dia   = dia;
            this.motiu = motiu;
        }

        public int    getDia()   { return dia; }
        public String getMotiu() { return motiu; }
    }
}

S'instancia sense cap cerimònia extra, qualificant amb el nom de la classe externa:

Prestec.Incidencia i = new Prestec.Incidencia(12, "Pagina 42 subratllada");

// O, amb un import estatic del tipus imbricat:
// import com.nexussoftware.bibliotech.domini.Prestec.Incidencia;
Incidencia i2 = new Incidencia(14, "Tapa despresa");

Les seves regles:

  • No té referència a cap instància externa. Per tant no pot accedir a camps ni mètodes d'instància de la classe externa.
  • Sí que pot accedir als membres static de l'externa, inclosos els private.
  • No provoca fuites, no complica la serialització i es construeix sense res previ.

I què guanya, doncs, enfront d'una classe de primer nivell en el seu propi fitxer? Tres coses gens menors:

  1. Expressa pertinença. Prestec.Incidencia diu, en el seu propi nom, que una incidència pertany al món dels préstecs.
  2. Permet amagar-la. Si la declares private, és invisible fora de Prestec (apartat 8). Una classe de primer nivell no pot ser private.
  3. Redueix el soroll del paquet. Un tipus auxiliar de quinze línies no mereix el seu propi fitxer.

El JDK la fa servir constantment. Map.Entry és una interfície imbricada dins de Map; Thread.State és un enum imbricat; Character.UnicodeBlock és una classe imbricada estàtica. Tots segueixen la mateixa lògica: el tipus auxiliar viu on té sentit.

Regla pràctica: declara la classe imbricada static per defecte. Lleva-li el static només quan comprovis que necessita accedir a l'estat de la instància externa. És exactament el mateix criteri dels IDE moderns, que t'avisen amb "aquesta classe interna pot ser estàtica".

  1. Classe local: una classe dins d'un mètode

Una classe local es declara dins del cos d'un mètode, un constructor o un bloc. El seu àmbit és aquell bloc: fora d'ell no existeix, ni tan sols es pot anomenar.

public class GeneradorInformes {

    /** Agrupa materials per gravetat fent servir un tipus que nomes viu aqui. */
    public String informeGravetat(Material[] materials, int diesTranscorreguts) {

        // Classe LOCAL: neix i mor dins d'aquest metode
        class Comptador {
            private int lleus;
            private int greus;

            void registrar(String gravetat) {
                if ("LLEU".equals(gravetat)) { lleus++; }
                if ("GREU".equals(gravetat)) { greus++; }
            }

            String resum() {
                return String.format("Als %d dies: %d lleus, %d greus",
                                     diesTranscorreguts, lleus, greus);
            }
        }

        Comptador comptador = new Comptador();
        for (Material m : materials) {
            comptador.registrar(m.classificarGravetat(diesTranscorreguts));
        }
        return comptador.resum();
    }
}
Material[] cataleg = {
    new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
    new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
    new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};
System.out.println(new GeneradorInformes().informeGravetat(cataleg, 20));
Als 20 dies: 1 lleus, 2 greus

Les seves característiques:

  • No admet modificadors d'accés. public class Comptador dins d'un mètode no compila: la seva visibilitat ja està fixada pel bloc.
  • Pot accedir als camps de la classe externa si el mètode no és static.
  • Pot capturar variables locals i paràmetres del mètode, amb una condició estricta que és el tema de l'apartat següent. Fixa't en resum(): fa servir diesTranscorreguts, que és un paràmetre del mètode.

Les classes locals són poc freqüents. Si el tipus cap en poques línies i només implementa una interfície, la lambda (04-05) és millor; si té entitat, una classe imbricada estàtica és més llegible. El seu nínxol és aquest: un tipus auxiliar amb diversos camps i diversos mètodes que de debò només té sentit dins d'un algorisme concret.

  1. Per què s'exigeix que les variables capturades siguin efectivament finals

Prova de modificar una variable local capturada:

public String informe(Material[] materials) {
    int total = 0;

    class Comptador {
        void sumar() {
            total++;      // ERROR DE COMPILACIO
        }
    }
    // ...
}
error: local variables referenced from an inner class must be final
       or effectively final

Una variable és efectivament final (effectively final, des de Java 8) quan no se li assigna cap valor després de la seva inicialització, encara que no porti la paraula final. El compilador ho dedueix tot sol.

Per què aquesta restricció? Pel cicle de vida de les variables locals, que ja coneixes des de 03-02:

  • Una variable local viu a la pila del fil, dins del marc de la crida. Quan el mètode acaba, aquell marc desapareix i la variable deixa d'existir.
  • Un objecte viu al monticle (heap), i pot sobreviure molt més que el mètode que el va crear.

Si l'objecte de la classe local sobreviu al mètode —perquè el retornes, el deses en un camp o el passes a un altre fil—, d'on llegiria el valor d'una variable local que ja no existeix?

flowchart TD
    A["El metode s'executa: 'total' viu a la pila"] --> B["Es crea l'objecte de la classe local al heap"]
    B --> C["El compilador COPIA el valor de 'total' en un camp ocult de l'objecte"]
    C --> D["El metode acaba: el marc de pila desapareix"]
    D --> E["L'objecte sobreviu amb LA SEVA COPIA del valor"]

La solució de Java és la captura per valor: el compilador copia el valor dins de l'objecte. I d'aquí surt la restricció, perquè si es permetés modificar la variable hi hauria dues còpies descoordinades:

  • Modificar total al mètode no canviaria la còpia de l'objecte.
  • Modificar total dins de l'objecte no canviaria la del mètode.

Un mateix nom amb dos valors diferents: una font inesgotable de bugs. Java prefereix prohibir-ho en compilació.

Altres llenguatges trien el contrari: JavaScript captura la variable per referència (closures reals), i per això allà sí que la pots modificar —a canvi de sorpreses pròpies, com el clàssic bucle el comptador del qual acaba valent el mateix a tots els callbacks—. Java va optar per la seguretat. Tornarà a ser rellevant al mòdul 8, on compartir estat mutable entre fils és precisament la font dels pitjors errors.

Alternatives quan de debò necessites acumular:

// Opcio A (la millor): un camp de la classe local o de l'externa
class Comptador {
    private int total;                 // camp: viu al heap, es mutable
    void sumar() { total++; }
    int getTotal() { return total; }
}

// Opcio B: un array d'un element (truc antic, evita'l si pots)
int[] total = {0};                     // 'total' no canvia: canvia total[0]
class C { void sumar() { total[0]++; } }

L'opció A és la correcta: la mutabilitat pertany a un objecte, no a una variable capturada. La B funciona perquè la variable total (la referència a l'array) no es reassigna mai, però és un truc de llegibilitat dubtosa. La mateixa regla s'aplicarà paraula per paraula a les lambdes a 04-05.

  1. Visibilitat: la classe imbricada private com a detall ocult

Una classe imbricada admet els quatre modificadors d'accés, una cosa impossible en una classe de primer nivell (que només pot ser public o de paquet). I la combinació més potent és private static:

public class Cataleg {

    private final Material[] materials;
    private int              mida;

    public Cataleg(int capacitat) {
        this.materials = new Material[Math.max(1, capacitat)];
        this.mida      = 0;
    }

    /**
     * Detall d'implementacio TOTALMENT ocult: fora de Cataleg
     * aquest tipus no existeix. Es pot canviar o eliminar sense trencar ningu.
     */
    private static class Estadistiques {
        int    disponibles;
        int    prestats;
        double multaAcumulada;
    }

    private Estadistiques calcular(int diesTranscorreguts) {
        Estadistiques e = new Estadistiques();
        for (int i = 0; i < mida; i++) {
            if (materials[i].estaDisponible()) { e.disponibles++; }
            else                               { e.prestats++;    }
            e.multaAcumulada += materials[i].calcularMulta(diesTranscorreguts);
        }
        return e;
    }

    /** API publica: no filtra el tipus intern. */
    public String resum(int diesTranscorreguts) {
        Estadistiques e = calcular(diesTranscorreguts);
        return String.format("%d disponibles, %d prestats, %.2f EUR acumulats",
                             e.disponibles, e.prestats, e.multaAcumulada);
    }
}

Aquest és l'encapsulament de 03-07 portat al nivell del tipus, ja no del camp. Estadistiques no apareix en cap signatura pública, així que:

  • Ningú de fora no pot declarar una variable d'aquest tipus.
  • Canviar-ne els camps, reanomenar-la o substituir-la no trenca cap client.
  • L'API pública de Cataleg continua sent mínima (03-08).

Fixa't que els seus camps no porten modificador ni getters. És acceptable precisament perquè la classe és private: la seva única "API pública" és l'interior de Cataleg, i allà la cerimònia no aporta res. Aquesta relaxació seria inacceptable en un tipus públic.

  1. Els fitxers .class generats i què revelen

Un detall que aclareix molt el model mental: la JVM no coneix les classes imbricades. És un mecanisme del llenguatge que el compilador tradueix a classes de primer nivell amb noms construïts.

Compila un Prestec amb una Incidencia imbricada i una classe local Comptador, i mira el directori de sortida:

Prestec.class
Prestec$Incidencia.class
Prestec$1Comptador.class
Prestec$1.class
Fitxer Correspon a
Prestec.class La classe externa
Prestec$Incidencia.class Classe imbricada (estàtica o no); el nom es conserva
Prestec$1Comptador.class Classe local: nom precedit d'un número, perquè pot haver-hi diversos Comptador en mètodes diferents
Prestec$1.class Classe anònima: només un número, perquè no té nom (04-04)

Tres conclusions pràctiques:

  1. Cada classe imbricada és un fitxer .class més. Cent classes anònimes són cent fitxers, amb el seu cost de càrrega i de mida del .jar. A Android aquest recompte va importar molt durant anys.
  2. Pots distingir al bytecode si una interna és estàtica: la no estàtica té el camp this$0. Amb javap -p Prestec\$Incidencia el veuràs llistat.
  3. L'accés a membres private entre externa i interna no és màgia. Fins a Java 10 el compilador generava mètodes pont sintètics (access$000) per permetre-ho; des de Java 11 es resol amb nest-based access control, un mecanisme de la mateixa JVM. Per això una classe interna pot llegir un camp private de l'externa sense violar l'encapsulament: totes dues pertanyen al mateix "niu".

  1. Casos d'ús reals

Abans d'aplicar-ho al projecte, convé fixar els tres escenaris on les classes imbricades són la resposta estàndard en codi professional.

Builders. Un constructor amb vuit paràmetres és il·legible (ho vas veure a 03-04). El patró Builder ofereix una classe imbricada estàtica que va acumulant valors i construeix l'objecte al final:

Llibre llibre = new Llibre.Builder()
        .titol("Java Eficac")
        .autor("Joshua Bloch")
        .isbn("978-0000000001")
        .any(2018)
        .construir();

El Builder va imbricat dins de Llibre perquè només serveix per construir llibres. És el cas d'ús número u de les classes imbricades estàtiques, i es formalitza a 12-02.

Nodes d'una estructura de dades. Una llista enllaçada necessita un node amb un valor i un enllaç al següent. Aquest node és pur detall intern:

public class LlistaSimple {

    private Node cap;

    /** Detall d'implementacio: ningu de fora no sap que existeix. */
    private static class Node {
        Material valor;
        Node     seguent;
        Node(Material valor) { this.valor = valor; }
    }
}

Així està escrita java.util.LinkedList al JDK, amb una private static class Node. Ho veuràs a la lliçó 05-04.

Agrupació de dades auxiliars. Quan un mètode necessita retornar dos o tres valors relacionats, una classe imbricada evita inventar un tipus públic. Des de Java 16 hi ha una forma encara millor per a aquest cas —els record, i poden ser imbricats—, que veuràs a 04-07.

  1. BiblioTech: Prestec.Incidencia

Ha arribat el moment de substituir el String[] d'incidències. Una incidència té dia, motiu i gravetat, i només té sentit dins d'un préstec: classe imbricada estàtica.

package com.nexussoftware.bibliotech.domini;

import java.util.Arrays;
import java.util.Objects;

public class Prestec {

    // ============================================================
    //  Tipus imbricat: una incidencia registrada durant el prestec
    // ============================================================

    /**
     * Incidencia detectada en un material durant un prestec.
     *
     * <p>Es ESTATICA perque una incidencia no necessita accedir a l'estat del
     * prestec: se li passa tot el que necessita en construir-la. Aixi es pot
     * crear, desar i comparar sense arrossegar el prestec sencer.</p>
     *
     * <p>Es IMMUTABLE: un cop anotada, una incidencia no canvia.</p>
     */
    public static class Incidencia {

        private final int    dia;         // dia (enter) en que es va detectar
        private final String motiu;
        private final String gravetat;    // "LLEU" o "GREU"; a 04-07 sera un enum

        public Incidencia(int dia, String motiu, String gravetat) {
            this.dia      = Math.max(0, dia);
            this.motiu    = (motiu == null || motiu.isBlank())
                            ? "Sense especificar" : motiu.trim();
            this.gravetat = "GREU".equalsIgnoreCase(gravetat) ? "GREU" : "LLEU";
        }

        /** Drecera per a les incidencies corrents. */
        public Incidencia(int dia, String motiu) {
            this(dia, motiu, "LLEU");
        }

        public int     getDia()      { return dia; }
        public String  getMotiu()    { return motiu; }
        public String  getGravetat() { return gravetat; }
        public boolean esGreu()      { return "GREU".equals(gravetat); }

        @Override
        public String toString() {
            return String.format("dia %d - %s [%s]", dia, motiu, gravetat);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) { return true; }
            if (o == null || getClass() != o.getClass()) { return false; }
            Incidencia altra = (Incidencia) o;
            return dia == altra.dia && motiu.equals(altra.motiu);
        }

        @Override
        public int hashCode() { return Objects.hash(dia, motiu); }
    }

    // ============================================================
    //  Prestec
    // ============================================================

    private final String     referencia;
    private final Material   material;
    private final Empleat    empleat;
    private Incidencia[]     incidencies;      // abans: String[]

    // ... resta de camps i constructor sense canvis; incidencies = new Incidencia[0] ...

    /** Anota una incidencia amb dia, motiu i gravetat. */
    public void anotarIncidencia(int dia, String motiu, String gravetat) {
        Incidencia nova = new Incidencia(dia, motiu, gravetat);
        Incidencia[] ampliat = Arrays.copyOf(incidencies, incidencies.length + 1);
        ampliat[incidencies.length] = nova;
        this.incidencies = ampliat;            // el modul 5 fara aixo molt millor
    }

    /** Drecera: incidencia lleu en el dia actual del prestec. */
    public void anotarIncidencia(String motiu) {
        anotarIncidencia(getDiesTranscorreguts(), motiu, "LLEU");
    }

    /** @return copia defensiva (03-07): modificar-la no afecta el prestec. */
    public Incidencia[] getIncidencies() {
        return Arrays.copyOf(incidencies, incidencies.length);
    }

    /** Compta quantes incidencies greus s'han registrat. */
    public int comptarIncidenciesGreus() {
        int greus = 0;
        for (Incidencia i : incidencies) {
            if (i.esGreu()) { greus++; }
        }
        return greus;
    }
}

Ús:

Llibre  javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Empleat marta      = new Empleat("Marta Ruiz", "EMP-001");
Prestec p          = new Prestec(javaEficac, marta, 100, 18);

p.anotarIncidencia(12, "Pagina 42 subratllada", "LLEU");
p.anotarIncidencia(15, "Tapa despresa",         "GREU");
p.anotarIncidencia("Marca de cafe a la contraportada");

System.out.println("Incidencies de " + p.getReferencia() + ":");
for (Prestec.Incidencia i : p.getIncidencies()) {
    System.out.println("  " + i);
}
System.out.println("Greus: " + p.comptarIncidenciesGreus());
Incidencies de PR-0001:
  dia 12 - Pagina 42 subratllada [LLEU]
  dia 15 - Tapa despresa [GREU]
  dia 18 - Marca de cafe a la contraportada [LLEU]
Greus: 1

Compara-ho amb la versió anterior. Abans: String[] amb textos que calia llegir amb els ulls i on comptarIncidenciesGreus() hauria exigit cercar subcadenes. Ara: un tipus amb tres dades, comportament propi (esGreu()), equals/hashCode correctes (03-09), immutabilitat (03-07) i un nom —Prestec.Incidencia— que diu exactament a quin món pertany.

I la decisió clau és al static. Si Incidencia no fos estàtica:

  • No la podries crear sense un Prestec (p.new Incidencia(...)).
  • Cadascuna arrossegaria una referència al préstec complet, amb el seu material i el seu empleat.
  • Desar un històric d'incidències en memòria retindria tots els préstecs als quals pertanyen, encara que ja no els facis servir.

  1. BiblioTech: un comparador imbricat del catàleg

Segon cas d'ús: ordenar el catàleg. Java ofereix Arrays.sort(array, comparador), on el comparador és un objecte que implementa Comparator i respon a una pregunta: donats dos elements, quin va abans?

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.Material;
import java.util.Arrays;
import java.util.Comparator;

/** Operacions de cataleg de BiblioTech. */
public class Cataleg {

    private final Material[] materials;

    public Cataleg(Material[] materials) {
        this.materials = Arrays.copyOf(materials, materials.length);
    }

    /**
     * Comparador per titol. Es una classe imbricada ESTATICA: no necessita
     * cap dada del cataleg, nomes els dos materials que rep.
     */
    public static class PerTitol implements Comparator<Material> {
        @Override
        public int compare(Material a, Material b) {
            return a.getTitol().compareToIgnoreCase(b.getTitol());
        }
    }

    /** Comparador per multa acumulada, de major a menor. */
    public static class PerMultaDescendent implements Comparator<Material> {

        private final int diesTranscorreguts;

        public PerMultaDescendent(int diesTranscorreguts) {
            this.diesTranscorreguts = diesTranscorreguts;
        }

        @Override
        public int compare(Material a, Material b) {
            return Double.compare(b.calcularMulta(diesTranscorreguts),
                                  a.calcularMulta(diesTranscorreguts));
        }
    }

    /** Retorna una copia ordenada segons el criteri rebut. */
    public Material[] ordenar(Comparator<Material> criteri) {
        Material[] copia = Arrays.copyOf(materials, materials.length);
        Arrays.sort(copia, criteri);
        return copia;
    }
}

Sobre Comparator<Material>: els claudàtors angulars indiquen el tipus amb què treballa el comparador; Comparator<Material> és "un comparador de materials", i gràcies a això el mètode compare rep directament Material sense conversions. Aquí només fas servir un tipus genèric ja existent; escriure els teus propis tipus genèrics és la lliçó 10-01.

Ús:

Material[] cataleg = {
    new Dvd("Refactoritzacio en directe", "DVD-0007", 95),
    new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018),
    new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
    new Llibre("Patrons de Disseny", "Erich Gamma", "978-0000000002", 1994)
};

Cataleg c = new Cataleg(cataleg);

System.out.println("--- Per titol ---");
for (Material m : c.ordenar(new Cataleg.PerTitol())) {
    System.out.println("  " + m.descriure());
}

System.out.println("--- Per multa als 20 dies ---");
for (Material m : c.ordenar(new Cataleg.PerMultaDescendent(20))) {
    System.out.printf("  %-24s %.2f EUR%n", m.getTitol(), m.calcularMulta(20));
}
--- Per titol ---
  Llibre "Java Eficac" (ref. 978-0000000001) - Joshua Bloch, 2018
  Revista "Java Magazine" (ref. REV-2024-03) - n.42 (Mensual)
  Llibre "Patrons de Disseny" (ref. 978-0000000002) - Erich Gamma, 1994
  DVD "Refactoritzacio en directe" (ref. DVD-0007) - 95 min
--- Per multa als 20 dies ---
  Refactoritzacio en directe 8,50 EUR
  Java Magazine            1,30 EUR
  Java Eficac              1,25 EUR
  Patrons de Disseny       1,25 EUR

Observa el contrast entre els dos comparadors: PerTitol no té estat i PerMultaDescendent sí (desa diesTranscorreguts). Tots dos són estàtics, perquè cap no necessita res del Cataleg; el segon rep el que necessita per constructor. Aquesta és la forma correcta: passar les dades, no capturar-les per la porta del darrere.

A 04-04 veuràs que PerTitol es pot escriure sense declarar la classe, al mateix punt on es fa servir. I a 04-05 quedarà en una sola línia.

Errors Habituals i Consells

Descuidar el static. És l'error més comú i el més car. Una classe imbricada sense static que no fa servir l'estat extern paga una referència oculta, complica la serialització i pot provocar fuites de memòria. Els IDE ho detecten; fes cas de l'avís.

Intentar new Externa.Interna() amb una interna no estàtica. No compila: necessita una instància externa, amb la sintaxi externa.new Interna(). Si aquella sintaxi et resulta incòmoda, és senyal que la classe hauria de ser static.

Declarar membres static en una classe interna abans de Java 16. Fins a Java 15 una classe interna no estàtica només admetia constants static final. Des de Java 16 es permet qualsevol membre static. Amb Java 17 com a referència del curs no és un problema, però ho veuràs en codi antic.

Confondre this amb Externa.this. Dins d'una classe interna, this és la instància interna. Per arribar a l'externa cal escriure Externa.this. Aquest malentès reapareix amplificat a les classes anònimes (04-04).

Modificar una variable local capturada. No compila: ha de ser final o efectivament final. La solució correcta és un camp, no un array d'un element.

Abusar de les classes imbricades. Si la teva classe externa acumula cinc-centes línies per culpa de quatre tipus imbricats, treu-los a fitxers propis. La imbricació és per a tipus petits i auxiliars; quan un tipus té entitat pròpia, mereix el seu fitxer.

Consell: situa les classes imbricades al principi o al final. Tria un conveni i mantén-lo a tot el projecte. Molts equips les posen al final, després dels mètodes; d'altres al principi, com a declaració de tipus. L'important és no intercalar-les entre els mètodes.

Consell: private static class és el punt de partida. Comença per la màxima restricció i relaxa-la només quan calgui. És el mateix criteri que vas aplicar als camps a 03-07, ara als tipus.

Exercicis

Exercici 1: Empleat.Historial

Afegeix a Empleat una classe imbricada estàtica Historial amb els camps prestecsTotals, devolucionsTotals i multaAcumulada (double), amb getters i toString. Afegeix a Empleat un camp historial inicialitzat al constructor i fes que registrarPrestec() i registrarDevolucio() l'actualitzin. Afegeix també registrarMulta(double). Justifica en un comentari per què la classe és static.

Exercici 2: fuita de memòria demostrada

Escriu dues versions d'una classe CachePesada que contingui un int[] dades = new int[5_000_000] i una classe imbricada Resum amb només un String. A la versió A, Resum és interna no estàtica; a la B, estàtica. En cada cas: crea la memòria cau, obtén el resum, posa la memòria cau a null, crida System.gc() i mostra la memòria usada amb Runtime.getRuntime().totalMemory() - freeMemory(). Explica la diferència.

Exercici 3: classe local per a un informe agrupat

Escriu un mètode String informePerTipus(Material[] materials, int diesTranscorreguts) que faci servir una classe local Grup (amb tipus, unitats i multaTotal) per agrupar els materials pel seu getTipus() i retornar un informe amb una línia per tipus. Recorda: sense col·leccions, fes servir arrays.

Solucions

Solució 1

package com.nexussoftware.bibliotech.domini;

public class Empleat {

    public static final int MAX_PRESTECS_SIMULTANIS = 3;

    /**
     * Historial acumulat d'un empleat.
     *
     * Es ESTATICA perque nomes desa comptadors propis: no necessita llegir
     * el nom ni l'identificador de l'empleat. Aixi un historial es pot
     * copiar, comparar o desar sense retenir l'empleat sencer.
     */
    public static class Historial {

        private int    prestecsTotals;
        private int    devolucionsTotals;
        private double multaAcumulada;

        public int    getPrestecsTotals()    { return prestecsTotals;    }
        public int    getDevolucionsTotals() { return devolucionsTotals; }
        public double getMultaAcumulada()    { return multaAcumulada;    }

        /** Nomes Empleat el pot modificar: metodes de paquet, no publics. */
        void sumarPrestec()    { prestecsTotals++;    }
        void sumarDevolucio()  { devolucionsTotals++; }
        void sumarMulta(double quantitat) {
            if (quantitat > 0) { multaAcumulada += quantitat; }
        }

        @Override
        public String toString() {
            return String.format("%d prestecs, %d devolucions, %.2f EUR en multes",
                                 prestecsTotals, devolucionsTotals, multaAcumulada);
        }
    }

    private final String    nom;
    private final String    identificador;
    private int             prestecsAcumulats;
    private final Historial historial;

    public Empleat(String nom, String identificador) {
        this.nom = (nom == null || nom.isBlank())
                   ? "Empleat sense nom" : nom.trim();
        this.identificador = (identificador == null || !identificador.startsWith("EMP-"))
                             ? "EMP-000" : identificador.trim();
        this.prestecsAcumulats = 0;
        this.historial = new Historial();
    }

    public String    getNom()           { return nom; }
    public String    getIdentificador() { return identificador; }
    public Historial getHistorial()     { return historial; }

    public boolean potPrendrePrestat() {
        return prestecsAcumulats < MAX_PRESTECS_SIMULTANIS;
    }

    public boolean registrarPrestec() {
        if (!potPrendrePrestat()) {
            System.out.printf("AVIS: %s ja te %d prestecs (maxim %d).%n",
                              nom, prestecsAcumulats, MAX_PRESTECS_SIMULTANIS);
            return false;
        }
        prestecsAcumulats++;
        historial.sumarPrestec();
        return true;
    }

    public void registrarDevolucio() {
        prestecsAcumulats = Math.max(0, prestecsAcumulats - 1);
        historial.sumarDevolucio();
    }

    public void registrarMulta(double quantitat) {
        historial.sumarMulta(quantitat);
    }
}
Empleat diego = new Empleat("Diego Alonso", "EMP-002");
diego.registrarPrestec();
diego.registrarPrestec();
diego.registrarDevolucio();
diego.registrarMulta(1.25);
diego.registrarMulta(8.50);

System.out.println(diego.getNom() + ": " + diego.getHistorial());
Diego Alonso: 2 prestecs, 1 devolucions, 9,75 EUR en multes

Detall de disseny: els mutadors d'Historial (sumarPrestec, sumarMulta) no són public, sinó de paquet. Així l'historial és de només lectura per a l'exterior —getHistorial() retorna una cosa que ningú no pot falsejar des d'un altre paquet— però Empleat sí que el pot actualitzar. És l'encapsulament de 03-07 aprofitant la visibilitat de paquet que vas estudiar allà.

Solució 2

package com.nexussoftware.bibliotech;

public class FugaDemo {

    // ---------- Versio A: classe interna NO estatica ----------
    static class CachePesadaA {
        private final int[] dades = new int[5_000_000];   // uns 20 MB
        private final String codi;

        CachePesadaA(String codi) {
            this.codi = codi;
            dades[0] = 1;                                  // evita optimitzacions
        }

        /** NO estatica: desa una referencia oculta a CachePesadaA. */
        class Resum {
            private final String text;
            Resum(String text) { this.text = text; }
            String getText() { return text; }
        }

        Resum resumir() { return new Resum("Cache " + codi); }
    }

    // ---------- Versio B: classe imbricada ESTATICA ----------
    static class CachePesadaB {
        private final int[] dades = new int[5_000_000];
        private final String codi;

        CachePesadaB(String codi) {
            this.codi = codi;
            dades[0] = 1;
        }

        /** ESTATICA: objecte independent, sense referencia oculta. */
        static class Resum {
            private final String text;
            Resum(String text) { this.text = text; }
            String getText() { return text; }
        }

        Resum resumir() { return new Resum("Cache " + codi); }
    }

    private static long memoriaUsadaMb() {
        Runtime r = Runtime.getRuntime();
        return (r.totalMemory() - r.freeMemory()) / (1024 * 1024);
    }

    private static void reposar() {
        System.gc();
        // Sense try/catch (modul 6) s'evita Thread.sleep: n'hi ha prou amb dos gc()
        System.gc();
    }

    public static void main(String[] args) {

        reposar();
        System.out.println("Memoria inicial:            " + memoriaUsadaMb() + " MB");

        // --- A ---
        CachePesadaA a = new CachePesadaA("CAT-A");
        CachePesadaA.Resum ra = a.resumir();
        a = null;
        reposar();
        System.out.println("A (interna no estatica):    " + memoriaUsadaMb()
                           + " MB  -> " + ra.getText());

        // --- B ---
        CachePesadaB b = new CachePesadaB("CAT-B");
        CachePesadaB.Resum rb = b.resumir();
        b = null;
        reposar();
        System.out.println("B (imbricada estatica):     " + memoriaUsadaMb()
                           + " MB  -> " + rb.getText());
    }
}

Sortida típica (els números varien segons la JVM i la memòria disponible):

Memoria inicial:            2 MB
A (interna no estatica):    21 MB  -> Cache CAT-A
B (imbricada estatica):     21 MB  -> Cache CAT-B

La lectura correcta d'aquests números: després del bloc A, els ~20 MB hi continuen sent encara que a valgui null, perquè ra manté viva la memòria cau A a través de la referència oculta this$0. Al bloc B se'n creen uns altres ~20 MB, però el recol·lector pot endur-se la memòria cau B tan bon punt b = null, així que la memòria no puja 20 MB més: el que veus és la memòria cau A, que mai no es va alliberar.

Per observar-ho encara més clar, repeteix el bloc en un bucle de cinc iteracions desant cada resum en un array: amb la versió A la memòria creix sense parar fins a l'OutOfMemoryError; amb la B es manté estable. Aquesta és, literalment, la fuita que apareix en aplicacions reals quan un listener o una entrada de memòria cau s'implementa com a classe interna no estàtica. Els detalls del recol·lector són a 10-07.

Solució 3

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.domini.Material;

public class InformeCataleg {

    /**
     * Agrupa els materials per tipus. La classe Grup es LOCAL: nomes te
     * sentit dins d'aquest algorisme i no ha d'apareixer a l'API.
     */
    public String informePerTipus(Material[] materials, int diesTranscorreguts) {

        class Grup {
            private final String tipus;
            private int          unitats;
            private double       multaTotal;

            Grup(String tipus) { this.tipus = tipus; }

            void afegir(Material m) {
                unitats++;
                // 'diesTranscorreguts' es un parametre capturat: no es modifica
                // en cap punt del metode, aixi que es efectivament final.
                multaTotal += m.calcularMulta(diesTranscorreguts);
            }

            String linia() {
                return String.format("  %-12s %2d unitats  %6.2f EUR%n",
                                     tipus, unitats, multaTotal);
            }
        }

        // Encara sense llistes (modul 5): arrays de mida maxima
        Grup[] grups   = new Grup[materials.length];
        int    numGrups = 0;

        for (Material m : materials) {
            Grup desti = null;
            for (int i = 0; i < numGrups; i++) {
                if (grups[i].tipus.equals(m.getTipus())) {
                    desti = grups[i];
                    break;
                }
            }
            if (desti == null) {
                desti = new Grup(m.getTipus());
                grups[numGrups++] = desti;
            }
            desti.afegir(m);
        }

        StringBuilder sb = new StringBuilder();
        sb.append(String.format("INFORME PER TIPUS (als %d dies)%n", diesTranscorreguts));
        for (int i = 0; i < numGrups; i++) {
            sb.append(grups[i].linia());
        }
        return sb.toString();
    }
}
Material[] cataleg = {
    new Llibre("Java Eficac",        "Joshua Bloch",  "978-0000000001", 2018),
    new Llibre("Patrons de Disseny", "Erich Gamma",   "978-0000000002", 1994),
    new Llibre("Refactoritzacio",    "Martin Fowler", "978-0000000003", 1999),
    new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"),
    new Dvd("Refactoritzacio en directe", "DVD-0007", 95)
};

System.out.print(new InformeCataleg().informePerTipus(cataleg, 20));
INFORME PER TIPUS (als 20 dies)
  Llibre        3 unitats    3,75 EUR
  Revista       1 unitats    1,30 EUR
  DVD           1 unitats    8,50 EUR

Tres punts didàctics de la solució:

  1. Grup és local i està bé que ho sigui. No apareix en cap signatura, no contamina el paquet i els seus camps sense getters són acceptables perquè el seu abast és el mètode. Si demà calgués a fora, passaria a ser una classe imbricada estàtica o un record (04-07).
  2. Captura de diesTranscorreguts. És un paràmetre del mètode i no es reassigna mai, així que és efectivament final i afegir el pot fer servir. Si en algun punt escrivissis diesTranscorreguts++, el mètode deixaria de compilar a la línia de Grup, no a la de l'increment.
  3. La cerca del grup és O(n²). Recórrer els grups per cada material és ineficient i aquí és inevitable, perquè no tens HashMap. Al mòdul 5 aquest mètode sencer es reduirà a unes poques línies.

Conclusió

Ja saps que Java permet declarar classes dins de classes i coneixes les quatre variants amb les seves regles: la interna de membre, lligada a una instància externa; la imbricada estàtica, independent; la local, confinada a un mètode; i l'anònima, que arriba a la lliçó següent. Tens la taula que les distingeix i, sobretot, el criteri per triar: static per defecte, i lleva'l només quan comprovis que necessites l'estat de l'objecte extern.

Has vist per dins el mecanisme de la classe interna: el camp ocult this$0 que el compilador hi afegeix, la sintaxi externa.new Interna() que delata la dependència, i Externa.this per desambiguar quan els noms xoquen. I n'has vist el cost real: mentre visqui la instància interna, l'externa no pot ser recol·lectada. Aquesta és la causa de fuites de memòria autèntiques en producció, difícils de diagnosticar precisament perquè el codi de la classe interna no menciona l'externa enlloc. La solució és una paraula.

Entens la restricció de les variables efectivament finals i —el més important— per què existeix: les variables locals viuen a la pila i moren amb el mètode, mentre que els objectes viuen al monticle i poden sobreviure; Java captura per valor, i permetre'n la modificació crearia dues còpies descoordinades del mateix nom. És una regla que reapareixerà idèntica a les lambdes. Saps fer servir una classe imbricada private static com a detall d'implementació totalment ocult —l'encapsulament de 03-07 aplicat al tipus sencer, ja no al camp— i pots llegir els fitxers Externa$Interna.class, Externa$1Local.class i Externa$1.class sabent què revela cada nom.

I ho has aplicat. Les incidències de BiblioTech han deixat de ser cadenes soltes: Prestec.Incidencia és una classe imbricada estàtica, immutable, amb dia, motiu i gravetat, amb equals/hashCode correctes i amb un nom que declara a quin món pertany. I el catàleg ja es pot ordenar amb Cataleg.PerTitol i Cataleg.PerMultaDescendent, dos comparadors imbricats que es passen com a paràmetre a Arrays.sort per canviar el criteri sense tocar el codi que ordena.

Aquest últim exemple deixa una pregunta a l'aire. PerTitol és una classe sencera —declaració, @Override, cos— per a una sola línia de lògica que només es fa servir en un lloc. Declarar-la, posar-li nom i donar-li un lloc propi a l'espai de noms de Cataleg és molta cerimònia per a tan poc. A la lliçó 04-04, Classes Anònimes, aprendràs a declarar i instanciar una implementació al mateix punt on la necessites i sense posar-li nom: en veuràs la sintaxi completa —inclòs el punt i coma final que tothom oblida—, què pot i què no pot fer, el parany de this dins d'una anònima, i una taula de decisió que et dirà, per a cada situació, si toca classe amb nom, imbricada, local, anònima o —el que ve després— una lambda.

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