Portes tres lliçons sentint a parlar de la memòria intermèdia. A 07-01 explicava per què llegir caràcter a caràcter és cent vegades més lent del que cal. A 07-02 explicava per què un fitxer acabat d'escriure té zero bytes. A 07-03 apareixia a totes les cadenes de decoradors i a la taula de rendiment amb un factor de seixanta.

Aquesta lliçó és la de la memòria intermèdia. Veuràs què fa exactament per dins, per què aquesta millora és tan gran, i les dues classes que l'aporten al món del text: BufferedReader, amb el readLine() que és el mètode d'E/S més usat de Java, i BufferedWriter, amb el seu newLine() portable.

I veuràs una cosa més important que el rendiment: el processament en flux. Un BufferedReader permet recórrer un fitxer d'un gigabyte amb un consum de memòria constant, línia a línia, sense carregar res més que la línia actual. Aquesta capacitat és la que converteix BiblioTech en un sistema capaç d'importar un inventari complet, i és el que construiràs al cas pràctic final: ImportadorCataleg, que llegeix milers de línies, valida cadascuna, compta les bones i les dolentes, registra els motius i retorna un informe.

try-with-resources a tot, com sempre. Amb memòries intermèdies d'escriptura hi ha un motiu afegit per no oblidar-ho: el close() és el que buida la memòria intermèdia al disc. Sense ell, no has escrit res.

Contingut

  1. Què fa exactament una memòria intermèdia
  2. Sense memòria intermèdia i amb memòria intermèdia: el diagrama
  3. Les xifres que justifiquen la diferència
  4. Construcció per decoració i mida de la memòria intermèdia
  5. BufferedReader.readLine() i el seu contracte
  6. El bucle canònic de lectura
  7. ready() i per què no serveix com a condició de final
  8. mark i reset: mirar sense consumir
  9. BufferedWriter: write, newLine i flush
  10. El trio PrintWriter + BufferedWriter + FileWriter
  11. Files.newBufferedReader i newBufferedWriter
  12. Processar un fitxer gran amb memòria constant
  13. Llegir de consola amb BufferedReader davant de Scanner
  14. BiblioTech: ImportadorCataleg amb informe complet
  15. Errors Comuns i Consells
  16. Exercicis

  1. Què fa exactament una memòria intermèdia

Una memòria intermèdia és un array intermedi a la RAM que agrupa les operacions per reduir el nombre de crides al sistema operatiu.

Recorda el diagrama de 07-01: entre el teu codi i el disc hi ha una frontera cara, la crida al sistema. Cada vegada que la creues, el processador canvia de context, salta al nucli, fa la seva feina i torna. Aquest viatge costa ordres de magnitud més que executar unes quantes instruccions en el teu propi procés.

El que fa un BufferedReader:

  1. Quan li demanes un caràcter, mira el seu array intern.
  2. Si té dades, te'n retorna un sense sortir de la memòria del procés.
  3. Si és buit, demana 8192 caràcters de cop al flux que embolcalla, els desa al seu array i et retorna el primer.

El resultat és que una lectura de cada 8192 arriba al sistema operatiu; les altres 8191 són accessos a un array.

El mateix principi, invertit, en escriptura:

  1. Quan li demanes escriure, desa al seu array.
  2. Quan l'array s'omple —o quan algú fa flush() o close()— el bolca d'una sola vegada.

D'aquí surt l'explicació completa de dues coses que ja has vist i que ara encaixen:

  • Per què FileReader.read() és tan lent: no té memòria intermèdia. Cada crida és una crida al sistema.
  • Per què un fitxer acabat d'escriure té 0 bytes (07-02): les dades són a l'array de la memòria intermèdia, no al disc.

  1. Sense memòria intermèdia i amb memòria intermèdia: el diagrama

flowchart TD
    subgraph SENSE["SENSE memoria intermedia: FileReader.read()"]
        A1["read() numero 1"] --> S1["crida al sistema"]
        A2["read() numero 2"] --> S2["crida al sistema"]
        A3["read() numero 3"] --> S3["crida al sistema"]
        A4["... 8192 vegades"] --> S4["... 8192 crides"]
        S1 --> D1["DISC"]
        S2 --> D1
        S3 --> D1
        S4 --> D1
    end

    style D1 fill:#ffcdd2
flowchart TD
    subgraph AMB["AMB memoria intermedia: BufferedReader.read()"]
        B1["read() numero 1"] --> BUF["array intern<br/>de 8192 caracters"]
        B2["read() numero 2"] --> BUF
        B3["read() numero 3"] --> BUF
        B4["... 8192 vegades"] --> BUF
        BUF -->|"UNA sola vegada,<br/>quan es buit"| SYS["crida al sistema"]
        SYS --> D2["DISC"]
    end

    style BUF fill:#c8e6c9
    style D2 fill:#c8e6c9

La comparació numèrica del cas complet, amb un fitxer de 5 MB:

Sense memòria intermèdia Amb memòria intermèdia de 8 KB
Crides a read() del teu codi 5 242 880 5 242 880
Crides al sistema operatiu 5 242 880 640
Memòria addicional usada 0 16 KB
Temps orientatiu ~4 500 ms ~60 ms

El codi fa el mateix nombre de crides; el que canvia és quantes creuen la frontera. Vuit mil vegades menys, a canvi de setze kilobytes de memòria. És probablement el millor intercanvi de memòria per temps que existeix en programació.

  1. Les xifres que justifiquen la diferència

Un experiment que pots executar:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class MesurarBuffer {

    /** Sense memoria intermedia: una crida al sistema per caracter. */
    static long senseBuffer(String cami) throws IOException {
        long n = 0;
        try (FileReader lector = new FileReader(cami, StandardCharsets.UTF_8)) {
            while (lector.read() != -1) {
                n++;
            }
        }
        return n;
    }

    /** Amb memoria intermedia, mateix bucle: absorbeix les crides. */
    static long ambBuffer(String cami) throws IOException {
        long n = 0;
        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {
            while (lector.read() != -1) {
                n++;
            }
        }
        return n;
    }

    /** Amb memoria intermedia i per linies: la forma idiomatica. */
    static long perLinies(String cami) throws IOException {
        long n = 0;
        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {
            String linia;
            while ((linia = lector.readLine()) != null) {
                n += linia.length();
            }
        }
        return n;
    }

    public static void main(String[] args) throws IOException {
        String cami = "dades/cataleg-gran.txt";        // uns 5 MB

        mesurar("Sense buffer, caracter a caracter", () -> senseBuffer(cami));
        mesurar("Amb buffer, caracter a caracter",   () -> ambBuffer(cami));
        mesurar("Amb buffer, per linies",            () -> perLinies(cami));
    }

    interface Mesurable { long executar() throws IOException; }

    static void mesurar(String nom, Mesurable m) throws IOException {
        long inici = System.nanoTime();
        long resultat = m.executar();
        long ms = (System.nanoTime() - inici) / 1_000_000;
        System.out.printf("%-35s %6d ms  (%d caracters)%n", nom, ms, resultat);
    }
}

Resultat orientatiu:

Sense buffer, caracter a caracter     4512 ms  (5242880 caracters)
Amb buffer, caracter a caracter         68 ms  (5242880 caracters)
Amb buffer, per linies                  41 ms  (5158400 caracters)

Tres observacions:

  1. La diferència entre la primera i la segona és una sola paraula al codi: new BufferedReader(...). Factor 66.
  2. La tercera és encara millor perquè readLine() treballa directament sobre l'array de la memòria intermèdia, buscant el salt de línia, sense cridar read() una vegada per caràcter.
  3. El recompte de la tercera és menor perquè readLine() no inclou el salt de línia. Aquest detall és part del seu contracte i s'explica a l'apartat 5.

  1. Construcció per decoració i mida de la memòria intermèdia

BufferedReader i BufferedWriter són filtres en el sentit de 07-03: embolcallen un altre Reader o Writer.

import java.io.*;
import java.nio.charset.StandardCharsets;

// Lectura, mida per defecte (8192 caracters)
BufferedReader lector = new BufferedReader(
        new FileReader("dades/cataleg.txt", StandardCharsets.UTF_8));

// Lectura, mida explicita
BufferedReader gran = new BufferedReader(
        new FileReader("dades/cataleg.txt", StandardCharsets.UTF_8), 65536);

// Escriptura
BufferedWriter escriptor = new BufferedWriter(
        new FileWriter("dades/informe.txt", StandardCharsets.UTF_8));

// Sobre un pont, quan la font no es un fitxer (07-03)
BufferedReader deConsola = new BufferedReader(
        new InputStreamReader(System.in, StandardCharsets.UTF_8));

Sobre la mida:

Mida Quan
8192 (per defecte) Gairebé sempre. Coincideix amb el bloc del sistema de fitxers
32768 – 65536 Fitxers molt grans llegits seqüencialment, si has mesurat millora
1024 o menys Mai, tret de memòria molt restringida
Més d'1 MB Contraproduent: deixa de cabre a la memòria cau i pressiona el recol·lector

I l'advertiment de 07-03, que aquí s'aplica igual: si obres cent fluxos alhora amb memòries intermèdies d'1 MB, són 100 MB de heap. La mida per defecte està molt ben triada; canvia-la només amb una mesura al davant.

