Les dues lliçons anteriors van deixar una pregunta sense resposta. Has escrit new PrintWriter(new FileWriter(cami, UTF_8)) i has acceptat que funciona, però per què PrintWriter embolcalla FileWriter en lloc de substituir-lo? Per què existeixen classes acabades en Reader i Writer i altres acabades en InputStream i OutputStream? I com es copia la imatge de portada d'un llibre, que no és text i que cap FileReader no pot llegir sense destrossar-la?

Aquesta lliçó respon a les tres. java.io no és una col·lecció de classes soltes que cal memoritzar: és un disseny amb una estructura molt deliberada, basada en dues idees —el flux com a abstracció i el decorador com a forma de compondre— que, un cop enteses, converteixen cinquanta classes en un grapat de regles.

En acabar sabràs mirar qualsevol expressió com new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f))) i llegir exactament què fa cada parèntesi, en quin ordre flueixen els bytes i què passa si en treus una capa. I BiblioTech podrà gestionar les portades dels seus llibres, que són fitxers binaris.

try-with-resources a tots els exemples, com sempre des de 06-06. Aquí adquireix un matís nou que convé tenir present: tancar el decorador exterior tanca tota la cadena, i això és exactament el que vols.

Contingut

  1. Què és un flux: el model conceptual
  2. Les dues jerarquies i per què són dues
  3. El mapa de les classes principals
  4. Classes de node i classes de filtre
  5. El patró Decorador a java.io
  6. Els ponts: InputStreamReader i OutputStreamWriter
  7. FileInputStream i FileOutputStream: byte a byte
  8. Lectura per blocs i el contracte de read(byte[])
  9. Copiar un fitxer binari: la portada d'un llibre
  10. DataInputStream i DataOutputStream: tipus primitius
  11. Fluxos en memòria: ByteArrayInputStream i ByteArrayOutputStream
  12. transferTo: la forma moderna de copiar
  13. Mida de bloc i rendiment
  14. Quin flux trio per a què
  15. Errors Comuns i Consells
  16. Exercicis

  1. Què és un flux: el model conceptual

Un flux (stream) és una seqüència ordenada de dades que es recorre de principi a fi i en un sol sentit.

La metàfora que li dona nom és la d'una canonada d'aigua. L'aigua entra per un extrem i surt per l'altre; pots agafar el que va passant, però no pots recular a veure el que ha passat fa una estona, ni avançar-te al que vindrà.

El vocabulari que cal fixar:

Terme Significat
Flux La seqüència de dades i la classe que la gestiona
Font (source) D'on surten les dades: fitxer, xarxa, memòria, teclat
Destinació (sink) On van: fitxer, xarxa, memòria, pantalla
Direcció Entrada (llegir) o sortida (escriure). Mai les dues
Posició Quant s'ha consumit. Avança sola i no recula

Les tres propietats que defineixen un flux i que cal interioritzar:

1. És unidireccional. No existeix una classe que llegeixi i escrigui. InputStream llegeix, OutputStream escriu. Si necessites totes dues coses sobre el mateix fitxer, obres dos fluxos —o fas servir RandomAccessFile, que no és un flux i que aquesta lliçó no cobreix—.

2. És seqüencial. No hi ha accés aleatori: per arribar al byte 1000 has de consumir els 999 anteriors. Aquesta limitació és el que permet que la mateixa abstracció serveixi per a un fitxer local, una connexió de xarxa o una dada que s'està generant sobre la marxa.

3. És agnòstic respecte a la font. I aquesta és la propietat que el fa valuós. Un mètode que rep un InputStream funciona amb un fitxer, amb un socket del mòdul 9, amb un array de bytes en memòria o amb la sortida d'un altre procés, sense canviar una línia:

/**
 * Compta bytes de QUALSEVOL flux d'entrada.
 *
 * No sap ni li importa si ve d'un fitxer, de la xarxa, de la memoria
 * o d'un array. Aixo es exactament el que aporta l'abstraccio.
 */
public static long comptarBytes(InputStream entrada) throws IOException {
    long total = 0;
    int llegits;
    byte[] bloc = new byte[8192];
    while ((llegits = entrada.read(bloc)) != -1) {
        total += llegits;
    }
    return total;
}

Aquest mètode es pot cridar amb un fitxer, amb una resposta HTTP (mòdul 9) o amb dades generades en memòria. És polimorfisme del mòdul 3 aplicat a l'E/S, i és la raó que la jerarquia existeixi.

Un avís terminològic important per evitar una confusió molt estesa:

Aquests fluxos no tenen res a veure amb l'API de Streams de col·leccions (stream(), filter, map, collect), que és de Java 8 i s'estudia a 10-04. Comparteixen el nom en anglès i res més. Els d'aquesta lliçó són de Java 1.0, són a java.io i transporten bytes o caràcters; els de 10-04 són a java.util.stream i transporten elements d'una col·lecció. L'únic punt de contacte el veuràs a 07-06, amb Files.lines().

  1. Les dues jerarquies i per què són dues

Java té dues famílies completes i paral·leles de classes de flux:

Família de bytes Família de caràcters
Classes base InputStream / OutputStream Reader / Writer
Unitat byte (8 bits, −128 a 127) char (16 bits, Unicode)
Des de Java 1.0 Java 1.1
Per a Dades binàries: imatges, PDF, ZIP, executables Text
Codificació No la coneix ni la necessita Imprescindible: tradueix bytes a caràcters
Mètode de lectura read() retorna 0..255 o −1 read() retorna 0..65535 o −1
Exemples de node FileInputStream, FileOutputStream FileReader, FileWriter

Per què no n'hi havia prou amb una

Java 1.0 només tenia la família de bytes, i es va descobrir aviat que era insuficient. El motiu és el que ja coneixes de 07-01: un caràcter no és un byte.

En ASCII, la correspondència és 1 a 1 i la distinció no importa. Però tan bon punt entra Unicode:

  • ç en UTF-8 són dos bytes.
  • en són tres.
  • Un emoji, quatre.

Si només tens fluxos de bytes i vols llegir text, has de fer la descodificació tu: acumular bytes, saber quants formen el caràcter següent, gestionar els caràcters partits entre dues lectures. Això és complex, propens a errors i sempre igual. La família Reader/Writer ho fa per tu.

I hi ha una segona raó, més subtil: els caràcters es poden partir entre dos blocs. Si llegeixes 8192 bytes i l'últim és el primer d'una ç, el segon byte arriba a la lectura següent. Un Reader manté aquest estat intern i t'entrega la ç completa. Gestionar-ho a mà és exactament el tipus d'error que apareix només amb fitxers grans i només de vegades.

La regla de decisió és simple i no admet excuses:

El fitxer és text que una persona podria llegir? Fes servir Reader/Writer. Són dades binàries? Fes servir InputStream/OutputStream. No n'estàs segur? És binari. Tracta'l com a bytes i no l'espatllaràs.

Què passa si t'equivoques: si llegeixes un JPEG amb un FileReader, el descodificador intenta interpretar bytes arbitraris com a caràcters UTF-8. La majoria no formen seqüències vàlides i se substitueixen per . Les dades es corrompen de forma irreversible. Si després les escrius, la imatge ja no s'obre. És la corrupció silenciosa de 07-01 portada a l'extrem.

  1. El mapa de les classes principals

La jerarquia de bytes:

flowchart TD
    IS["InputStream<br/>(abstracta)"]
    IS --> FIS["FileInputStream<br/>NODE: fitxer"]
    IS --> BAIS["ByteArrayInputStream<br/>NODE: memoria"]
    IS --> ALTRES["Altres nodes:<br/>socket, proces, recurs"]
    IS --> FILT["FilterInputStream<br/>(decorador base)"]
    FILT --> BIS["BufferedInputStream<br/>afegeix buffer"]
    FILT --> DIS["DataInputStream<br/>afegeix tipus primitius"]
    IS --> OIS["ObjectInputStream<br/>objectes serialitzats (07-05)"]

    style IS fill:#e3f2fd
    style FILT fill:#fff3e0
    style FIS fill:#e8f5e9
    style BAIS fill:#e8f5e9

La jerarquia de caràcters:

flowchart TD
    R["Reader<br/>(abstracta)"]
    R --> ISR["InputStreamReader<br/>PONT bytes a caracters"]
    ISR --> FR["FileReader<br/>NODE: fitxer de text"]
    R --> SR["StringReader<br/>NODE: memoria"]
    R --> CAR["CharArrayReader<br/>NODE: memoria"]
    R --> FIR["FilterReader<br/>(decorador base)"]
    R --> BR["BufferedReader<br/>afegeix buffer i readLine"]

    style R fill:#e3f2fd
    style ISR fill:#f3e5f5
    style BR fill:#fff3e0
    style FR fill:#e8f5e9

I la d'escriptura, que és simètrica:

flowchart TD
    W["Writer<br/>(abstracta)"]
    W --> OSW["OutputStreamWriter<br/>PONT caracters a bytes"]
    OSW --> FW["FileWriter<br/>NODE: fitxer de text"]
    W --> SW["StringWriter<br/>NODE: memoria"]
    W --> BW["BufferedWriter<br/>afegeix buffer i newLine"]
    W --> PW["PrintWriter<br/>afegeix println i printf"]

    style W fill:#e3f2fd
    style OSW fill:#f3e5f5
    style BW fill:#fff3e0
    style PW fill:#fff3e0
    style FW fill:#e8f5e9