Un detall que convé assenyalar: embolcallar una cosa que ja té memòria intermèdia no aporta res. new BufferedReader(new BufferedReader(...)) és redundant, i new BufferedInputStream(new ByteArrayInputStream(...)) també, perquè un array a la RAM no fa crides al sistema. La memòria intermèdia serveix on hi ha una frontera cara per creuar.

  1. BufferedReader.readLine() i el seu contracte

readLine() és, de bon tros, el mètode d'E/S més usat de Java. El seu contracte té quatre punts, i tots quatre importen:

public String readLine() throws IOException

1. Retorna la línia sense el terminador. Si el fitxer té Java Eficac\n, readLine() retorna "Java Eficac", d'11 caràcters. El \n es consumeix però no s'inclou. Per això el recompte de l'experiment de l'apartat 3 sortia menor.

2. Reconeix les tres convencions. \n, \r\n i \r es tracten com a final de línia, independentment del sistema. Un fitxer escrit a Windows es llegeix correctament a Linux sense fer res. Aquesta és la raó per la qual l'apartat 8 de 07-02 concloïa que, per al teu propi programa, el separador és igual.

3. Retorna null al final del fitxer. No "", no una excepció: null. I aquesta distinció és essencial, perquè una línia buida retorna "", que no és el mateix:

Contingut restant readLine() retorna
Java Eficac\n "Java Eficac"
\n (línia buida) "" (cadena buida, longitud 0)
Res: final de fitxer null
Sense salt final (última línia sense \n) "Sense salt final"

4. Bloqueja fins a tenir una línia completa. Amb un fitxer és instantani. Amb la consola o un socket, espera fins que arribi un salt de línia o es tanqui el flux. És el que fa que la consola esperi que premis Retorn.

Un cas límit que convé conèixer: una línia sense terminador al final del fitxer es retorna igualment. La crida següent retorna null. És correcte i evita perdre l'última línia de fitxers generats per eines que no posen salt final.

I un avís de dimensionament: readLine() carrega la línia sencera en memòria. Amb un fitxer el contingut del qual sigui una única línia de 2 GB —n'hi ha: bolcats de bases de dades, JSON en una línia—, readLine() intenta construir un String de 2 GB i provoca OutOfMemoryError. És rar, però quan passa desconcerta, perquè el codi "processa línia a línia" i tot i així es queda sense memòria.

  1. El bucle canònic de lectura

Aquesta és la forma idiomàtica, i cal escriure-la sempre igual:

try (BufferedReader lector = new BufferedReader(
        new FileReader(cami, StandardCharsets.UTF_8))) {

    String linia;
    while ((linia = lector.readLine()) != null) {
        processar(linia);
    }
}

Desglossat:

  1. String linia; es declara fora del bucle, perquè la condició necessita veure-la.
  2. linia = lector.readLine() llegeix i assigna. L'assignació és una expressió, com a 07-01.
  3. Els parèntesis interns són obligatoris: sense ells, linia = lector.readLine() != null intentaria assignar un boolean a un String i no compila.
  4. != null és el final. No !linia.isEmpty(), que s'aturaria a la primera línia en blanc.

Els tres errors que substitueixen aquest bucle i tots fallen:

// MALAMENT 1: s'atura a la primera linia buida
while (!(linia = lector.readLine()).isEmpty()) { }
// A mes: NullPointerException en arribar al final, perque readLine retorna null

// MALAMENT 2: llegeix cada linia DUES vegades i se'n salta una de cada dues
while (lector.readLine() != null) {
    processar(lector.readLine());    // aquesta es la SEGUENT linia
}

// MALAMENT 3: ready() no significa "queden dades". Apartat 7
while (lector.ready()) {
    processar(lector.readLine());
}

El segon és especialment traïdor perquè funciona a mitges: processa les línies parelles i se salta les senars, i amb un fitxer de prova petit pot semblar correcte.

Amb comptador de línies, que és l'habitual en codi real:

int numeroLinia = 0;
String linia;
while ((linia = lector.readLine()) != null) {
    numeroLinia++;

    String neta = linia.trim();
    if (neta.isEmpty() || neta.startsWith("#")) {
        continue;                        // buides i comentaris: 02-04
    }
    processar(neta, numeroLinia);        // el numero, per poder informar de l'error
}

Aquest numeroLinia no és decoratiu: és el que permet dir "línia 4 217: l'any no és un nombre" en lloc de "error en importar". És l'aplicació directa de 06-04 —l'excepció ha de portar el context que qui la llegeix necessita— a la importació de fitxers.

  1. ready() i per què no serveix com a condició de final

ready() existeix i el seu nom convida al malentès:

public boolean ready() throws IOException

Retorna true si una lectura no es bloquejaria, és a dir, si hi ha dades disponibles a la memòria intermèdia o llestes a la font. I això no és el mateix que "queda alguna cosa per llegir".

// MALAMENT: us incorrecte de ready()
try (BufferedReader lector = new BufferedReader(new FileReader(cami, UTF_8))) {
    while (lector.ready()) {              // <-- BUG
        System.out.println(lector.readLine());
    }
}

Per què falla, en tres escenaris reals:

Escenari Què passa
Fitxer local petit Sol funcionar. Per això el bug sobreviu a les proves
Fitxer gran, memòria intermèdia buida en aquell instant ready() retorna false i el bucle acaba a mitges
Consola ready() és false mentre l'usuari pensa: el bucle no arriba a llegir res
Socket de xarxa (mòdul 9) ready() és false entre paquets: es perden dades
Fitxer en unitat de xarxa lenta Falla de forma intermitent i irreproduïble

És el pitjor tipus de bug: funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes, de forma no determinista.

L'única condició de final correcta és readLine() != null. ready() serveix per a una altra cosa: comprovar si pots llegir sense bloquejar-te, en un programa que té una altra feina a fer mentre espera. Aquest cas pertany al mòdul 8.

  1. mark i reset: mirar sense consumir

Un flux no permet recular... tret que la memòria intermèdia ho recordi. BufferedReader ofereix aquesta capacitat limitada:

public void mark(int limitDeLectura) throws IOException
public void reset() throws IOException
public boolean markSupported()

mark(n) marca la posició actual i promet poder tornar-hi sempre que no es llegeixin més de n caràcters. reset() torna a la marca.

Ús típic: inspeccionar el principi d'un fitxer per decidir com processar-lo, sense perdre aquella primera línia.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class DeteccioDeCapcalera {

    /**
     * Detecta si el fitxer te capcalera; si no en te, la primera
     * linia es una dada i NO s'ha de perdre.
     */
    public static void processar(String cami) throws IOException {
        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {

            lector.mark(8192);                     // marge ampli
            String primera = lector.readLine();

            boolean teCapcalera = primera != null
                    && (primera.startsWith("#") || primera.toLowerCase().contains("titol"));

            if (!teCapcalera) {
                lector.reset();                    // tornar la linia al flux
                System.out.println("Sense capcalera: la primera linia es una dada");
            } else {
                System.out.println("Capcalera detectada: " + primera);
            }

            String linia;
            while ((linia = lector.readLine()) != null) {
                System.out.println("  dada: " + linia);
            }
        }
    }
}

Els seus límits, que cal respectar:

  • mark pot fallar si el readAheadLimit és massa gran. La memòria intermèdia ha de créixer fins a aquesta mida; demanar mark(Integer.MAX_VALUE) intenta reservar 2 GB.
  • Llegir més de n caràcters invalida la marca i reset() llança IOException.
  • No tots els Reader ho suporten. markSupported() ho diu. BufferedReader sí; FileReader a seques, no.

A la pràctica es fa servir poc: gairebé sempre és més simple llegir la primera línia i decidir què fer-ne en una variable. Val la pena conèixer-ho perquè apareix en codi d'anàlisi de formats, i perquè explica per què existeix el paràmetre de mida de la memòria intermèdia.

  1. BufferedWriter: write, newLine i flush

La contrapartida d'escriptura. La seva API és curta:

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class EscripturaAmbBuffer {

    public static void escriure(String cami) throws IOException {
        try (BufferedWriter escriptor = new BufferedWriter(
                new FileWriter(cami, StandardCharsets.UTF_8))) {

            escriptor.write("# Cataleg de BiblioTech");
            escriptor.newLine();                       // separador DEL SISTEMA

            escriptor.write("LLIBRE;978-0000000001;Java Eficac");
            escriptor.newLine();

            escriptor.write("LLIBRE;978-0000000002;Patrons de Disseny");
            escriptor.newLine();
        }
        // close() -> flush() -> les dades arriben al disc (07-02)
    }
}
Mètode Què fa
write(String) Escriu la cadena a la memòria intermèdia
write(String, int, int) Escriu una subcadena
write(char[], int, int) Escriu part d'un array
write(int) Escriu un caràcter
newLine() Escriu System.lineSeparator()
flush() Bolca la memòria intermèdia al flux embolcallat
close() flush() + tancament en cascada

Dues coses a destacar:

newLine() davant de write("\n"). newLine() fa servir el separador del sistema; write("\n") escriu sempre LF. És la decisió de l'apartat 8 de 07-02: newLine() per a informes, "\n" fix per a fitxers de dades que es comparen o versionen.

A diferència de PrintWriter, BufferedWriter sí que llança IOException. Tots els seus mètodes la declaren. Aquest és un argument seriós al seu favor en codi on el contingut del fitxer importa: no hi ha cap checkError() que puguis oblidar.

Comparació de les dues formes d'escriure text amb memòria intermèdia:

BufferedWriter PrintWriter sobre BufferedWriter
Errors d'E/S Llança IOException Els desa: cal cridar checkError()
Formatatge No en té printf, format
Salt de línia newLine() explícit println() el posa
Escriure objectes Només String i char[] print(Object) fa servir toString()
Quan fer-la servir Dades: la fallada s'ha de detectar Informes i sortida llegible

  1. El trio PrintWriter + BufferedWriter + FileWriter

Aquesta cadena de tres capes és la més habitual per escriure text en Java, i ara pots explicar exactament què aporta cadascuna:

try (PrintWriter sortida = new PrintWriter(         // 3. formatatge
        new BufferedWriter(                          // 2. memoria intermedia
            new FileWriter(cami,                     // 1. fitxer + charset
                    StandardCharsets.UTF_8)))) {

    sortida.printf("%-25s %8.2f%n", "Java Eficac", 3.75);
}
Capa Tipus (07-03) Què aporta Què passa si la treus
FileWriter Node Connexió amb el fitxer i codificació No hi ha fitxer. És imprescindible
BufferedWriter Filtre Agrupa les escriptures; newLine() Funciona, però molt més lent
PrintWriter Filtre println, printf, print(Object) Funciona, però cal formatar a mà

Un matís honest que convé saber: PrintWriter ja té la seva pròpia memòria intermèdia interna, així que la capa intermèdia aporta menys del que la gent es pensa. En molts casos, new PrintWriter(new FileWriter(cami, UTF_8)) rendeix pràcticament igual. La cadena de tres s'escriu per costum i perquè és explícita, no perquè la millora sigui gran.

El que que canvia segons la capa exterior és la gestió d'errors, i aquesta és la decisió de veritat:

// Opcio A: PrintWriter. Comode, pero els errors cal preguntar-los.
try (PrintWriter sortida = new PrintWriter(
        new BufferedWriter(new FileWriter(cami, UTF_8)))) {
    sortida.printf("%-25s %8.2f%n", titol, multa);
    if (sortida.checkError()) {
        throw new IOException("Fallada en escriure " + cami);
    }
}

// Opcio B: BufferedWriter. Mes verbos, pero els errors es propaguen sols.
try (BufferedWriter sortida = new BufferedWriter(new FileWriter(cami, UTF_8))) {
    sortida.write(String.format("%-25s %8.2f", titol, multa));
    sortida.newLine();
}

L'opció B fa servir String.format per conservar el formatatge i BufferedWriter per conservar les excepcions. És la que farem servir a BiblioTech per als fitxers de dades; l'A queda per als informes de consulta.

  1. Files.newBufferedReader i newBufferedWriter

Des de Java 7 existeix una forma més curta de construir aquestes cadenes, que és la que veuràs en codi modern:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

// Lectura: equival a new BufferedReader(new InputStreamReader(
//          new FileInputStream(...), UTF_8))
try (BufferedReader lector = Files.newBufferedReader(
        Path.of("dades/cataleg.txt"), StandardCharsets.UTF_8)) {

    String linia;
    while ((linia = lector.readLine()) != null) {
        processar(linia);
    }
}

// Escriptura, amb opcions explicites
try (BufferedWriter escriptor = Files.newBufferedWriter(
        Path.of("dades/informe.txt"), StandardCharsets.UTF_8)) {
    escriptor.write("...");
    escriptor.newLine();
}

Avantatges sobre la construcció manual:

Manual Files.newBufferedReader
Longitud Tres constructors imbricats Una crida
Charset Es pot oblidar Paràmetre obligatori a la pràctica
Excepció si no existeix FileNotFoundException NoSuchFileException, més específica
Opcions d'obertura Només append StandardOpenOption completes

Aquesta és la forma recomanada en codi nou. Path, Files i StandardOpenOption són l'API NIO.2, que es desenvolupa sencera a 07-06. Aquí queda presentada perquè retorna exactament el BufferedReader d'aquesta lliçó: l'objecte és el mateix, només canvia com es construeix.

  1. Processar un fitxer gran amb memòria constant

Aquesta és la capacitat que fa important BufferedReader, més enllà del rendiment.

El problema de 07-01: carregar un fitxer sencer en memòria consumeix tanta memòria com el fitxer, i amb fitxers grans provoca OutOfMemoryError. La solució: processar en flux.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ProcessamentEnFlux {

    /**
     * Recorre un fitxer de QUALSEVOL mida amb memoria constant.
     *
     * En cada instant hi ha en memoria: la memoria intermedia (8 KB), la linia
     * actual i els acumuladors. Tant li fa que el fitxer tingui 1 MB o 50 GB.
     */
    public static Resum analitzar(String cami) throws IOException {
        long linies = 0;
        long caracters = 0;
        long liniesBuides = 0;
        int  maxLongitud = 0;
        String liniaMesLlarga = "";

        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {

            String linia;
            while ((linia = lector.readLine()) != null) {
                linies++;
                caracters += linia.length();

                if (linia.isBlank()) {
                    liniesBuides++;
                }
                if (linia.length() > maxLongitud) {
                    maxLongitud = linia.length();
                    liniaMesLlarga = linia;     // se'n desa UNA, no totes
                }
                // 'linia' queda disponible per al recollector a la volta seguent
            }
        }
        return new Resum(linies, caracters, liniesBuides, maxLongitud, liniaMesLlarga);
    }

    public record Resum(long linies, long caracters, long liniesBuides,
                        int maxLongitud, String liniaMesLlarga) { }
}

Comparació de memòria amb un fitxer de 2 GB:

Enfocament Memòria al pic Resultat
Files.readString() > 4 GB OutOfMemoryError
Files.readAllLines() > 5 GB (llista de milions d'String) OutOfMemoryError
BufferedReader línia a línia ~50 KB Funciona

La regla, que ja apuntava 07-01 i ara té la seva eina:

Si el fitxer pot créixer amb l'ús, processa'l en flux. Configuració, plantilles i fitxers de pocs KB es poden carregar sencers. Dades, registres, exportacions i importacions, mai.

I el matís important: processar en flux obliga a dissenyar l'algorisme d'una altra manera. Només pots fer un recorregut i només veus una línia cada vegada. Comptar, sumar, buscar el màxim o filtrar i escriure en un altre fitxer funcionen perfectament. Ordenar el fitxer sencer, no: això requereix una ordenació externa, que és un altre problema.

  1. Llegir de consola amb BufferedReader davant de Scanner

Des del mòdul 1 fas servir Scanner per a la consola. BufferedReader és l'alternativa:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class ConsolaAmbBufferedReader {

    public static void main(String[] args) throws IOException {
        // System.in es un InputStream (bytes): cal el PONT (07-03)
        BufferedReader consola = new BufferedReader(
                new InputStreamReader(System.in, StandardCharsets.UTF_8));

        System.out.print("Referencia del material: ");
        String referencia = consola.readLine();

        System.out.print("Dies de prestec: ");
        String text = consola.readLine();

        // BufferedReader NO analitza: cal convertir a ma
        int dies;
        try {
            dies = Integer.parseInt(text.trim());
        } catch (NumberFormatException e) {
            System.out.println("'" + text + "' no es un numero valid");
            return;
        }

        System.out.printf("Prestec de %s per %d dies%n", referencia, dies);

        // ATENCIO: NO es tanca el BufferedReader. Tancar-lo tancaria System.in
        // per a tota l'aplicacio, i no hi hauria forma de reobrir-lo (06-06).
    }
}

La comparació completa:

Scanner BufferedReader
Anàlisi de tipus nextInt, nextDouble, nextBoolean Cap: Integer.parseInt a mà
Llegir una línia nextLine() readLine()
Memòria intermèdia 1024 caràcters 8192 caràcters
Velocitat Més lenta: fa servir expressions regulars Notablement més ràpida
Excepcions No comprovades (InputMismatchException) Comprovada (IOException)
Final d'entrada hasNextLine() retorna false readLine() retorna null
Sensible al Locale : coma o punt decimal No: no analitza
Parany del salt de línia : nextInt() no el consumeix No: readLine() consumeix la línia sencera
Expressions regulars useDelimiter, hasNext(patro) No en té
Tancar System.in Perillós Perillós

Quan fer servir cadascun:

  • Scanner per a menús interactius i entrada per consola, que és el que fa MenuBiblioTech des del mòdul 2. La seva anàlisi compensa i el rendiment és irrellevant quan esperes que algú teclegi.
  • BufferedReader per a fitxers, sempre. En un fitxer de cent mil línies, la diferència de velocitat és de diversos segons.
  • BufferedReader per a entrada massiva per canonada, quan el programa rep dades per stdin des d'un altre procés.

I l'advertiment que ja coneixes de 06-07, aplicable a tots dos: no tanquis mai System.in. És un recurs global de l'aplicació; tancar-lo deixa el programa sense entrada per sempre. És l'excepció explícita a la regla de tancar-ho tot que 06-06 documentava.

  1. BiblioTech: ImportadorCataleg amb informe complet