Fixa't en un detall revelador dels dos últims diagrames: FileReader estén InputStreamReader i FileWriter estén OutputStreamWriter. No són classes independents: són dreceres. FileReader és un pont al qual ja li han connectat un FileInputStream per dins. Això explica de cop per què accepta un Charset al constructor i per què a 07-01 l'alternativa new InputStreamReader(new FileInputStream(cami), UTF_8) feia exactament el mateix.

El contracte mínim de cada classe base, que és l'únic que cal memoritzar:

// InputStream
public abstract int read() throws IOException;            // 0..255, o -1 al final
public int read(byte[] b) throws IOException;             // quants en va llegir, o -1
public int read(byte[] b, int off, int len);
public void close() throws IOException;
public long transferTo(OutputStream desti);               // Java 9

// OutputStream
public abstract void write(int b) throws IOException;     // el byte menys significatiu
public void write(byte[] b);
public void write(byte[] b, int off, int len);
public void flush() throws IOException;
public void close() throws IOException;

// Reader
public int read() throws IOException;                     // 0..65535, o -1
public int read(char[] c);
public void close() throws IOException;

// Writer
public void write(int c);
public void write(String s);
public void write(char[] c);
public void flush();
public void close();

Tota la resta són afegits. readLine() és de BufferedReader, println() és de PrintWriter, readInt() és de DataInputStream. Si coneixes les quatre classes base, saps llegir qualsevol codi d'E/S de Java.

  1. Classes de node i classes de filtre

Aquesta és la distinció que ho organitza tot:

Classe de node (o de connexió) Classe de filtre (o decorador)
D'on treu les dades D'un recurs real: fitxer, xarxa, memòria D'un altre flux
Què afegeix L'accés al recurs Una capacitat: memòria intermèdia, tipus, format
Es pot fer servir sola No: necessita alguna cosa per embolcallar
Exemples d'entrada FileInputStream, ByteArrayInputStream BufferedInputStream, DataInputStream
Exemples de sortida FileOutputStream, ByteArrayOutputStream BufferedOutputStream, PrintWriter

Es reconeixen a simple vista pel constructor:

// NODE: rep un recurs (un cami, un File, un array)
new FileInputStream("cataleg.txt")
new ByteArrayInputStream(dades)

// FILTRE: rep UN ALTRE FLUX
new BufferedInputStream(altreFlux)
new DataInputStream(altreFlux)
new PrintWriter(altreWriter)

I d'aquí la regla que explica totes les expressions imbricades que has vist:

La capa més interna sempre és un node. Totes les de fora són filtres.

new DataInputStream(          // filtre: afegeix readInt, readDouble...
    new BufferedInputStream(  // filtre: afegeix memoria intermedia
        new FileInputStream(  // NODE: connecta amb el fitxer
            "dades/prestecs.bin")))

Es llegeix de dins cap enfora: "obro el fitxer, li poso una memòria intermèdia, i a sobre llegeixo tipus primitius".

  1. El patró Decorador a java.io

El que acabes de veure té nom propi al catàleg de patrons de disseny: és el Decorador, i java.io n'és l'exemple canònic —el que apareix a tots els llibres—.

La idea: en lloc de crear una classe per a cada combinació de capacitats, es creen capes que s'embolcallen les unes a les altres. Cada decorador implementa la mateixa interfície que decora i delega en l'embolcallat, afegint-hi la seva part.

Imagina l'alternativa sense decorador. Si volguessis cobrir totes les combinacions de font (fitxer, memòria, xarxa) per capacitat (amb memòria intermèdia, amb tipus, amb totes dues), necessitaries una classe per combinació:

Simple Amb memòria Amb tipus Amb memòria i tipus
Fitxer FileInput BufferedFileInput DataFileInput BufferedDataFileInput
Memòria MemoryInput BufferedMemoryInput DataMemoryInput BufferedDataMemoryInput
Xarxa SocketInput BufferedSocketInput DataSocketInput BufferedDataSocketInput

Dotze classes per a tres fonts i dues capacitats. I cada capacitat nova en multiplica el nombre: amb una tercera capacitat en serien vint-i-quatre. És l'explosió combinatòria que el decorador evita: 3 nodes + 2 filtres = 5 classes, amb les dotze combinacions disponibles per composició.

Com flueixen les dades per la cadena:

flowchart LR
    APP["El teu codi"] -->|"readInt()"| D["DataInputStream"]
    D -->|"read(byte[4])"| B["BufferedInputStream"]
    B -->|"read(byte[8192])<br/>nomes si la memoria es buida"| F["FileInputStream"]
    F -->|"crida al sistema"| SO["Sistema operatiu"]
    SO --> DISC["Disc"]

    style D fill:#fff3e0
    style B fill:#fff3e0
    style F fill:#e8f5e9

Segueix una crida a readInt() per aquesta cadena:

  1. DataInputStream.readInt() necessita 4 bytes i els demana al BufferedInputStream.
  2. BufferedInputStream mira la seva memòria interna. Si té 4 bytes, els retorna sense tocar el disc.
  3. Si és buida, demana 8192 bytes de cop al FileInputStream, els desa i retorna els 4 demanats.
  4. FileInputStream fa una crida al sistema.
  5. DataInputStream combina els 4 bytes en un int i el retorna.

Dues mil crides a readInt() produeixen una sola crida al sistema. Aquest és el valor de la memòria intermèdia, i és el tema complet de 07-04.

Les tres conseqüències pràctiques del decorador, que cal tenir sempre presents:

1. L'ordre importa. Aquestes dues cadenes no són equivalents:

// CORRECTE: la memoria intermedia esta entre el fitxer i els tipus
new DataInputStream(new BufferedInputStream(new FileInputStream(f)))

// INUTIL: la memoria intermedia esta fora. DataInputStream llegeix directe del
// fitxer, 4 bytes cada vegada, amb una crida al sistema per cadascun.
new BufferedInputStream(new DataInputStream(new FileInputStream(f)))

La regla general: la memòria intermèdia va tan a prop com sigui possible del node, perquè sigui ella qui absorbeixi les crides al sistema.

2. Tancar l'exterior ho tanca tot. El close() de cada decorador crida el close() de l'embolcallat, en cascada. Per això n'hi ha prou de declarar la capa exterior al try-with-resources:

// N'hi ha prou amb aixo: el close() de PrintWriter tanca BufferedWriter,
// que tanca FileWriter, que tanca el fitxer.
try (PrintWriter sortida = new PrintWriter(
        new BufferedWriter(
            new FileWriter("informe.txt", StandardCharsets.UTF_8)))) {
    sortida.println("...");
}

I això té un corol·lari incòmode que convé conèixer: si el constructor d'una capa intermèdia falla, les capes ja construïdes queden sense tancar. Amb la cadena de dalt, si new BufferedWriter(...) llancés una excepció, el FileWriter ja construït quedaria obert i sense referència. És un cas rar però real; la forma robusta és declarar cada capa per separat al try-with-resources:

try (FileWriter fw = new FileWriter("informe.txt", StandardCharsets.UTF_8);
     BufferedWriter bw = new BufferedWriter(fw);
     PrintWriter sortida = new PrintWriter(bw)) {
    sortida.println("...");
}
// Cada recurs declarat per separat: tots es tanquen, en ordre invers (06-06)

Més verbós, però sense aquell buit. Per a codi normal, la forma imbricada és acceptable; per a codi d'infraestructura crítica, aquesta.

3. L'abstracció es conserva. Un mètode que rep InputStream accepta qualsevol cadena de decoradors, perquè tots són InputStream.

Aquest patró es formalitza, amb la seva estructura general i els seus altres usos, a la lliçó 12-02. Aquí l'has vist funcionant al lloc on millor s'aprecia.

  1. Els ponts: InputStreamReader i OutputStreamWriter

Les dues jerarquies no estan aïllades: hi ha dues classes que les connecten, i són exactament el punt on es decideix la codificació que portes dues lliçons especificant.

Classe Converteix És un
InputStreamReader Bytes → caràcters Reader que embolcalla un InputStream
OutputStreamWriter Caràcters → bytes Writer que embolcalla un OutputStream
flowchart LR
    subgraph BYTES["Mon de BYTES"]
        FIS["FileInputStream"]
    end
    subgraph PONT["EL PONT"]
        ISR["InputStreamReader<br/>charset UTF-8"]
    end
    subgraph CARS["Mon de CARACTERS"]
        BR["BufferedReader"]
        APP["El teu codi:<br/>String"]
    end

    FIS -->|"bytes 0xC3 0xA7"| ISR
    ISR -->|"caracter 'c trencada'"| BR
    BR --> APP

    style ISR fill:#f3e5f5

Aquest diagrama conté la idea completa: la traducció passa en un únic punt, i aquest punt és on s'especifica el charset. Tot el que hi ha a l'esquerra són bytes sense significat; tot el que hi ha a la dreta són caràcters.

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

public class UsDelPont {