Hora del cas pràctic gran de la lliçó, i el segon deute que se salda: a 06-06 vas declarar ImportadorCataleg com a esbós; aquí s'escriu sencer.

Els requisits, que són els d'una importació real:

  1. Llegir un fitxer de milers de línies amb memòria constant.
  2. Validar cada línia sense avortar la importació a la primera dolenta.
  3. Comptar correctes i descartades, amb el motiu de cada descart.
  4. Registrar els errors amb el logger, no en consola.
  5. Retornar un informe que qui crida pugui mostrar o desar.
  6. Ser transaccional en el que és raonable: no deixar el catàleg a mitges si falla el fitxer sencer.
package com.nexussoftware.bibliotech.servei;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.ReferenciaDuplicadaException;
import com.nexussoftware.bibliotech.infraestructura.FormatFitxers;

/**
 * Importacio massiva del cataleg de BiblioTech des d'un fitxer de text.
 *
 * Format esperat (una linia per material):
 *   LLIBRE;isbn;titol;autor;any
 *
 * Politica (06-07):
 *   - Una linia dolenta NO avorta la importacio: es descarta i es compta.
 *   - El fitxer absent o illegible SI que avorta: no hi ha res per importar.
 *   - Els materials s'acumulen i es bolquen al cataleg AL FINAL, per no
 *     deixar-lo a mitges si el fitxer resulta estar corrupte del tot.
 *
 * Processa en FLUX: memoria constant sigui quina sigui la mida del fitxer.
 */
public class ImportadorCataleg {

    private static final Logger LOG = Logger.getLogger(ImportadorCataleg.class.getName());

    private static final int CAMPS_ESPERATS = 5;
    private static final int ANY_MINIM = 1450;
    private static final int ANY_MAXIM = 2100;

    /** Si mes d'aquesta fraccio de linies falla, el fitxer es sospitos. */
    private static final double LLINDAR_FITXER_CORRUPTE = 0.5;

    /** Errors detallats que es desen; la resta nomes es compten. */
    private static final int MAX_ERRORS_DETALLATS = 100;

    private final File fitxer;

    public ImportadorCataleg(String cami) {
        this.fitxer = new File(Objects.requireNonNull(cami, "El cami no pot ser nul"));
    }

    // ------------------------- L'INFORME -------------------------

    /** Motiu pel qual es descarta una línia. Enum (04-07): sense cadenes magiques. */
    public enum MotiuDescart {
        NUMERO_DE_CAMPS("Nombre de camps incorrecte"),
        TIPUS_DESCONEGUT("Tipus de material no reconegut"),
        CAMP_BUIT("Camp obligatori buit"),
        ANY_NO_NUMERIC("L'any no es un numero"),
        ANY_FORA_DE_RANG("Any fora del rang admes"),
        ISBN_INVALID("L'ISBN no te el format esperat"),
        DUPLICAT("Referencia o ISBN ja presents");

        private final String descripcio;
        MotiuDescart(String descripcio) { this.descripcio = descripcio; }
        public String getDescripcio() { return descripcio; }
    }

    /** Una linia descartada, amb tot el necessari per corregir-la. */
    public record LiniaDescartada(int numero, MotiuDescart motiu, String detall,
                                  String contingut) {
        @Override
        public String toString() {
            return String.format("Linia %d [%s]: %s | %s",
                    numero, motiu.name(), detall, retallar(contingut));
        }
        private static String retallar(String s) {
            return (s.length() <= 60) ? s : s.substring(0, 57) + "...";
        }
    }

    /** Resultat complet de la importacio. */
    public static class Informe {
        private final List<Material> importats = new ArrayList<>();
        private final List<LiniaDescartada> descartades = new ArrayList<>();
        private final Map<MotiuDescart, Integer> perMotiu = new LinkedHashMap<>();

        private int liniesLlegides = 0;
        private int liniesIgnorades = 0;       // buides i comentaris
        private int descartsTotals = 0;        // inclou els no detallats
        private long millisegons = 0;
        private boolean avortada = false;
        private String motiuAvortament = null;

        void comptarLinia()     { liniesLlegides++; }
        void comptarIgnorada()  { liniesIgnorades++; }
        void afegir(Material m) { importats.add(m); }

        void descartar(LiniaDescartada d) {
            descartsTotals++;
            perMotiu.merge(d.motiu(), 1, Integer::sum);         // 05-05
            if (descartades.size() < MAX_ERRORS_DETALLATS) {
                descartades.add(d);
            }
        }

        void avortar(String motiu) {
            avortada = true;
            motiuAvortament = motiu;
        }

        public List<Material> getImportats()             { return List.copyOf(importats); }
        public List<LiniaDescartada> getDescartades()    { return List.copyOf(descartades); }
        public Map<MotiuDescart, Integer> getPerMotiu()  { return Map.copyOf(perMotiu); }
        public int getLiniesLlegides()   { return liniesLlegides; }
        public int getLiniesIgnorades()  { return liniesIgnorades; }
        public int getImportatsTotal()   { return importats.size(); }
        public int getDescartsTotals()   { return descartsTotals; }
        public long getMillisegons()     { return millisegons; }
        public boolean estaAvortada()    { return avortada; }
        public String getMotiuAvortament() { return motiuAvortament; }

        public double taxaExit() {
            int processades = importats.size() + descartsTotals;
            return (processades == 0) ? 0.0 : (importats.size() * 100.0) / processades;
        }

        /** Informe llegible per a la capa de presentacio. */
        public String resum() {
            StringBuilder sb = new StringBuilder();
            sb.append("=== INFORME D'IMPORTACIO ===\n");

            if (avortada) {
                sb.append("  IMPORTACIO AVORTADA: ").append(motiuAvortament).append('\n');
                return sb.toString();
            }

            sb.append(String.format("  Linies llegides  : %d%n", liniesLlegides));
            sb.append(String.format("  Ignorades        : %d (buides i comentaris)%n",
                    liniesIgnorades));
            sb.append(String.format("  Importades       : %d%n", importats.size()));
            sb.append(String.format("  Descartades      : %d%n", descartsTotals));
            sb.append(String.format("  Taxa d'exit      : %.1f%%%n", taxaExit()));
            sb.append(String.format("  Temps            : %d ms%n", millisegons));

            if (!perMotiu.isEmpty()) {
                sb.append("  --- Descarts per motiu ---\n");
                for (Map.Entry<MotiuDescart, Integer> e : perMotiu.entrySet()) {
                    sb.append(String.format("    %-22s %4d  (%s)%n",
                            e.getKey().name(), e.getValue(),
                            e.getKey().getDescripcio()));
                }
            }
            if (!descartades.isEmpty()) {
                sb.append("  --- Primeres linies descartades ---\n");
                int mostrades = Math.min(10, descartades.size());
                for (int i = 0; i < mostrades; i++) {
                    sb.append("    ").append(descartades.get(i)).append('\n');
                }
                if (descartsTotals > mostrades) {
                    sb.append(String.format("    ... i %d mes%n",
                            descartsTotals - mostrades));
                }
            }
            return sb.toString();
        }
    }

    // ------------------------- LA IMPORTACIO -------------------------

    /**
     * Importa el fitxer al cataleg donat.
     *
     * @return l'informe. Mai null, ni tan sols si avorta.
     */
    public Informe importar(Cataleg cataleg) {
        Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");

        Informe informe = new Informe();
        long inici = System.nanoTime();

        LOG.info(() -> "Iniciant importacio des de " + fitxer.getAbsolutePath());

        // BufferedReader: memoria constant encara que el fitxer tingui milions
        // de linies. Charset explicit, el mateix que fa servir l'exportador.
        try (BufferedReader lector = new BufferedReader(
                new FileReader(fitxer, FormatFitxers.CHARSET))) {

            String linia;
            int numero = 0;

            while ((linia = lector.readLine()) != null) {   // el bucle canonic
                numero++;
                informe.comptarLinia();
                processarLinia(linia, numero, informe);
            }

        } catch (java.io.FileNotFoundException e) {
            informe.avortar("No existeix el fitxer " + fitxer.getAbsolutePath());
            LOG.log(Level.WARNING, "Importacio avortada: fitxer no trobat", e);
            return finalitzar(informe, inici);

        } catch (IOException e) {
            informe.avortar("Fallada d'E/S llegint " + fitxer.getAbsolutePath()
                    + ": " + e.getMessage());
            LOG.log(Level.SEVERE, "Importacio avortada per fallada d'E/S", e);
            return finalitzar(informe, inici);
        }

        // Comprovacio de seny: si falla mes de la meitat, alguna cosa va malament
        // amb el fitxer sencer (charset equivocat, format diferent, columnes
        // canviades). Millor rebutjar-lo que ficar brossa al cataleg.
        int processades = informe.getImportatsTotal() + informe.getDescartsTotals();
        if (processades > 10
                && informe.getDescartsTotals() > processades * LLINDAR_FITXER_CORRUPTE) {

            informe.avortar(String.format(
                    "Ha fallat el %.0f%% de les linies (%d de %d). El fitxer no sembla "
                            + "tenir el format esperat; no s'importa res.",
                    100 - informe.taxaExit(), informe.getDescartsTotals(), processades));

            LOG.severe(() -> "Importacio rebutjada: " + informe.getMotiuAvortament());
            return finalitzar(informe, inici);
        }

        // Bolcat al cataleg AL FINAL: fins aqui no s'ha tocat res
        bolcar(cataleg, informe);
        return finalitzar(informe, inici);
    }