    /** Lectura de text amb la cadena completa, explicita. */
    public static void llegir(String cami) throws IOException {
        try (BufferedReader lector = new BufferedReader(       // 3. memoria i readLine
                new InputStreamReader(                          // 2. PONT: aqui el charset
                        new FileInputStream(cami),              // 1. NODE: bytes del fitxer
                        StandardCharsets.UTF_8))) {

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

    /** Escriptura simetrica. */
    public static void escriure(String cami, String text) throws IOException {
        try (BufferedWriter escriptor = new BufferedWriter(
                new OutputStreamWriter(
                        new FileOutputStream(cami),
                        StandardCharsets.UTF_8))) {
            escriptor.write(text);
        }
    }
}

Ara ja pots veure l'equivalència exacta amb el que vas fer servir a 07-01 i 07-02:

// Aquestes dues linies son practicament el mateix:
new FileReader(cami, StandardCharsets.UTF_8)
new InputStreamReader(new FileInputStream(cami), StandardCharsets.UTF_8)

// I aquestes dues tambe:
new FileWriter(cami, StandardCharsets.UTF_8)
new OutputStreamWriter(new FileOutputStream(cami), StandardCharsets.UTF_8)

FileReader és literalment una subclasse d'InputStreamReader amb el FileInputStream ja connectat. La forma curta és còmoda per a fitxers; la llarga és necessària tan bon punt la font no és un fitxer:

// Llegir text de l'ENTRADA ESTANDARD: no hi ha cap "FileReader de System.in"
new InputStreamReader(System.in, StandardCharsets.UTF_8)

// Llegir text d'una connexio de xarxa (modul 9)
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)

// Llegir text d'un recurs dins del .jar
new InputStreamReader(getClass().getResourceAsStream("/config.txt"),
                      StandardCharsets.UTF_8)

Cap d'aquests tres casos no es pot resoldre amb FileReader, perquè la font no és un fitxer. El pont és el que fa que el charset sigui especificable en qualsevol font de text.

I un cas especial que mereix un avís: System.out és un PrintStream, un flux de bytes amb mètodes de text. És una raresa històrica de Java 1.0, anterior a l'existència de Writer. Per això System.out.println("Vençut") pot mostrar malament els accents en una consola de Windows: la conversió la fa PrintStream amb la codificació de la consola, que no ha de ser necessàriament UTF-8. Si necessites control:

PrintWriter consola = new PrintWriter(
        new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true);
consola.println("Prestec vençut");       // charset controlat

El true del segon paràmetre activa el buidatge automàtic a cada println, que en consola és el que vols.

  1. FileInputStream i FileOutputStream: byte a byte

Aquestes són les classes de node per a dades binàries. La seva API és la d'InputStream/OutputStream sense afegits.

import java.io.FileInputStream;
import java.io.IOException;

public class LecturaBinariaSimple {

    /** Mostra els primers bytes d'un fitxer en hexadecimal. */
    public static void bolcarCapcalera(String cami, int quants) throws IOException {
        try (FileInputStream entrada = new FileInputStream(cami)) {

            for (int i = 0; i < quants; i++) {
                int b = entrada.read();          // 0..255, o -1 al final
                if (b == -1) {
                    System.out.println("\n(final del fitxer)");
                    break;
                }
                System.out.printf("%02X ", b);
                if ((i + 1) % 16 == 0) {
                    System.out.println();
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) throws IOException {
        bolcarCapcalera("portades/java-eficac.jpg", 32);
    }
}

Sortida amb un JPEG:

FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 48
00 48 00 00 FF DB 00 43 00 08 06 06 07 06 05 08

Aquests primers bytes són la signatura del format: FF D8 FF identifica un JPEG, igual que 89 50 4E 47 identifica un PNG i 25 50 44 46 identifica un PDF. És el tipus de dada que només es pot llegir amb fluxos de bytes.

El detall tècnic que confon tothom:

read() retorna un int entre 0 i 255, però el tipus byte de Java va de −128 a 127. El mètode fa la conversió sense signe per tu. Si converteixes a byte, el valor 200 es converteix en −56. Per tornar a 0..255 cal emmascarar: b & 0xFF.

byte b = -56;                    // aixi ho guarda un byte[]
int senseSigne = b & 0xFF;       // 200: el valor real
System.out.printf("%02X%n", senseSigne);   // C8

Aquest detall causa fallades reals en sumes de verificació, comparacions i bolcats hexadecimals. Quan treballis amb bytes, emmascara amb & 0xFF sempre que necessitis el valor numèric.

En escriptura, l'asimetria és simètrica:

try (FileOutputStream sortida = new FileOutputStream("prova.bin")) {
    sortida.write(255);    // escriu el byte 0xFF
    sortida.write(65);     // escriu el byte 0x41, que es la 'A' en ASCII
    sortida.write(300);    // escriu 0x2C: es queda amb els 8 bits BAIXOS, sense avisar
}

write(int) descarta els bits alts en silenci. Escriure 300 escriu 44. És una font d'errors en codi que calcula valors i els escriu sense comprovar el rang.

I com a 07-02, FileOutputStream té el seu paràmetre append:

new FileOutputStream("dades.bin");         // SOBREESCRIU, trunca a zero
new FileOutputStream("dades.bin", true);   // AFEGEIX al final

Amb les mateixes conseqüències i el mateix risc. L'advertiment de 07-02 s'aplica idèntic.

  1. Lectura per blocs i el contracte de read(byte[])

Llegir byte a byte té el mateix problema de rendiment que llegir caràcter a caràcter, per la mateixa raó. La solució immediata és llegir per blocs:

public int read(byte[] buffer) throws IOException

El seu contracte té tres punts que cal conèixer amb precisió, perquè tots tres es fallen:

1. Retorna quants bytes ha llegit, no quants hi caben. Pot retornar menys que la mida de l'array encara que quedin dades per llegir. Amb un fitxer local això passa només al final; amb un socket de xarxa (mòdul 9) passa constantment, perquè les dades arriben a trossos.

2. Retorna −1 només quan no hi ha absolutament res més. Un 0 és possible amb un array de longitud zero, però mai no significa final de fitxer.

3. No garanteix omplir l'array. No existeix cap promesa que ho faci. Suposar-ho és l'error clàssic.

El bucle correcte:

import java.io.FileInputStream;
import java.io.IOException;

public class LecturaPerBlocs {

    private static final int MIDA_BLOC = 8192;

    public static long comptarBytes(String cami) throws IOException {
        long total = 0;

        try (FileInputStream entrada = new FileInputStream(cami)) {
            byte[] bloc = new byte[MIDA_BLOC];
            int llegits;

            // 'llegits' es QUANTS bytes hi ha de veritat a l'array.
            // Nomes els primers 'llegits' son dades noves; la resta es
            // brossa de la iteracio anterior.
            while ((llegits = entrada.read(bloc)) != -1) {
                total += llegits;
                processar(bloc, llegits);          // SEMPRE amb el limit
            }
        }
        return total;
    }

    private static void processar(byte[] bloc, int quants) {
        for (int i = 0; i < quants; i++) {         // fins a 'quants', no fins a length
            // ...
        }
    }
}

L'error, en la seva forma més habitual i més destructiva:

// MALAMENT: copia SEMPRE l'array sencer
while ((llegits = entrada.read(bloc)) != -1) {
    sortida.write(bloc);                      // <-- hauria de ser write(bloc, 0, llegits)
}

Amb un fitxer de 10 000 bytes i blocs de 8192: la primera lectura porta 8192, la segona porta 1808. Però write(bloc) n'escriu 8192 a totes dues, així que el fitxer copiat té 16 384 bytes en lloc de 10 000, i els últims 6 384 són brossa de la primera lectura. Amb una imatge, el resultat no s'obre. Amb un ZIP, està corrupte.

Escriu sempre write(bloc, 0, llegits). És el mateix parany de l'append(bloc, 0, llegits) de 07-01, en la seva versió binària i més greu.

I com a avís: si necessites de veritat omplir l'array —perquè el format exigeix llegir exactament N bytes—, cal insistir en bucle. Java ho té resolt:

// Java 9+: llegeix EXACTAMENT n bytes o llanca EOFException
byte[] capcalera = entrada.readNBytes(16);

// Java 9+: llegeix TOT el que queda. Compte amb fitxers grans (07-01)
byte[] tot = entrada.readAllBytes();

  1. Copiar un fitxer binari: la portada d'un llibre

BiblioTech desa la imatge de portada de cada llibre. Copiar-les requereix fluxos de bytes, sí o sí.

package com.nexussoftware.bibliotech.servei;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Logger;

/**
 * Gestio de les imatges de portada del cataleg de BiblioTech.
 *
 * Les portades son JPEG o PNG: dades BINARIES. Fer servir Reader/Writer sobre
 * elles les destruiria, perque el descodificador de caracters substitueix
 * els bytes que no formen sequencies valides.
 */
public class GestorPortades {

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

    private static final int MIDA_BLOC = 8192;
    private static final long MIDA_MAXIMA = 5 * 1024 * 1024;     // 5 MB

    private final File directori;

    public GestorPortades(String directoriPortades) {
        this.directori = new File(directoriPortades);
        if (!directori.exists() && !directori.mkdirs()) {
            LOG.warning("No s'ha pogut crear " + directori.getAbsolutePath());
        }
    }

    /**
     * Copia la portada d'un llibre al magatzem, anomenant-la pel seu ISBN.
     *
     * @return bytes copiats
     * @throws IOException si falla la copia
     */
    public long importarPortada(String isbn, File origen) throws IOException {
        if (!origen.isFile()) {
            throw new IOException("No es un fitxer llegible: " + origen.getAbsolutePath());
        }
        if (origen.length() > MIDA_MAXIMA) {
            throw new IOException("La portada supera el maxim de "
                    + (MIDA_MAXIMA / 1024) + " KB: " + origen.length() + " bytes");
        }

        File desti = new File(directori, isbn + extensio(origen.getName()));
        long copiats = copiar(origen, desti);

        LOG.info(() -> String.format("Portada de %s importada: %d bytes a %s",
                isbn, copiats, desti.getName()));
        return copiats;
    }

    /**
     * Copia binaria byte a byte, amb memoria intermedia als dos extrems.
     *
     * La cadena de decoradors:
     *   FileInputStream  -> NODE: llegeix del fitxer
     *   BufferedInputStream -> FILTRE: agrupa les lectures
     *   (i el mateix al costat d'escriptura)
     */
    public long copiar(File origen, File desti) throws IOException {
        long total = 0;

        try (BufferedInputStream entrada =
                     new BufferedInputStream(new FileInputStream(origen), MIDA_BLOC);
             BufferedOutputStream sortida =
                     new BufferedOutputStream(new FileOutputStream(desti), MIDA_BLOC)) {

            byte[] bloc = new byte[MIDA_BLOC];
            int llegits;

            while ((llegits = entrada.read(bloc)) != -1) {
                // CLAU: (bloc, 0, llegits), MAI (bloc) a seques.
                // Amb (bloc) es copiaria brossa al final i la imatge no obriria.
                sortida.write(bloc, 0, llegits);
                total += llegits;
            }
            // El close() del try-with-resources fa flush: sense ell, els ultims
            // bytes de la memoria intermedia no arribarien al fitxer (07-02).
        }
        return total;
    }

    /** Detecta el format llegint la SIGNATURA del fitxer, no la seva extensio. */
    public String detectarFormat(File fitxer) throws IOException {
        try (FileInputStream entrada = new FileInputStream(fitxer)) {
            byte[] sign = entrada.readNBytes(8);       // Java 9+

            if (sign.length >= 3
                    && (sign[0] & 0xFF) == 0xFF
                    && (sign[1] & 0xFF) == 0xD8
                    && (sign[2] & 0xFF) == 0xFF) {
                return "JPEG";
            }
            if (sign.length >= 8
                    && (sign[0] & 0xFF) == 0x89
                    && sign[1] == 'P' && sign[2] == 'N' && sign[3] == 'G') {
                return "PNG";
            }
            if (sign.length >= 4 && sign[0] == 'G' && sign[1] == 'I' && sign[2] == 'F') {
                return "GIF";
            }
            return "DESCONEGUT";
        }
    }

    private String extensio(String nom) {
        int punt = nom.lastIndexOf('.');
        return (punt > 0) ? nom.substring(punt).toLowerCase() : ".bin";
    }
}

Dues coses que ensenya aquesta classe més enllà de la còpia:

  • El & 0xFF de detectarFormat. Sense ell, la comparació sign[0] == 0xFF seria sempre falsa: sign[0] val −1 com a byte, i 0xFF val 255 com a int. Aquesta és exactament la fallada de l'apartat 7, i aquí es veu per què importa.
  • Detectar el format per la signatura, no per l'extensió. L'extensió és una convenció que qualsevol pot canviar; els primers bytes són el format real. És una comprovació de seguretat elemental quan acceptes fitxers de tercers.

  1. DataInputStream i DataOutputStream: tipus primitius

Aquests dos filtres afegeixen mètodes per llegir i escriure tipus primitius de Java en un format binari definit i portable.

import java.io.*;

public class DadesBinaries {

    public static void escriure(String cami) throws IOException {
        try (DataOutputStream sortida = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(cami)))) {

            sortida.writeUTF("PR-0001");        // cadena: 2 bytes de longitud + UTF-8 modificat
            sortida.writeInt(15);               // 4 bytes
            sortida.writeDouble(0.25);          // 8 bytes
            sortida.writeBoolean(true);         // 1 byte
            sortida.writeLong(1_700_000_000L);  // 8 bytes
        }
    }

    public static void llegir(String cami) throws IOException {
        try (DataInputStream entrada = new DataInputStream(
                new BufferedInputStream(new FileInputStream(cami)))) {

            // L'ORDRE de lectura ha de ser EXACTAMENT el d'escriptura.
            // El fitxer no porta cap descripcio del seu contingut.
            String referencia = entrada.readUTF();
            int    dies       = entrada.readInt();
            double tarifa     = entrada.readDouble();
            boolean retornat  = entrada.readBoolean();
            long   marca      = entrada.readLong();

            System.out.printf("%s: %d dies, %.2f EUR/dia, retornat=%b, marca=%d%n",
                    referencia, dies, tarifa, retornat, marca);
        }
    }
}

El format és fix i documentat:

Mètode Bytes Format
writeByte 1 El byte
writeShort 2 Big-endian
writeInt 4 Big-endian
writeLong 8 Big-endian
writeFloat 4 IEEE 754
writeDouble 8 IEEE 754
writeBoolean 1 0 o 1
writeChar 2 UTF-16
writeUTF 2 + n Longitud de 2 bytes + UTF-8 modificat

Les tres propietats que fan útil aquest format:

  1. És portable. Sempre big-endian, independentment de l'arquitectura de la màquina. Un fitxer escrit en un servidor ARM es llegeix igual en un x86.
  2. És compacte. Un int ocupa 4 bytes sempre. En text, 1234567890 n'ocuparia 10.
  3. És ràpid. No hi ha anàlisi: els bytes es combinen directament en el valor.

I les seves tres limitacions, que cal acceptar abans de triar-lo:

  1. No és autodescriptiu. El fitxer no diu què conté. Si llegeixes en un ordre diferent del que vas escriure, obtens brossa sense cap error: un readDouble sobre el que era un int i un float retorna un nombre perfectament vàlid i completament fals.
  2. No és llegible. No es pot obrir amb un editor ni comparar amb diff.
  3. És fràgil davant de canvis. Afegir un camp trenca tots els fitxers existents.

Un avís concret sobre writeUTF: no escriu UTF-8 estàndard. Fa servir l'anomenat UTF-8 modificat, que codifica el caràcter nul de forma diferent i les cadenes estan limitades a 65 535 bytes. És un format intern de Java: fes-lo servir només per a fitxers que llegirà Java. Per a intercanvi amb altres llenguatges, text.

Quan fer servir DataStream:

Situació DataStream?
Memòria cau binària interna de l'aplicació Sí, hi encaixa bé
Format propi compacte i controlat
Protocol binari de xarxa (mòdul 9) Sí, és el seu cas natural
Dades que un altre llenguatge ha de llegir No: text o un format estàndard
Dades que algú inspeccionarà No: no és llegible
Dades amb estructura d'objectes i referències No: això és serialització (07-05)

BiblioTech no el farà servir per al seu catàleg —vol fitxers llegibles i versionables, i això és 07-07—, però conèixer-lo és imprescindible per al mòdul 9 i per entendre que la serialització de 07-05 està construïda sobre aquestes mateixes primitives.

  1. Fluxos en memòria: ByteArrayInputStream i ByteArrayOutputStream

Aquests dos nodes no toquen el disc: el seu "fitxer" és un array de bytes en memòria.

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

public class FluxosEnMemoria {

    /** ByteArrayOutputStream: acumula en memoria el que se li escriu. */
    public static byte[] generarEnMemoria() throws IOException {
        ByteArrayOutputStream memoria = new ByteArrayOutputStream();

        try (DataOutputStream sortida = new DataOutputStream(memoria)) {
            sortida.writeUTF("PR-0001");
            sortida.writeInt(15);
            sortida.writeDouble(0.25);
        }
        // toByteArray() retorna una COPIA del que s'ha acumulat
        return memoria.toByteArray();
    }

    /** ByteArrayInputStream: llegeix d'un array com si fos un fitxer. */
    public static void llegirDeMemoria(byte[] dades) throws IOException {
        try (DataInputStream entrada = new DataInputStream(
                new ByteArrayInputStream(dades))) {

            System.out.println(entrada.readUTF());
            System.out.println(entrada.readInt());
            System.out.println(entrada.readDouble());
        }
    }

    public static void main(String[] args) throws IOException {
        byte[] dades = generarEnMemoria();
        System.out.println("Generats " + dades.length + " bytes sense tocar el disc");
        llegirDeMemoria(dades);
    }
}

Per a què serveixen de veritat:

Ús Explicació
Proves Provar un mètode que rep InputStream sense crear fitxers. És el seu ús més valuós
Memòria completa Acumular una sortida i decidir després què fer-ne
Conversió Passar entre byte[] i flux quan una API n'exigeix un i tens l'altre
Enviar per xarxa Preparar el contingut en memòria i enviar-lo de cop (mòdul 9)
Còpia de seguretat en memòria Desar l'estat abans d'una operació arriscada

L'ús en proves mereix un exemple, perquè canvia la forma d'escriure codi verificable:

/**
 * Metode de produccio: rep un InputStream, no un cami.
 * Aquesta decisio de disseny es el que el fa provable sense fitxers.
 */
public int comptarLinies(InputStream entrada) throws IOException {
    int linies = 0;
    try (BufferedReader lector = new BufferedReader(
            new InputStreamReader(entrada, StandardCharsets.UTF_8))) {
        while (lector.readLine() != null) {
            linies++;
        }
    }
    return linies;
}

// PROVA: sense fitxer, sense disc, sense neteja, sense dependencies de l'entorn
public void provaComptarLinies() {
    String contingut = "LLIBRE;978-0000000001;Java Eficac\n"
                     + "LLIBRE;978-0000000002;Patrons de Disseny\n";

    InputStream simulat =
            new ByteArrayInputStream(contingut.getBytes(StandardCharsets.UTF_8));

    int resultat = comptarLinies(simulat);
    // se n'esperen 2
}

Lliçó de disseny que convé gravar-se: un mètode que rep InputStream es pot provar amb un array en memòria; un que rep String cami obliga a crear fitxers de prova, netejar-los i dependre del sistema de fitxers. Accepta l'abstracció més general que et serveixi. Això es reprendrà amb JUnit a 11-04.

I una nota pràctica: ByteArrayOutputStream no cal tancar-lo —el seu close() no fa res— i per això a l'exemple està fora del try-with-resources. Si hi fos a dins, quedaria tancat abans de poder cridar toByteArray(). És una de les poquíssimes excepcions legítimes a la regla de tancar-ho tot, i de les quals 06-06 ja advertia.

Els seus equivalents a la família de caràcters són StringWriter i StringReader, útils quan el que acumules és text:

StringWriter memoria = new StringWriter();
try (PrintWriter sortida = new PrintWriter(memoria)) {
    sortida.printf("%-25s %8.2f%n", "Java Eficac", 3.75);
}
String resultat = memoria.toString();