    /** Valida i construeix el material d'una linia. No llanca mai cap amunt. */
    private void processarLinia(String linia, int numero, Informe informe) {
        String neta = linia.trim();

        if (neta.isEmpty() || neta.startsWith(FormatFitxers.COMENTARI)) {
            informe.comptarIgnorada();
            return;
        }

        // El -1 conserva els camps buits del final (07-01, solucio 3)
        String[] camps = neta.split(FormatFitxers.SEPARADOR_CAMPS, -1);

        if (camps.length != CAMPS_ESPERATS) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.NUMERO_DE_CAMPS,
                    "n'hi ha " + camps.length + " i se n'esperaven " + CAMPS_ESPERATS,
                    neta));
            return;
        }

        String tipus = camps[0].trim().toUpperCase();
        String isbn  = camps[1].trim();
        String titol = camps[2].trim();
        String autor = camps[3].trim();
        String anyTx = camps[4].trim();

        if (!"LLIBRE".equals(tipus)) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.TIPUS_DESCONEGUT,
                    "'" + tipus + "'", neta));
            return;
        }
        if (titol.isEmpty() || isbn.isEmpty()) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.CAMP_BUIT,
                    titol.isEmpty() ? "titol" : "isbn", neta));
            return;
        }
        if (!isbn.matches("\\d{3}-\\d{10}")) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.ISBN_INVALID,
                    "'" + isbn + "' no compleix NNN-NNNNNNNNNN", neta));
            return;
        }

        int any;
        try {
            any = Integer.parseInt(anyTx);
        } catch (NumberFormatException e) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.ANY_NO_NUMERIC,
                    "'" + anyTx + "'", neta));
            return;
        }
        if (any < ANY_MINIM || any > ANY_MAXIM) {
            informe.descartar(new LiniaDescartada(numero, MotiuDescart.ANY_FORA_DE_RANG,
                    any + " fora de [" + ANY_MINIM + ", " + ANY_MAXIM + "]", neta));
            return;
        }

        informe.afegir(new Llibre(titol, autor.isEmpty() ? "Desconegut" : autor, isbn, any));
    }

    /**
     * Bolca al cataleg el que s'ha importat.
     *
     * Els duplicats es detecten AQUI, perque nomes el cataleg els coneix.
     * Un duplicat no invalida la importacio sencera: es descarta i es continua.
     */
    private void bolcar(Cataleg cataleg, Informe informe) {
        List<Material> pendents = informe.getImportats();
        List<Material> acceptats = new ArrayList<>(pendents.size());

        for (Material m : pendents) {
            try {
                cataleg.registrar(m);
                acceptats.add(m);
            } catch (ReferenciaDuplicadaException e) {
                informe.descartar(new LiniaDescartada(0, MotiuDescart.DUPLICAT,
                        e.getMessage(), m.getReferencia()));
            }
        }
        informe.importats.clear();
        informe.importats.addAll(acceptats);       // nomes el realment registrat
    }

    private Informe finalitzar(Informe informe, long inici) {
        informe.millisegons = (System.nanoTime() - inici) / 1_000_000;

        if (informe.estaAvortada()) {
            LOG.warning(() -> "Importacio avortada: " + informe.getMotiuAvortament());
        } else {
            LOG.info(() -> String.format(
                    "Importacio acabada: %d importats, %d descartats, %d ms",
                    informe.getImportatsTotal(), informe.getDescartsTotals(),
                    informe.getMillisegons()));
        }
        return informe;
    }
}

I el seu ús des de la presentació:

package com.nexussoftware.bibliotech.presentacio;

import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.ImportadorCataleg;

public class DemoImportacio {

    public static void main(String[] args) {
        Cataleg cataleg = new Cataleg();

        ImportadorCataleg importador =
                new ImportadorCataleg("dades/cataleg-complet.txt");

        ImportadorCataleg.Informe informe = importador.importar(cataleg);

        System.out.println(informe.resum());
        System.out.println("Materials al cataleg: " + cataleg.mida());
    }
}

Sortida amb un fitxer de 5 000 línies amb alguns errors:

=== INFORME D'IMPORTACIO ===
  Linies llegides  : 5003
  Ignorades        : 3 (buides i comentaris)
  Importades       : 4962
  Descartades      : 38
  Taxa d'exit      : 99.2%
  Temps            : 87 ms
  --- Descarts per motiu ---
    ANY_NO_NUMERIC           14  (L'any no es un numero)
    ISBN_INVALID             11  (L'ISBN no te el format esperat)
    NUMERO_DE_CAMPS           7  (Nombre de camps incorrecte)
    DUPLICAT                  4  (Referencia o ISBN ja presents)
    CAMP_BUIT                 2  (Camp obligatori buit)
  --- Primeres linies descartades ---
    Linia 47 [ANY_NO_NUMERIC]: 'mil nou-cents' | LLIBRE;978-0000000047;Un llibre;Autor;mil...
    Linia 112 [NUMERO_DE_CAMPS]: n'hi ha 4 i se n'esperaven 5 | LLIBRE;978-0000000112;Altre;A
    Linia 340 [ISBN_INVALID]: '97800000340' no compleix NNN-NNNNNNNNNN | LLIBRE;97800000340;
    ... i 35 mes

Les set decisions de disseny que cal entendre d'aquesta classe:

  1. BufferedReader i bucle canònic. Memòria constant: el fitxer podria tenir cinc milions de línies i el consum seria el mateix.
  2. Una línia dolenta no avorta. Es descarta amb el seu motiu i el seu número de línia. Qui importa vol veure tots els problemes de cop.
  3. Un fitxer majoritàriament dolent sí que avorta. Si falla més de la meitat, el fitxer no és el que s'esperava —charset equivocat, columnes canviades, format diferent— i ficar aquella brossa al catàleg seria pitjor que no importar. És la distinció recuperable/irrecuperable de 06-07 aplicada a la importació.
  4. El bolcat al catàleg va al final. Fins aquell moment el catàleg no s'ha tocat, així que un avortament el deixa exactament com estava. És la coherència d'estat de 06-05, en versió importació.
  5. Els detalls es limiten a 100. Un fitxer corrupte amb un milió de línies dolentes generaria un milió d'objectes LiniaDescartada i esgotaria la memòria. Es compten totes i es detallen les cent primeres: és informació suficient per corregir, i acotada.
  6. MotiuDescart és un enum, no una cadena. Permet agrupar, comptar i traduir, i elimina les cadenes màgiques. És 04-07 aplicat.
  7. Tot el diagnòstic va al logger; l'informe es retorna. La classe de servei no imprimeix: retorna un objecte i qui la crida decideix si el mostra per consola, el desa o l'envia per correu. És la separació de capes de 06-07.

Errors Comuns i Consells

  • Fer servir ready() com a condició de final. El bug que funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes. La condició correcta és sempre readLine() != null.
  • Cridar readLine() dues vegades per volta. Processa les línies parelles i se salta les senars. Amb un fitxer de prova petit pot semblar correcte.
  • Confondre null amb "". null és final de fitxer; "" és una línia buida. Fer servir isEmpty() com a condició de final atura la lectura al primer buit i després llança NullPointerException.
  • Oblidar els parèntesis a while ((linia = lector.readLine()) != null). No compila, i és la forma idiomàtica que cal memoritzar.
  • Esperar que readLine() inclogui el salt de línia. No l'inclou. Si estàs reescrivint el fitxer, l'has de reposar amb newLine().
  • No tancar el BufferedWriter. El fitxer queda buit: les dades són a la memòria intermèdia. En escriptura, no tancar és perdre dades (07-02).
  • Creure que BufferedWriter escriu immediatament. No ho fa, i per disseny. Si necessites que un altre procés vegi les dades ja, flush().
  • Tancar System.in. Deixa l'aplicació sense entrada per sempre. És l'excepció documentada a la regla de tancar-ho tot.
  • Embolcallar una cosa que ja té memòria intermèdia. new BufferedReader(new BufferedReader(...)) no aporta res; sobre un array a la RAM, tampoc.
  • Posar una memòria intermèdia enorme "per si de cas". Cent fluxos amb memòries intermèdies d'1 MB són 100 MB de heap, i a partir de 64 KB la millora és menyspreable.
  • Fer servir PrintWriter sense checkError() per a dades que importen. S'empassa les IOException. Per a fitxers de dades, BufferedWriter, els mètodes del qual sí que les declaren.
  • Fer servir Scanner per llegir un fitxer de cent mil línies. Funciona i triga diversos segons de més. BufferedReader per a fitxers, Scanner per a menús.
  • Acumular totes les línies en una llista "per processar-les després". Anul·la l'avantatge del processament en flux i reintrodueix l'OutOfMemoryError.
  • Desar un objecte d'error per cada línia dolenta sense límit. Un fitxer corrupte d'un milió de línies esgota la memòria mentre informa que hi ha un problema.
  • Consell: porta sempre el número de línia. "Línia 4 217: l'any no és un nombre" és accionable; "error en importar" no ho és. És 06-04 aplicat a fitxers.
  • Consell: posa un llindar de seny a tota importació. Si falla més de la meitat, el problema és el fitxer, no les línies. Rebutjar-lo sencer és més correcte que importar brossa.
  • Consell: que el servei retorni l'informe, no el imprimeixi. Qui crida decideix si el mostra, el desa o l'envia. La capa de servei no sap qui hi ha a l'altre costat.
  • Consell: fes servir Files.newBufferedReader en codi nou. És més curt, obliga al charset i dona excepcions més específiques. El veuràs sencer a 07-06.

Exercicis

Exercici 1: comptador d'estadístiques d'un fitxer

Escriu EstadistiquesFitxer que analitzi un fitxer de text en un sol recorregut i amb memòria constant, produint:

  1. Nombre de línies, de paraules i de caràcters.
  2. Línies buides i línies de comentari (que comencen per #).
  3. Longitud mitjana, mínima i màxima de línia.
  4. Les tres paraules més freqüents (fes servir un HashMap del mòdul 5; no facis servir l'API de Streams, que és 10-04).
  5. El número de la línia més llarga i els seus primers 60 caràcters.

Requisits: BufferedReader, charset explícit, try-with-resources, i un record per al resultat. Ha de funcionar igual amb un fitxer d'1 KB que amb un d'1 GB.

Exercici 2: filtre de línies amb reescriptura

Escriu FiltreCataleg que llegeixi un fitxer de catàleg i escrigui un altre fitxer només amb les línies que compleixin un criteri, sense carregar cap dels dos en memòria:

  1. filtrar(String origen, String desti, Predicat criteri) on Predicat és una interfície funcional pròpia (04-06) amb boolean compleix(String linia, int numero).
  2. Conserva les línies de comentari i la capçalera.
  3. Compta línies llegides, escrites i descartades, i retorna el resultat en un record.
  4. Fes servir escriptura atòmica (07-02): escriu a un temporal i reanomena al final.
  5. Un main que filtri per any de publicació posterior a 2000 i per autor que contingui un text donat.

Exercici 3: comparador de dos catàlegs

Escriu ComparadorCatalegs que compari dos fitxers de catàleg i informi de les diferències:

  1. Materials només al primer (baixes).
  2. Materials només al segon (altes).
  3. Materials a tots dos però amb dades diferents (modificacions), indicant quin camp ha canviat.
  4. Materials idèntics (només el recompte).

Requisits: llegeix cada fitxer una sola vegada amb BufferedReader, indexa per ISBN amb un HashMap (05-05), i produeix un informe amb printf. Explica en un comentari per què aquest algorisme que necessita carregar un dels dos fitxers en memòria i quin convé que sigui, i què faries si cap dels dos no hi cabés.

Solucions

Solució 1

package com.nexussoftware.bibliotech.util;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Estadistiques d'un fitxer de text en UN SOL RECORREGUT i amb memoria
 * constant respecte a la mida del fitxer.
 *
 * L'unic que creix es el mapa de frequencies, que depen del nombre de
 * paraules DIFERENTS, no de la mida del fitxer. Un fitxer de 10 GB en
 * catala te de l'ordre de cent mil paraules diferents: perfectament
 * manejable.
 */
public class EstadistiquesFitxer {

    private static final int PARAULES_TOP = 3;
    private static final int LONGITUD_MOSTRA = 60;

    public record Estadistiques(
            long linies, long paraules, long caracters,
            long liniesBuides, long liniesComentari,
            double longitudMitjana, int longitudMinima, int longitudMaxima,
            int numeroLiniaMesLlarga, String mostraLiniaMesLlarga,
            List<Map.Entry<String, Integer>> paraulesFrequents) {

        public String informe() {
            StringBuilder sb = new StringBuilder();
            sb.append("=== ESTADISTIQUES DEL FITXER ===\n");
            sb.append(String.format("  Linies            : %d%n", linies));
            sb.append(String.format("    buides          : %d%n", liniesBuides));
            sb.append(String.format("    comentaris      : %d%n", liniesComentari));
            sb.append(String.format("  Paraules          : %d%n", paraules));
            sb.append(String.format("  Caracters         : %d%n", caracters));
            sb.append(String.format("  Longitud mitjana  : %.1f%n", longitudMitjana));
            sb.append(String.format("  Longitud minima   : %d%n", longitudMinima));
            sb.append(String.format("  Longitud maxima   : %d (linia %d)%n",
                    longitudMaxima, numeroLiniaMesLlarga));
            sb.append(String.format("    mostra          : %s%n", mostraLiniaMesLlarga));
            sb.append("  Paraules mes frequents:\n");
            for (Map.Entry<String, Integer> e : paraulesFrequents) {
                sb.append(String.format("    %-20s %6d%n", e.getKey(), e.getValue()));
            }
            return sb.toString();
        }
    }

    public static Estadistiques analitzar(String cami) throws IOException {
        long linies = 0, paraules = 0, caracters = 0;
        long buides = 0, comentaris = 0;
        int  minima = Integer.MAX_VALUE, maxima = 0;
        int  numeroMesLlarga = 0;
        String mostraMesLlarga = "";

        // Nomes aixo creix, i amb el nombre de paraules DIFERENTS
        Map<String, Integer> frequencies = new HashMap<>();

        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {

            String linia;
            while ((linia = lector.readLine()) != null) {     // bucle canonic
                linies++;
                caracters += linia.length();

                int longitud = linia.length();
                if (longitud < minima) { minima = longitud; }
                if (longitud > maxima) {
                    maxima = longitud;
                    numeroMesLlarga = (int) linies;
                    mostraMesLlarga = longitud <= LONGITUD_MOSTRA
                            ? linia
                            : linia.substring(0, LONGITUD_MOSTRA) + "...";
                }

                String neta = linia.trim();
                if (neta.isEmpty()) {
                    buides++;
                    continue;
                }
                if (neta.startsWith("#")) {
                    comentaris++;
                    continue;
                }

                // Separa per qualsevol cosa que no sigui lletra o digit
                for (String paraula : neta.toLowerCase().split("[^\\p{L}\\p{N}]+")) {
                    if (paraula.length() > 2) {           // ignora articles curts
                        paraules++;
                        frequencies.merge(paraula, 1, Integer::sum);   // 05-05
                    }
                }
                // 'linia' es descarta aqui: memoria constant
            }
        }

        if (minima == Integer.MAX_VALUE) {
            minima = 0;                                   // fitxer buit
        }

        return new Estadistiques(
                linies, paraules, caracters, buides, comentaris,
                linies == 0 ? 0.0 : (double) caracters / linies,
                minima, maxima, numeroMesLlarga, mostraMesLlarga,
                mesFrequents(frequencies, PARAULES_TOP));
    }

    /**
     * Les n entrades amb valor mes gran.
     *
     * Sense l'API de Streams (10-04): llista + Comparator + sort, que es
     * exactament el de 05-09.
     */
    private static List<Map.Entry<String, Integer>> mesFrequents(
            Map<String, Integer> frequencies, int n) {

        List<Map.Entry<String, Integer>> entrades = new ArrayList<>(frequencies.entrySet());

        // Descendent per valor; a igualtat, alfabetic per clau
        entrades.sort(Comparator
                .comparing(Map.Entry<String, Integer>::getValue).reversed()
                .thenComparing(Map.Entry::getKey));

        return List.copyOf(entrades.subList(0, Math.min(n, entrades.size())));
    }

    public static void main(String[] args) throws IOException {
        String cami = (args.length > 0) ? args[0] : "dades/cataleg.txt";
        System.out.println(analitzar(cami).informe());
    }
}

Sortida:

=== ESTADISTIQUES DEL FITXER ===
  Linies            : 5003
  Linies buides     : 1
  Comentaris        : 2
  Paraules          : 19842
  Caracters         : 248915
  Longitud mitjana  : 49.8
  Longitud minima   : 0
  Longitud maxima   : 87 (linia 1204)
    mostra          : LLIBRE;978-0000001204;Introduccio als sistemes distribuits...
  Paraules mes frequents:
    llibre                 5000
    programacio             412
    sistemes                287

El punt clau és al comentari de capçalera: l'única dada que creix amb el contingut és el mapa de freqüències, i creix amb el nombre de paraules diferents, no amb la mida del fitxer. Tota la resta són acumuladors de mida fixa. Per això aquesta anàlisi funciona amb un fitxer de 10 GB. Fixa't també que la "línia més llarga" es desa retallada: desar-la sencera seria un risc si aquella línia tingués 100 MB.

Solució 2

package com.nexussoftware.bibliotech.servei;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;

/**
 * Filtra un fitxer de cataleg escrivint-ne un altre amb les linies que
 * compleixin un criteri, SENSE carregar cap dels dos en memoria.
 *
 * Llegeix i escriu en flux simultaniament: en cada instant hi ha en memoria
 * una linia i dues memories intermedies de 8 KB.
 */
public class FiltreCataleg {

    private static final Logger LOG = Logger.getLogger(FiltreCataleg.class.getName());

    private static final String COMENTARI = "#";

    /**
     * Criteri de filtratge. Interficie funcional propia (04-06): rep la
     * linia i el seu numero, per si el criteri depen de la posicio.
     */
    @FunctionalInterface
    public interface Predicat {
        boolean compleix(String linia, int numero);
    }

    public record ResultatFiltre(int llegides, int escrites, int descartades,
                                 int comentarisConservats, long millisegons) {

        public String resum() {
            return String.format(
                    "Filtrat: %d llegides, %d escrites, %d descartades, "
                            + "%d comentaris conservats (%d ms)",
                    llegides, escrites, descartades, comentarisConservats, millisegons);
        }
    }

    /**
     * Filtra origen cap a desti de forma ATOMICA (07-02).
     *
     * Si alguna cosa falla a mitges, el desti anterior queda intacte.
     */
    public ResultatFiltre filtrar(String origen, String desti, Predicat criteri)
            throws IOException {

        long inici = System.nanoTime();

        File fitxerDesti = new File(desti);
        File temporal = new File(fitxerDesti.getAbsolutePath() + ".tmp");

        int llegides = 0, escrites = 0, descartades = 0, comentaris = 0;
        boolean completat = false;

        try {
            // Lectura i escriptura simultanies, totes dues amb memoria intermedia.
            // Es declaren els dos recursos al mateix try-with-resources:
            // es tanquen en ordre invers, l'escriptor primer (06-06).
            try (BufferedReader lector = new BufferedReader(
                         new FileReader(origen, StandardCharsets.UTF_8));
                 BufferedWriter escriptor = new BufferedWriter(
                         new FileWriter(temporal, StandardCharsets.UTF_8))) {

                String linia;
                int numero = 0;

                while ((linia = lector.readLine()) != null) {
                    numero++;
                    llegides++;

                    String neta = linia.trim();

                    // Comentaris i capcaleres: es conserven sempre
                    if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
                        escriptor.write(linia);
                        escriptor.newLine();
                        comentaris++;
                        continue;
                    }

                    if (criteri.compleix(linia, numero)) {
                        escriptor.write(linia);
                        escriptor.newLine();         // readLine NO retorna el salt
                        escrites++;
                    } else {
                        descartades++;
                    }
                }
                // BufferedWriter llanca IOException de veritat: sense checkError
            }

            // Substitucio atomica
            if (fitxerDesti.exists() && !fitxerDesti.delete()) {
                throw new IOException("No s'ha pogut substituir " + desti);
            }
            if (!temporal.renameTo(fitxerDesti)) {
                throw new IOException("No s'ha pogut reanomenar " + temporal.getName());
            }
            completat = true;

        } finally {
            if (!completat && temporal.exists() && !temporal.delete()) {
                LOG.warning(() -> "Temporal sense esborrar: " + temporal.getAbsolutePath());
            }
        }

        ResultatFiltre resultat = new ResultatFiltre(llegides, escrites, descartades,
                comentaris, (System.nanoTime() - inici) / 1_000_000);

        LOG.info(resultat::resum);
        return resultat;
    }

    // ------------------------- CRITERIS -------------------------

    /** Any de publicacio posterior al donat. Camp 5 del format. */
    public static Predicat publicatDespresDe(int any) {
        return (linia, numero) -> {
            String[] camps = linia.split(";", -1);
            if (camps.length < 5) {
                return false;
            }
            try {
                return Integer.parseInt(camps[4].trim()) > any;
            } catch (NumberFormatException e) {
                return false;               // any illegible: no compleix
            }
        };
    }

    /** L'autor conte el text donat, sense distingir majuscules. */
    public static Predicat autorConte(String text) {
        String buscat = text.toLowerCase();
        return (linia, numero) -> {
            String[] camps = linia.split(";", -1);
            return camps.length >= 4 && camps[3].toLowerCase().contains(buscat);
        };
    }

    /** Combinacio de dos criteris (composicio de lambdes, 04-05). */
    public static Predicat i(Predicat a, Predicat b) {
        return (linia, numero) -> a.compleix(linia, numero) && b.compleix(linia, numero);
    }

    public static void main(String[] args) throws IOException {
        FiltreCataleg filtre = new FiltreCataleg();

        System.out.println(filtre.filtrar(
                "dades/cataleg.txt",
                "dades/cataleg-recents.txt",
                publicatDespresDe(2000)).resum());

        System.out.println(filtre.filtrar(
                "dades/cataleg.txt",
                "dades/cataleg-bloch.txt",
                autorConte("bloch")).resum());

        System.out.println(filtre.filtrar(
                "dades/cataleg.txt",
                "dades/cataleg-recents-fowler.txt",
                i(publicatDespresDe(1995), autorConte("fowler"))).resum());
    }
}

Sortida:

Filtrat: 5003 llegides, 3418 escrites, 1582 descartades, 3 comentaris conservats (61 ms)
Filtrat: 5003 llegides, 12 escrites, 4988 descartades, 3 comentaris conservats (48 ms)
Filtrat: 5003 llegides, 7 escrites, 4993 descartades, 3 comentaris conservats (52 ms)

Els quatre punts didàctics:

  1. Lectura i escriptura simultànies en flux. Cap dels dos fitxers no es carrega: el consum és d'una línia i dues memòries intermèdies, sigui quina sigui la mida. És el patró que fan servir les eines de línia d'ordres de tota la vida.
  2. Els dos recursos van al mateix try-with-resources. Es tanquen en ordre invers —l'escriptor primer— i les excepcions de tancament es registren com a suprimides (06-06). Sense aquest tancament, la memòria intermèdia de l'escriptor no es buidaria.
  3. escriptor.newLine() és obligatori. readLine() no retorna el terminador, així que si no el reposes, el fitxer de sortida surt amb totes les línies enganxades. És l'error més freqüent en reescriure fitxers.
  4. La interfície funcional pròpia permet compondre criteris amb lambdes i combinar-los amb i(...), exactament com ReglaTarifa i FiltreMaterial de 04-06. A 10-04 veuràs que l'API de Streams té això mateix amb Predicate i and.

Solució 3

package com.nexussoftware.bibliotech.servei;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Compara dos fitxers de cataleg i informa d'altes, baixes i canvis.
 *
 * PER QUE AQUEST ALGORISME SI QUE NECESSITA MEMORIA:
 * Per saber si un material del fitxer B existeix a A, cal poder
 * consultar-lo. Com que els fluxos son SEQUENCIALS i no permeten tornar
 * enrere (07-03), l'unica forma de consultar A mentre es recorre B es tenir
 * A indexat en memoria.
 *
 * QUIN CONVE CARREGAR: el mes petit. Es recorre en flux el gran.
 *
 * SI CAP NO HI CAP: hi ha dues sortides classiques. Ordenar tots dos fitxers
 * per clau i fer-ne una "fusio" recorrent-los en parallel, que necessita
 * memoria constant; o carregar nomes les CLAUS (els ISBN) amb un hash de
 * cada linia, que ocupa una fraccio minima. La segona es l'habitual.
 */
public class ComparadorCatalegs {

    private static final String SEPARADOR = ";";
    private static final String COMENTARI = "#";

    /** Dades d'un material tal com apareixen al fitxer. */
    private record Registre(String isbn, String titol, String autor, int any) {

        /** Quins camps difereixen respecte a un altre registre amb el mateix ISBN. */
        List<String> diferenciesAmb(Registre altre) {
            List<String> canvis = new ArrayList<>();
            if (!titol.equals(altre.titol)) {
                canvis.add("titol: '" + titol + "' -> '" + altre.titol + "'");
            }
            if (!autor.equals(altre.autor)) {
                canvis.add("autor: '" + autor + "' -> '" + altre.autor + "'");
            }
            if (any != altre.any) {
                canvis.add("any: " + any + " -> " + altre.any);
            }
            return canvis;
        }
    }

    public record Diferencies(List<String> baixes, List<String> altes,
                              List<String> modificacions, int identics) {

        public String informe() {
            StringBuilder sb = new StringBuilder();
            sb.append("=== COMPARACIO DE CATALEGS ===\n");
            sb.append(String.format("  Identics       : %d%n", identics));
            sb.append(String.format("  Altes          : %d%n", altes.size()));
            sb.append(String.format("  Baixes         : %d%n", baixes.size()));
            sb.append(String.format("  Modificacions  : %d%n", modificacions.size()));

            imprimirBloc(sb, "ALTES (nomes al nou)", altes);
            imprimirBloc(sb, "BAIXES (nomes a l'antic)", baixes);
            imprimirBloc(sb, "MODIFICACIONS", modificacions);
            return sb.toString();
        }

        private void imprimirBloc(StringBuilder sb, String titol, List<String> linies) {
            if (linies.isEmpty()) {
                return;
            }
            sb.append("  --- ").append(titol).append(" ---\n");
            int mostrades = Math.min(10, linies.size());
            for (int i = 0; i < mostrades; i++) {
                sb.append("    ").append(linies.get(i)).append('\n');
            }
            if (linies.size() > mostrades) {
                sb.append(String.format("    ... i %d mes%n", linies.size() - mostrades));
            }
        }
    }

    /**
     * Compara dos catalegs.
     *
     * @param camiAntic el que es carrega EN MEMORIA (conve el mes petit)
     * @param camiNou   el que es recorre EN FLUX
     */
    public Diferencies comparar(String camiAntic, String camiNou) throws IOException {

        // FASE 1: indexar l'antic. Un sol recorregut.
        Map<String, Registre> antics = indexar(camiAntic);

        List<String> altes = new ArrayList<>();
        List<String> modificacions = new ArrayList<>();
        int identics = 0;

        // FASE 2: recorrer el nou EN FLUX, consultant l'index
        try (BufferedReader lector = new BufferedReader(
                new FileReader(camiNou, StandardCharsets.UTF_8))) {

            String linia;
            while ((linia = lector.readLine()) != null) {
                Registre nou = parsejar(linia);
                if (nou == null) {
                    continue;                        // comentari o linia dolenta
                }

                // remove() en lloc de get(): el que quedi al final son les BAIXES.
                // Estalvia un segon recorregut i una estructura addicional.
                Registre antic = antics.remove(nou.isbn());

                if (antic == null) {
                    altes.add(nou.isbn() + " - " + nou.titol());
                    continue;
                }

                List<String> canvis = antic.diferenciesAmb(nou);
                if (canvis.isEmpty()) {
                    identics++;
                } else {
                    modificacions.add(nou.isbn() + " - " + String.join("; ", canvis));
                }
            }
        }

        // FASE 3: el que sobra de l'index son les baixes
        List<String> baixes = new ArrayList<>();
        for (Registre r : antics.values()) {
            baixes.add(r.isbn() + " - " + r.titol());
        }

        baixes.sort(null);                           // ordre natural (05-09)
        altes.sort(null);
        modificacions.sort(null);

        return new Diferencies(baixes, altes, modificacions, identics);
    }

    /** Carrega un cataleg indexat per ISBN. Un sol recorregut, amb memoria intermedia. */
    private Map<String, Registre> indexar(String cami) throws IOException {
        Map<String, Registre> index = new HashMap<>();

        try (BufferedReader lector = new BufferedReader(
                new FileReader(cami, StandardCharsets.UTF_8))) {

            String linia;
            while ((linia = lector.readLine()) != null) {
                Registre r = parsejar(linia);
                if (r != null) {
                    index.put(r.isbn(), r);
                }
            }
        }
        return index;
    }

    /** Retorna null si la linia no es un registre valid. */
    private Registre parsejar(String linia) {
        String neta = linia.trim();
        if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
            return null;
        }
        String[] c = neta.split(SEPARADOR, -1);
        if (c.length != 5) {
            return null;
        }
        try {
            return new Registre(c[1].trim(), c[2].trim(), c[3].trim(),
                    Integer.parseInt(c[4].trim()));
        } catch (NumberFormatException e) {
            return null;
        }
    }

    public static void main(String[] args) throws IOException {
        Diferencies d = new ComparadorCatalegs()
                .comparar("dades/cataleg-2025.txt", "dades/cataleg-2026.txt");
        System.out.println(d.informe());
    }
}

Sortida:

=== COMPARACIO DE CATALEGS ===
  Identics       : 4871
  Altes          : 92
  Baixes         : 37
  Modificacions  : 14
  --- ALTES (nomes al nou) ---
    978-0000005001 - Arquitectura hexagonal
    978-0000005002 - Java modern
    ... i 90 mes
  --- BAIXES (nomes a l'antic) ---
    978-0000000341 - Manual de COBOL
    ... i 36 mes
  --- MODIFICACIONS ---
    978-0000000002 - titol: 'Patrons de Disseny' -> 'Patrons de Disseny i Refactoritzacio'
    978-0000000047 - any: 2018 -> 2019
    ... i 12 mes

Els tres punts que cal extreure'n:

  1. El comentari de capçalera respon a la pregunta de l'enunciat. Un flux és seqüencial i no permet tornar enrere, així que consultar un fitxer mentre en recorres un altre obliga a tenir-ne un en memòria. Carrega el petit, recorre el gran. I si cap no hi cap, les dues sortides clàssiques són la fusió de fitxers ordenats —memòria constant— o carregar només les claus amb un hash de cada línia, que és una fracció mínima del contingut.
  2. remove() en lloc de get(). Un truc que estalvia feina: en treure de l'índex el que es troba, el que queda al final són exactament les baixes. Sense ell calarien un segon recorregut i una estructura addicional.
  3. Fixa't en la primera modificació de la sortida: un títol que canvia perquè algú l'ha completat a mà en una de les dues versions del fitxer. És exactament el tipus de diferència que un comparador de catàlegs treu a la llum, i una raó més per mantenir una única font de veritat del catàleg.

Conclusió

Ja saps què és una memòria intermèdia i per què és a tot arreu.

Entens que és un array intermedi que agrupa les operacions per reduir les crides al sistema: una de cada 8192 en lloc de totes. Tens les xifres que ho justifiquen —de 4 500 ms a 68 ms només embolcallant el flux, un factor de seixanta-sis— i saps que el preu són setze kilobytes de memòria, probablement el millor intercanvi de memòria per temps que existeix. I ara tens també l'explicació completa de dues coses que vas acceptar abans: per què FileReader.read() és tan lent, i per què un fitxer acabat d'escriure té zero bytes.

Saps construir-les per decoració sobre FileReader/FileWriter o sobre un pont, i saps triar la mida: 8192 tret que hagis mesurat, amb l'advertiment que cent fluxos amb memòries intermèdies d'un megabyte són cent megabytes de heap, i que embolcallar una cosa que ja en té no aporta res.

Domines readLine() i els seus quatre punts de contracte: retorna la línia sense el terminador, reconeix les tres convencions de final de línia —per això un fitxer de Windows es llegeix sense problema a Linux—, retorna null al final i no "", i bloqueja fins a tenir una línia completa. Escrius el bucle canònic sense dubtar, amb els parèntesis obligatoris i != null com a única condició correcta, i reconeixes a l'instant els tres errors que el substitueixen: l'isEmpty() que s'atura a la primera línia buida, el doble readLine() que processa una de cada dues, i el ready() que funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes. Saps també que readLine() carrega la línia sencera en memòria, i què significa això amb un fitxer d'una sola línia gegant.

Coneixes mark/reset per inspeccionar el principi d'un flux sense perdre'l, amb els seus límits reals. I coneixes BufferedWriter amb el seu newLine() portable i —l'argument que més pesa— els seus mètodes sí que declaren IOException, a diferència de PrintWriter, que se les empassa. D'aquí la regla: PrintWriter per a informes, BufferedWriter per a dades la fallada de les quals cal detectar. Saps què aporta exactament cada capa del trio PrintWriter + BufferedWriter + FileWriter, inclosa l'observació honesta que la capa intermèdia aporta menys del que es creu, perquè PrintWriter ja té la seva pròpia memòria intermèdia.

Saps que Files.newBufferedReader i newBufferedWriter construeixen exactament aquests objectes de forma més curta, obligant al charset i amb excepcions més específiques, i que aquesta és la forma recomanada en codi nou. És NIO.2, i és la lliçó següent.

I tens la capacitat que va més enllà del rendiment: el processament en flux. Un fitxer de dos gigabytes que peta amb readString() o readAllLines() es recorre amb BufferedReader fent servir cinquanta kilobytes. Saps que això obliga a dissenyar l'algorisme d'una altra manera —un sol recorregut, una línia cada vegada— i què es pot i què no es pot fer així. Coneixes a més la comparació completa entre BufferedReader i Scanner, amb el criteri per triar: Scanner per a menús interactius, BufferedReader per a fitxers i entrada massiva; i l'advertiment, un altre cop, de no tancar mai System.in.

BiblioTech importa de veritat. ImportadorCataleg recorre milers de línies amb memòria constant, valida cadascuna contra set regles, descarta les dolentes amb el seu número de línia i el seu motiu tipat en un enum, compta totes i detalla les cent primeres per no esgotar la memòria informant d'un problema, avorta la importació sencera si falla més de la meitat —perquè llavors el problema és el fitxer, no les línies—, bolca al catàleg només al final per no deixar-lo a mitges, i retorna un informe en lloc d'imprimir res, perquè una classe de servei no sap qui hi ha a l'altre costat. És el mòdul 5, el mòdul 6 i aquest, funcionant junts en una sola classe.

Fins ara, tot el que BiblioTech desa és text que tu has decidit com formatar: LLIBRE;isbn;titol;autor;any. Funciona, però té un límit evident. I si volguessis desar l'estat complet del sistema —els préstecs amb les seves referències a materials i empleats, les reserves, l'historial, les incidències imbricades— amb totes les seves relacions intactes? Escriure aquest graf d'objectes a mà, amb les seves referències creuades i els seus objectes compartits, seria una feina enorme i plena de detalls.

A la lliçó 07-05, Serialització, veuràs que Java ho sap fer sol. Coneixeràs què significa convertir un graf d'objectes en bytes conservant les referències compartides; la interfície marcadora Serializable, que reprèn les interfícies marcadores de 04-01, amb ObjectOutputStream i ObjectInputStream; què es desa i què no, amb transient i static; el serialVersionUID que ja vas declarar a les teves excepcions del mòdul 6 sense saber del tot per a què servia, i la InvalidClassException que apareix quan canvia la classe; quins canvis d'una classe són compatibles i quins trenquen els fitxers existents; i —el més important de la lliçó— els riscos de seguretat reals de deserialitzar dades que no controles, que és una de les vies d'atac més explotades de la història de Java. En acabar-la, BiblioTech desarà i restaurarà una sessió completa entre execucions, i sabràs exactament quan no has de fer servir aquesta eina.

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