  1. transferTo: la forma moderna de copiar

Java 9 va afegir un mètode que redueix tot el bucle de còpia a una línia:

public long transferTo(OutputStream desti) throws IOException
import java.io.*;

public class CopiaModerna {

    /** Copia completa. Java 9+. */
    public static long copiar(File origen, File desti) throws IOException {
        try (InputStream entrada = new BufferedInputStream(new FileInputStream(origen));
             OutputStream sortida = new BufferedOutputStream(new FileOutputStream(desti))) {

            return entrada.transferTo(sortida);     // tot el bucle, en una linia
        }
    }
}

Comparació amb el bucle manual:

Bucle manual transferTo
Línies de codi 6-8 1
Risc d'oblidar (bloc, 0, llegits) Alt Cap
Mida de bloc La tries tu Interna (8 KB)
Progrés durant la còpia Se'n pot informar No es pot
Transformar les dades al vol No
Disponible des de Sempre Java 9

Fes servir transferTo quan només vulguis copiar. Fes servir el bucle quan necessitis informar del progrés, transformar o aturar-te a mitges:

/** Copia informant del progres: aqui el bucle continua sent necessari. */
public long copiarAmbProgres(File origen, File desti) throws IOException {
    long mida = origen.length();
    long copiats = 0;
    int ultimPercentatge = -1;

    try (InputStream entrada = new BufferedInputStream(new FileInputStream(origen));
         OutputStream sortida = new BufferedOutputStream(new FileOutputStream(desti))) {

        byte[] bloc = new byte[8192];
        int llegits;
        while ((llegits = entrada.read(bloc)) != -1) {
            sortida.write(bloc, 0, llegits);
            copiats += llegits;

            int percentatge = (int) (copiats * 100 / Math.max(1, mida));
            if (percentatge != ultimPercentatge && percentatge % 10 == 0) {
                System.out.printf("  %d%% (%d de %d bytes)%n", percentatge, copiats, mida);
                ultimPercentatge = percentatge;
            }
        }
    }
    return copiats;
}

I a 07-06 veuràs la forma més alta de totes, que ni tan sols obre fluxos:

Files.copy(origen, desti, StandardCopyOption.REPLACE_EXISTING);

  1. Mida de bloc i rendiment

Quant importa la mida del bloc? Aquestes xifres, il·lustratives i mesurades sobre un fitxer de 100 MB en un SSD corrent, donen la forma de la corba:

Tècnica Temps orientatiu Relatiu
read() byte a byte, sense memòria intermèdia ~95 000 ms 1x (referència)
read() byte a byte, amb BufferedInputStream ~1 400 ms 68x
Blocs de 512 bytes ~420 ms 226x
Blocs de 8 KB ~180 ms 528x
Blocs de 64 KB ~165 ms 576x
Blocs d'1 MB ~170 ms 559x
transferTo ~150 ms 633x
Files.copy (07-06) ~130 ms 731x

Les tres conclusions:

1. El salt que importa és el primer. De byte a byte sense memòria intermèdia a qualsevol cosa amb memòria intermèdia hi ha un factor de 70. De 8 KB a 64 KB hi ha un 8%. Si només faràs una optimització, és aquesta.

2. 8 KB és el punt d'equilibri. No és casualitat: coincideix amb la mida típica de bloc del sistema de fitxers, i és el valor per defecte de BufferedInputStream i de transferTo. Pujar més dona rendiments decreixents i consumeix memòria: cent còpies concurrents amb memòries intermèdies d'1 MB són 100 MB de heap.

3. Una memòria intermèdia massa gran pot ser pitjor. Deixa de cabre a la memòria cau del processador i augmenta la pressió sobre el recol·lector de brossa. Més no és millor a partir de cert punt.

Regla pràctica: 8 KB tret que hagis mesurat i tinguis un motiu. I abans de tocar la mida del bloc, assegura't que hi ha una memòria intermèdia: aquí hi ha el 99% de la millora possible.

  1. Quin flux trio per a què

La taula de decisió completa del mòdul:

Necessito... Classe Família
Llegir text d'un fitxer BufferedReader sobre FileReader Caràcters
Escriure text en un fitxer PrintWriter o BufferedWriter sobre FileWriter Caràcters
Llegir text de la consola BufferedReader sobre InputStreamReader(System.in) Caràcters
Llegir text de la xarxa BufferedReader sobre InputStreamReader(socket) Caràcters
Llegir un fitxer binari BufferedInputStream sobre FileInputStream Bytes
Escriure un fitxer binari BufferedOutputStream sobre FileOutputStream Bytes
Copiar un fitxer transferTo, o Files.copy (07-06) Bytes
Llegir/escriure primitius amb format fix DataInputStream / DataOutputStream Bytes
Llegir/escriure objectes complets ObjectInputStream / ObjectOutputStream (07-05) Bytes
Treballar amb dades en memòria ByteArrayInputStream / ByteArrayOutputStream Bytes
Acumular text en memòria StringWriter / StringReader Caràcters
Convertir bytes en caràcters InputStreamReader amb charset Pont
Convertir caràcters en bytes OutputStreamWriter amb charset Pont

I les quatre regles de composició que resumeixen la lliçó:

  1. El node va dins; els filtres, fora.
  2. La memòria intermèdia va tan a prop com sigui possible del node.
  3. El charset s'especifica al pont, que és l'únic lloc on es tradueix.
  4. Tanca la capa exterior: el tancament es propaga en cascada.

Errors Comuns i Consells

  • Fer servir Reader/Writer amb dades binàries. Destrueix el fitxer de forma irreversible: els bytes que no formen caràcters vàlids se substitueixen pel caràcter de reemplaçament. La imatge ja no s'obre.
  • Fer servir InputStream/OutputStream amb text sense pont. Funciona fins que apareix una ç, i llavors es parteix a mig bloc.
  • Posar la memòria intermèdia al lloc equivocat. BufferedInputStream(DataInputStream(...)) no aporta res: la memòria intermèdia ha de quedar entre el node i els filtres de format.
  • write(bloc) en lloc de write(bloc, 0, llegits). L'error més destructiu de la lliçó: el fitxer copiat surt més gran i amb brossa al final.
  • Suposar que read(byte[]) omple l'array. No ho promet mai. Amb sockets falla constantment.
  • Comparar bytes sense & 0xFF. signatura[0] == 0xFF és sempre fals perquè byte té signe. Emmascara quan necessitis el valor numèric.
  • Creure que write(300) escriu 300. Escriu 44: es queda amb els 8 bits baixos, sense avisar.
  • Llegir DataInputStream en un ordre diferent de l'escrit. No dona error: retorna valors perfectament vàlids i completament falsos.
  • Fer servir writeUTF per intercanviar amb altres llenguatges. No és UTF-8 estàndard, és l'UTF-8 modificat de Java.
  • Tancar ByteArrayOutputStream abans de toByteArray(). No és fatal —el seu close() no fa res— però delata que no s'ha entès per a què serveix.
  • Oblidar que System.out és un PrintStream de bytes. Per això els accents surten malament en algunes consoles. Embolcalla'l en un OutputStreamWriter amb charset si necessites control.
  • Construir cadenes imbricades on una capa intermèdia pot fallar. Si el constructor intermedi llança, la capa ja construïda queda sense tancar. Per a codi crític, declara cada capa per separat al try-with-resources.
  • Buscar rendiment a la mida del bloc abans de posar una memòria intermèdia. La memòria intermèdia dona un factor de 70; la mida del bloc, un 8%.
  • Consell: llegeix les cadenes de dins cap enfora. new DataInputStream(new BufferedInputStream(new FileInputStream(f))) és "obro el fitxer, li poso memòria intermèdia, i llegeixo tipus". Amb aquesta lectura, cap expressió de java.io no torna a ser intimidant.
  • Consell: accepta InputStream, no String cami. És la decisió de disseny que fa el teu codi provable sense fitxers i reutilitzable amb xarxa i memòria.
  • Consell: comprova el format per la signatura, no per l'extensió. Els primers bytes no menteixen; el nom del fitxer sí.
  • Consell: si dubtes entre text i binari, tria binari. Tractar text com a bytes el conserva intacte. A l'inrevés, el destrueix.

Exercicis

Exercici 1: visor hexadecimal

Escriu VisorHexadecimal amb un mètode public static void bolcar(String cami, int maxBytes) que mostri el contingut d'un fitxer en el format clàssic de tres columnes:

00000000  FF D8 FF E0 00 10 4A 46  49 46 00 01 01 00 00 48  |......JFIF.....H|
00000010  00 48 00 00 FF DB 00 43  00 08 06 06 07 06 05 08  |.H.....C........|

Requisits:

  1. Desplaçament en hexadecimal de 8 dígits.
  2. 16 bytes per línia, agrupats de 8 en 8.
  3. Columna ASCII: els bytes imprimibles (32 a 126) com a caràcter, la resta com a ..
  4. Lectura per blocs, mai byte a byte.
  5. Un main que detecti a més el format per la signatura (JPEG, PNG, GIF, PDF, ZIP, classe de Java).

Exercici 2: comparador de rendiment

Escriu ComparadorRendiment que mesuri cinc formes de copiar el mateix fitxer:

  1. FileInputStream.read() byte a byte sense memòria intermèdia.
  2. Amb BufferedInputStream/BufferedOutputStream, byte a byte.
  3. Blocs de 512 bytes.
  4. Blocs de 8192 bytes.
  5. transferTo.

Requisits:

  1. Genera primer un fitxer de prova de 5 MB amb dades pseudoaleatòries.
  2. Verifica en cada cas que la còpia té la mateixa mida que l'original.
  3. Mostra una taula amb el temps i la millora relativa respecte al primer mètode.
  4. Afegeix un comentari explicant per què el salt gran és entre els mètodes 1 i 2.

Adverteix en un comentari que això no és un banc de proves rigorós: la memòria cau del sistema operatiu i l'escalfament de la JVM distorsionen les mesures, i per mesurar de debò es fa servir JMH (esmentat a 10-07).

Exercici 3: magatzem binari de préstecs

Escriu MagatzemPrestecs que desi i recuperi els préstecs de BiblioTech en format binari propi fent servir DataOutputStream/DataInputStream:

  1. Una capçalera amb un nombre màgic (0x424C4942, que és BLIB), la versió del format (1) i el nombre de registres.
  2. Per cada préstec: referència (writeUTF), referència del material, identificador de l'empleat, dia d'inici, dia de devolució (−1 si no s'ha retornat) i multa acumulada.
  3. En llegir, validi el nombre màgic i llanci IOException si no coincideix, per no interpretar un fitxer aliè com a propi.
  4. Validi també la versió, i rebutgi les versions futures amb un missatge clar.
  5. Faci servir BufferedInputStream/BufferedOutputStream i escriptura sobre fitxer temporal amb reanomenat (07-02).
  6. Un main que desi tres préstecs, els recuperi i els mostri, i que després intenti llegir un fitxer amb nombre màgic incorrecte per comprovar el rebuig.

Solucions

Solució 1

package com.nexussoftware.bibliotech.util;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Visor hexadecimal de fitxers, en el format classic de tres columnes.
 *
 * Treballa amb FLUXOS DE BYTES perque el contingut pot ser qualsevol cosa:
 * fer servir un Reader corrompria les dades abans de poder mostrar-les.
 */
public class VisorHexadecimal {

    private static final int BYTES_PER_LINIA = 16;
    private static final int PRIMER_IMPRIMIBLE = 32;
    private static final int ULTIM_IMPRIMIBLE = 126;

    public static void bolcar(String cami, int maxBytes) throws IOException {
        try (InputStream entrada = new BufferedInputStream(new FileInputStream(cami))) {

            byte[] linia = new byte[BYTES_PER_LINIA];
            int desplacament = 0;
            int llegits;

            // readNBytes (Java 9+) insisteix fins a omplir l'array o arribar al
            // final. Amb read() a seques podriem rebre menys de 16 bytes
            // enmig del fitxer i el bolcat quedaria desalineat.
            while (desplacament < maxBytes
                    && (llegits = entrada.readNBytes(linia, 0, BYTES_PER_LINIA)) > 0) {

                imprimirLinia(desplacament, linia, llegits);
                desplacament += llegits;
            }
        }
    }

    private static void imprimirLinia(int desplacament, byte[] dades, int quants) {
        StringBuilder hex   = new StringBuilder();
        StringBuilder ascii = new StringBuilder();

        for (int i = 0; i < BYTES_PER_LINIA; i++) {
            if (i == 8) {
                hex.append(' ');                    // separacio dels dos grups
            }
            if (i < quants) {
                // & 0xFF: sense aixo, un byte negatiu s'imprimiria com FFFFFFxx
                int b = dades[i] & 0xFF;
                hex.append(String.format("%02X ", b));
                ascii.append(esImprimible(b) ? (char) b : '.');
            } else {
                hex.append("   ");                  // farciment de l'ultima linia
                ascii.append(' ');
            }
        }

        System.out.printf("%08X  %s |%s|%n", desplacament, hex, ascii);
    }

    private static boolean esImprimible(int b) {
        return b >= PRIMER_IMPRIMIBLE && b <= ULTIM_IMPRIMIBLE;
    }

    /** Detecta el format per la seva SIGNATURA. L'extensio no es fiable. */
    public static String detectarFormat(String cami) throws IOException {
        try (InputStream entrada = new BufferedInputStream(new FileInputStream(cami))) {
            byte[] f = entrada.readNBytes(8);

            if (comenca(f, 0xFF, 0xD8, 0xFF))                 { return "JPEG"; }
            if (comenca(f, 0x89, 0x50, 0x4E, 0x47))           { return "PNG"; }
            if (comenca(f, 0x47, 0x49, 0x46, 0x38))           { return "GIF"; }
            if (comenca(f, 0x25, 0x50, 0x44, 0x46))           { return "PDF"; }
            if (comenca(f, 0x50, 0x4B, 0x03, 0x04))           { return "ZIP (o JAR, DOCX...)"; }
            if (comenca(f, 0xCA, 0xFE, 0xBA, 0xBE))           { return "CLASSE DE JAVA"; }
            if (comenca(f, 0xAC, 0xED))                       { return "OBJECTE SERIALITZAT (07-05)"; }
            return "DESCONEGUT (possiblement text)";
        }
    }

    /** Compara els primers bytes amb la signatura donada. */
    private static boolean comenca(byte[] dades, int... signatura) {
        if (dades.length < signatura.length) {
            return false;
        }
        for (int i = 0; i < signatura.length; i++) {
            if ((dades[i] & 0xFF) != signatura[i]) {  // el & 0xFF, un altre cop imprescindible
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) throws IOException {
        String cami = (args.length > 0) ? args[0] : "portades/java-eficac.jpg";

        System.out.println("Fitxer : " + cami);
        System.out.println("Format : " + detectarFormat(cami));
        System.out.println();
        bolcar(cami, 64);
    }
}

Sortida amb un JPEG:

Fitxer : portades/java-eficac.jpg
Format : JPEG

00000000  FF D8 FF E0 00 10 4A 46  49 46 00 01 01 00 00 48  |......JFIF.....H|
00000010  00 48 00 00 FF DB 00 43  00 08 06 06 07 06 05 08  |.H.....C........|
00000020  07 07 07 09 09 08 0A 0C  14 0D 0C 0B 0B 0C 19 12  |................|
00000030  13 0F 14 1D 1A 1F 1E 1D  1A 1C 1C 20 24 2E 27 20  |........... $.' |

Els dos punts didàctics: el & 0xFF apareix tres vegades a la solució i a les tres és imprescindible —sense ell, un byte negatiu s'imprimeix com a FFFFFFC3 i totes les comparacions de signatura fallen—. I readNBytes(array, 0, n) en lloc de read(array) garanteix que cada línia tingui els seus 16 bytes: amb read() a seques, una lectura curta a mig fitxer desalinearia tot el bolcat.

Solució 2

package com.nexussoftware.bibliotech.util;

import java.io.*;
import java.util.Random;

/**
 * Comparativa de cinc tecniques de copia binaria.
 *
 * AVIS: aixo NO es un banc de proves rigoros. La memoria cau de pagina del
 * sistema operatiu, el compilador JIT i el recollector de brossa distorsionen
 * les mesures; la primera execucio sempre surt pitjor. Serveix per veure els
 * ORDRES DE MAGNITUD, que es el que importa aqui. Per mesurar de debo es
 * fa servir JMH, que s'esmenta a 10-07.
 */
public class ComparadorRendiment {

    private static final String ORIGEN = "prova-rendiment.bin";
    private static final String DESTI  = "copia-rendiment.bin";
    private static final int MIDA_MB   = 5;

    // ---------- Les cinc tecniques ----------

    /** 1. Byte a byte SENSE memoria intermedia: una crida al sistema per byte. */
    static long byteAByteSenseBuffer(File o, File d) throws IOException {
        try (InputStream in = new FileInputStream(o);
             OutputStream out = new FileOutputStream(d)) {
            int b;
            long n = 0;
            while ((b = in.read()) != -1) {
                out.write(b);
                n++;
            }
            return n;
        }
    }

    /** 2. Byte a byte AMB memoria intermedia: absorbeix les crides al sistema. */
    static long byteAByteAmbBuffer(File o, File d) throws IOException {
        try (InputStream in = new BufferedInputStream(new FileInputStream(o));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(d))) {
            int b;
            long n = 0;
            while ((b = in.read()) != -1) {
                out.write(b);
                n++;
            }
            return n;
        }
    }

    /** 3 i 4. Per blocs de la mida indicada. */
    static long perBlocs(File o, File d, int mida) throws IOException {
        try (InputStream in = new FileInputStream(o);
             OutputStream out = new FileOutputStream(d)) {
            byte[] bloc = new byte[mida];
            int llegits;
            long n = 0;
            while ((llegits = in.read(bloc)) != -1) {
                out.write(bloc, 0, llegits);     // el limit, SEMPRE
                n += llegits;
            }
            return n;
        }
    }

    /** 5. transferTo (Java 9+). */
    static long ambTransferTo(File o, File d) throws IOException {
        try (InputStream in = new BufferedInputStream(new FileInputStream(o));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(d))) {
            return in.transferTo(out);
        }
    }

    // ---------- Infraestructura de mesura ----------

    interface Tecnica {
        long copiar(File origen, File desti) throws IOException;
    }

    static void generarFitxerProva(File f, int megues) throws IOException {
        if (f.exists() && f.length() == megues * 1024L * 1024L) {
            return;                                // ja esta generat
        }
        Random aleatori = new Random(42);           // llavor fixa: reproduible
        byte[] bloc = new byte[1024 * 1024];

        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
            for (int i = 0; i < megues; i++) {
                aleatori.nextBytes(bloc);
                out.write(bloc);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        File origen = new File(ORIGEN);
        File desti  = new File(DESTI);

        System.out.println("Generant fitxer de prova de " + MIDA_MB + " MB...");
        generarFitxerProva(origen, MIDA_MB);
        System.out.println("Mida: " + origen.length() + " bytes");
        System.out.println();

        String[] noms = {
                "1. Byte a byte SENSE buffer",
                "2. Byte a byte AMB buffer",
                "3. Blocs de 512 B",
                "4. Blocs de 8 KB",
                "5. transferTo"
        };
        Tecnica[] tecniques = {
                ComparadorRendiment::byteAByteSenseBuffer,      // referencies a metode (04-06)
                ComparadorRendiment::byteAByteAmbBuffer,
                (o, d) -> perBlocs(o, d, 512),
                (o, d) -> perBlocs(o, d, 8192),
                ComparadorRendiment::ambTransferTo
        };

        long referencia = 0;

        System.out.printf("%-28s %10s %10s %12s%n", "TECNICA", "TEMPS", "MB/s", "MILLORA");
        System.out.println("-".repeat(64));

        for (int i = 0; i < tecniques.length; i++) {
            long inici = System.nanoTime();
            long copiats = tecniques[i].copiar(origen, desti);
            long ms = (System.nanoTime() - inici) / 1_000_000;

            // VERIFICACIO: una copia rapida pero incorrecta no val res
            if (copiats != origen.length() || desti.length() != origen.length()) {
                System.out.printf("%-28s  COPIA INCORRECTA: %d bytes%n",
                        noms[i], desti.length());
                continue;
            }

            if (i == 0) {
                referencia = ms;
            }
            double mbs = (origen.length() / 1024.0 / 1024.0) / (ms / 1000.0);

            System.out.printf("%-28s %8d ms %10.1f %11.1fx%n",
                    noms[i], ms, mbs, (double) referencia / Math.max(1, ms));
        }

        System.out.println();
        System.out.println("El salt gran esta entre 1 i 2, i la causa es una de sola:");
        System.out.println("sense memoria intermedia hi ha UNA CRIDA AL SISTEMA PER BYTE");
        System.out.println("(5 milions); amb memoria intermedia, una cada 8192 bytes (unes");
        System.out.println("640). La resta de millores son marginals en comparacio.");

        desti.delete();
    }
}

Sortida típica:

Generant fitxer de prova de 5 MB...
Mida: 5242880 bytes

TECNICA                           TEMPS       MB/s      MILLORA
----------------------------------------------------------------
1. Byte a byte SENSE buffer      4680 ms        1.1         1.0x
2. Byte a byte AMB buffer          78 ms       64.2        60.0x
3. Blocs de 512 B                  26 ms      192.5       180.0x
4. Blocs de 8 KB                    9 ms      555.6       520.0x
5. transferTo                       7 ms      714.3       668.7x

El salt gran esta entre 1 i 2, i la causa es una de sola:
sense memoria intermedia hi ha UNA CRIDA AL SISTEMA PER BYTE
(5 milions); amb memoria intermedia, una cada 8192 bytes (unes
640). La resta de millores son marginals en comparacio.

El que cal extreure'n: la millora d'1 a 2 és de 60x i consisteix a escriure dues paraules més (Buffered...). La millora de 4 a 5 és d'un 28%. Quan algú pregunti per on començar a optimitzar l'E/S, la resposta és sempre la mateixa: posar una memòria intermèdia. Fixa't també en la verificació de la mida: una tècnica ràpida que produeixi una còpia incorrecta —el clàssic write(bloc) sense límit— es detecta allà i no es comptabilitza.

Solució 3

package com.nexussoftware.bibliotech.servei;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;

/**
 * Magatzem binari de prestecs de BiblioTech.
 *
 * Format propi (BLIB v1):
 *   [4 bytes] nombre magic 0x424C4942 = "BLIB"
 *   [4 bytes] versio del format
 *   [4 bytes] nombre de registres
 *   per registre:
 *     UTF  referencia del prestec
 *     UTF  referencia del material
 *     UTF  identificador de l'empleat
 *     INT  dia d'inici
 *     INT  dia de devolucio (-1 si no s'ha retornat)
 *     DOUBLE multa acumulada
 *
 * Aquest format es COMPACTE i RAPID, pero NO es llegible ni versionable en
 * control de codi. Per al cataleg, BiblioTech fara servir CSV (07-07); aixo
 * queda com a memoria cau binaria interna.
 */
public class MagatzemPrestecs {

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

    /** "BLIB" en ASCII. Identifica el fitxer com a nostre. */
    private static final int NUMERO_MAGIC = 0x424C4942;
    private static final int VERSIO_ACTUAL = 1;

    /** Registre pla d'un prestec, tal com es desa. */
    public record RegistrePrestec(String referencia, String referenciaMaterial,
                                  String idEmpleat, int diaInici,
                                  int diaDevolucio, double multa) {

        public RegistrePrestec {
            Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
            Objects.requireNonNull(referenciaMaterial, "El material no pot ser nul");
            Objects.requireNonNull(idEmpleat, "L'empleat no pot ser nul");
            if (multa < 0) {
                throw new IllegalArgumentException("La multa no pot ser negativa: " + multa);
            }
        }

        public boolean estaRetornat() { return diaDevolucio >= 0; }
    }

    private final File fitxer;

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

    // ---------------------------- ESCRIPTURA ----------------------------

    /**
     * Desa tots els prestecs. Escriptura ATOMICA (07-02): s'escriu en
     * un temporal i nomes es reanomena al final.
     */
    public void desar(List<RegistrePrestec> prestecs) throws IOException {
        Objects.requireNonNull(prestecs, "La llista no pot ser nulla");

        File temporal = new File(fitxer.getAbsolutePath() + ".tmp");
        boolean completat = false;

        try {
            try (DataOutputStream sortida = new DataOutputStream(
                    new BufferedOutputStream(new FileOutputStream(temporal)))) {

                // Capcalera
                sortida.writeInt(NUMERO_MAGIC);
                sortida.writeInt(VERSIO_ACTUAL);
                sortida.writeInt(prestecs.size());

                // Registres, en el MATEIX ordre en que es llegiran
                for (RegistrePrestec p : prestecs) {
                    sortida.writeUTF(p.referencia());
                    sortida.writeUTF(p.referenciaMaterial());
                    sortida.writeUTF(p.idEmpleat());
                    sortida.writeInt(p.diaInici());
                    sortida.writeInt(p.diaDevolucio());
                    sortida.writeDouble(p.multa());
                }
            }   // close() -> flush garantit

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

            LOG.info(() -> String.format("Desats %d prestecs a %s (%d bytes)",
                    prestecs.size(), fitxer.getName(), fitxer.length()));

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

    // ---------------------------- LECTURA ----------------------------

    /**
     * Carrega els prestecs desats.
     *
     * @throws IOException si el fitxer no es nostre, la versio no se
     *                     suporta o esta truncat
     */
    public List<RegistrePrestec> carregar() throws IOException {
        if (!fitxer.exists()) {
            LOG.warning(() -> "No hi ha magatzem a " + fitxer.getAbsolutePath()
                    + "; es retorna una llista buida");
            return new ArrayList<>();               // degradacio elegant (07-01)
        }

        try (DataInputStream entrada = new DataInputStream(
                new BufferedInputStream(new FileInputStream(fitxer)))) {

            // 1. VALIDAR EL NOMBRE MAGIC.
            //    Sense aixo, un fitxer alie s'interpretaria com a nostre i
            //    produiria dades absurdes SENSE CAP ERROR. Es la diferencia
            //    entre fallar i corrompre.
            int magic = entrada.readInt();
            if (magic != NUMERO_MAGIC) {
                throw new IOException(String.format(
                        "%s no es un magatzem de BiblioTech "
                                + "(nombre magic 0x%08X, s'esperava 0x%08X)",
                        fitxer.getName(), magic, NUMERO_MAGIC));
            }

            // 2. VALIDAR LA VERSIO
            int versio = entrada.readInt();
            if (versio > VERSIO_ACTUAL) {
                throw new IOException(String.format(
                        "El magatzem fa servir la versio %d del format i aquesta versio "
                                + "de BiblioTech nomes enten fins a la %d. "
                                + "Actualitza l'aplicacio.",
                        versio, VERSIO_ACTUAL));
            }

            // 3. Nombre de registres, amb un minim de seny
            int quants = entrada.readInt();
            if (quants < 0) {
                throw new IOException("Nombre de registres invalid: " + quants);
            }

            List<RegistrePrestec> prestecs = new ArrayList<>(quants);

            for (int i = 0; i < quants; i++) {
                try {
                    prestecs.add(new RegistrePrestec(
                            entrada.readUTF(),      // l'ORDRE ha de coincidir
                            entrada.readUTF(),      // exactament amb desar()
                            entrada.readUTF(),
                            entrada.readInt(),
                            entrada.readInt(),
                            entrada.readDouble()));

                } catch (EOFException e) {
                    // Fitxer TRUNCAT: es va tallar una escriptura anterior.
                    // S'encadena la causa (06-03) i es diu quants n'hi havia.
                    throw new IOException(String.format(
                            "Magatzem truncat: s'esperaven %d registres i nomes "
                                    + "n'hi ha %d de complets", quants, i), e);
                }
            }

            LOG.info(() -> String.format("Carregats %d prestecs de %s (format v%d)",
                    prestecs.size(), fitxer.getName(), versio));
            return prestecs;
        }
    }

    // ---------------------------- DEMOSTRACIO ----------------------------

    public static void main(String[] args) throws IOException {
        MagatzemPrestecs magatzem = new MagatzemPrestecs("dades/prestecs.blib");

        List<RegistrePrestec> originals = List.of(
                new RegistrePrestec("PR-0001", "978-0000000001", "E-001", 10, 22,  1.75),
                new RegistrePrestec("PR-0002", "978-0000000002", "E-002", 12, -1,  0.00),
                new RegistrePrestec("PR-0003", "978-0000000003", "E-003",  5, 45, 20.00));

        magatzem.desar(originals);

        System.out.println("=== RECUPERATS ===");
        for (RegistrePrestec p : magatzem.carregar()) {
            System.out.printf("  %s  material=%s  empleat=%s  dies %d..%s  multa %.2f%n",
                    p.referencia(), p.referenciaMaterial(), p.idEmpleat(),
                    p.diaInici(),
                    p.estaRetornat() ? String.valueOf(p.diaDevolucio()) : "pendent",
                    p.multa());
        }

        // --- Comprovacio del rebuig d'un fitxer alie ---
        File impostor = new File("dades/impostor.blib");
        try (DataOutputStream d = new DataOutputStream(new FileOutputStream(impostor))) {
            d.writeInt(0xDEADBEEF);      // nombre magic incorrecte
            d.writeInt(1);
            d.writeInt(0);
        }

        System.out.println();
        System.out.println("=== FITXER ALIE ===");
        try {
            new MagatzemPrestecs(impostor.getPath()).carregar();
            System.out.println("  ERROR: hauria d'haver estat rebutjat");
        } catch (IOException e) {
            System.out.println("  Rebutjat correctament:");
            System.out.println("  " + e.getMessage());
        }
        impostor.delete();
    }
}

Sortida:

=== RECUPERATS ===
  PR-0001  material=978-0000000001  empleat=E-001  dies 10..22  multa 1.75
  PR-0002  material=978-0000000002  empleat=E-002  dies 12..pendent  multa 0.00
  PR-0003  material=978-0000000003  empleat=E-003  dies 5..45  multa 20.00

=== FITXER ALIE ===
  Rebutjat correctament:
  impostor.blib no es un magatzem de BiblioTech (nombre magic 0xDEADBEEF, s'esperava 0x424C4942)

Les quatre idees que cal endur-se'n:

  1. El nombre màgic és imprescindible en qualsevol format binari propi. Sense ell, readUTF() sobre un fitxer aliè llegeix dos bytes qualssevol com a longitud, intenta llegir aquesta quantitat i retorna brossa —o llança una EOFException incomprensible—. Amb ell, el missatge diu exactament què passa. És la mateixa filosofia de 06-04: l'excepció ha de transportar el que qui la llegeix necessita per decidir.
  2. La versió del format es declara des del primer dia. Quan d'aquí a un any vulguis afegir un camp, el fitxer antic continuarà sent llegible perquè sabràs quina versió té. Afegir-la després és impossible: els fitxers ja escrits no la porten.
  3. L'EOFException es tradueix. El seu missatge per defecte no diu res. Traduïda amb el context —"s'esperaven 3 registres i només n'hi ha 2 de complets"— apunta directament a la causa: una escriptura anterior que es va tallar, exactament el que l'escriptura atòmica de 07-02 existeix per evitar.
  4. L'ordre de lectura i escriptura ha de ser idèntic. No hi ha manera de comprovar-ho automàticament en aquest format, i aquest és el seu punt feble davant del text de 07-07. Un record amb els camps en el mateix ordre ajuda que l'error salti a la vista en llegir el codi.

Conclusió

Ja no fas servir java.io de memòria: n'entens el disseny.

Tens el model del flux: una seqüència ordenada, unidireccional, seqüencial i agnòstica respecte a la font, amb el seu vocabulari de font, destinació i direcció. Aquesta tercera propietat és la que dona valor a tota la jerarquia: un mètode que accepta InputStream funciona amb un fitxer, amb un socket del mòdul 9 o amb un array en memòria, sense canviar una línia. És el polimorfisme del mòdul 3 aplicat a l'E/S. I saps que aquests fluxos no tenen res a veure amb l'API de Streams de col·leccions de 10-04, més enllà del nom.

Saps per què hi ha dues jerarquies i no una: InputStream/OutputStream per a bytes, Reader/Writer per a caràcters, perquè un caràcter no és un byteç en són dos, en són tres— i perquè els caràcters es poden partir entre dos blocs de lectura. Tens la regla de decisió sense excuses: si és text llegible, caràcters; si no, bytes; i si dubtes, bytes, perquè tractar text com a bytes el conserva i a l'inrevés el destrueix.

Distingeixes les classes de node —que es connecten a un recurs real i es reconeixen perquè el seu constructor rep un camí, un fitxer o un array— de les classes de filtre —que embolcallen un altre flux i es reconeixen perquè el seu constructor rep un flux—. I d'aquí surt la regla que desxifra qualsevol expressió imbricada: el node va dins, els filtres fora, i es llegeix de dins cap enfora.

Has vist el patró Decorador en el seu exemple canònic, amb l'argument que el justifica: sense ell calarien dotze classes per a tres fonts i dues capacitats, i amb ell n'hi ha prou amb cinc. Coneixes les seves tres conseqüències pràctiques: l'ordre importa —la memòria intermèdia va tan a prop com sigui possible del node—, tancar l'exterior tanca tota la cadena en cascada, i l'abstracció es conserva perquè tots els decoradors són el tipus que decoren. Saps també que una capa intermèdia que falli en construir-se deixa oberta l'anterior, i com evitar-ho declarant cada capa per separat. El patró es formalitza a 12-02; aquí l'has vist on millor s'aprecia.

Coneixes els ponts InputStreamReader i OutputStreamWriter, i saps que són el punt exacte on es decideix la codificació que portes tres lliçons especificant: a un costat bytes sense significat, a l'altre caràcters. Entens ara per què FileReader estén InputStreamReader —és un pont amb el fitxer ja connectat— i per què el pont és imprescindible per llegir text de System.in, d'un socket o d'un recurs del .jar, on no hi ha cap FileReader possible.

Saps llegir i escriure dades binàries amb FileInputStream/FileOutputStream, byte a byte i per blocs, amb els tres punts del contracte de read(byte[]) —retorna quants en va llegir, no omple l'array, i només -1 és final— i amb la regla que evita l'error més destructiu de la lliçó: write(bloc, 0, llegits), mai write(bloc). Coneixes el parany del byte amb signe i el & 0xFF que el resol, i saps que write(300) escriu 44 sense avisar. Amb això, GestorPortades copia les imatges de BiblioTech sense corrompre-les i en detecta el format per la signatura, no per l'extensió.

Coneixes DataInputStream/DataOutputStream per a tipus primitius en format fix, portable i compacte, amb les seves tres virtuts i les seves tres limitacions —no és autodescriptiu, no és llegible, és fràgil davant de canvis—, i amb l'avís que writeUTF no és UTF-8 estàndard. Coneixes ByteArrayInputStream/ByteArrayOutputStream i el seu millor ús, que és provar codi d'E/S sense tocar el disc, amb la lliçó de disseny que l'acompanya: accepta InputStream, no String cami. I coneixes transferTo de Java 9, amb el criteri per triar entre ell i el bucle manual: el bucle només si necessites progrés, transformació o aturada.

I tens les xifres que ordenen les prioritats de rendiment: la memòria intermèdia dona un factor de seixanta; la mida del bloc, un vuit per cent. Vuit kilobytes tret que hagis mesurat. Abans de tocar res, posa la memòria intermèdia.

Aquesta memòria intermèdia —la que apareix a totes les cadenes, la que produeix el salt de rendiment, la de la qual portes tres lliçons sentint a parlar— és la protagonista de la següent. A 07-04, BufferedReader i BufferedWriter, veuràs exactament què fa per dins i per què redueix tant la feina, amb el diagrama de les lectures amb i sense ella; el contracte complet de readLine() —que retorna null al final i no inclou el salt de línia— i el bucle canònic que el fa servir, a més de per què ready() no serveix com a condició de final; newLine() i el buidatge en escriptura; què aporta exactament cada capa del trio PrintWriter + BufferedWriter + FileWriter; com processar un fitxer d'un gigabyte amb memòria constant; i la comparació definitiva entre BufferedReader i el Scanner que fas servir des del mòdul 1. I BiblioTech estrenarà ImportadorCataleg, capaç de llegir milers de línies, validar-les una a una i retornar un informe del que s'ha carregat i del que s'ha descartat.

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