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-resourcesa 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
- Què és un flux: el model conceptual
- Les dues jerarquies i per què són dues
- El mapa de les classes principals
- Classes de node i classes de filtre
- El patró Decorador a
java.io - Els ponts:
InputStreamReaderiOutputStreamWriter FileInputStreamiFileOutputStream: byte a byte- Lectura per blocs i el contracte de
read(byte[]) - Copiar un fitxer binari: la portada d'un llibre
DataInputStreamiDataOutputStream: tipus primitius- Fluxos en memòria:
ByteArrayInputStreamiByteArrayOutputStream transferTo: la forma moderna de copiar- Mida de bloc i rendiment
- Quin flux trio per a què
- Errors Comuns i Consells
- Exercicis
- 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 ajava.ioi transporten bytes o caràcters; els de 10-04 són ajava.util.streami transporten elements d'una col·lecció. L'únic punt de contacte el veuràs a 07-06, ambFiles.lines().
- 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 servirInputStream/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.
- 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.
- 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 | Sí | 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".
- El patró Decorador a
java.io
java.ioEl 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:
DataInputStream.readInt()necessita 4 bytes i els demana alBufferedInputStream.BufferedInputStreammira la seva memòria interna. Si té 4 bytes, els retorna sense tocar el disc.- Si és buida, demana 8192 bytes de cop al
FileInputStream, els desa i retorna els 4 demanats. FileInputStreamfa una crida al sistema.DataInputStreamcombina els 4 bytes en uninti 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.
- Els ponts:
InputStreamReader i OutputStreamWriter
InputStreamReader i OutputStreamWriterLes 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 controlatEl true del segon paràmetre activa el buidatge automàtic a cada println, que en consola és el que vols.
FileInputStream i FileOutputStream: byte a byte
FileInputStream i FileOutputStream: byte a byteAquestes 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:
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 unintentre 0 i 255, però el tipusbytede Java va de −128 a 127. El mètode fa la conversió sense signe per tu. Si converteixes abyte, 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); // C8Aquest 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 finalAmb les mateixes conseqüències i el mateix risc. L'advertiment de 07-02 s'aplica idèntic.
- Lectura per blocs i el contracte de
read(byte[])
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:
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();
- 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
& 0xFFdedetectarFormat. Sense ell, la comparaciósign[0] == 0xFFseria sempre falsa:sign[0]val −1 com abyte, i0xFFval 255 com aint. 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.
DataInputStream i DataOutputStream: tipus primitius
DataInputStream i DataOutputStream: tipus primitiusAquests 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:
- É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.
- És compacte. Un
intocupa 4 bytes sempre. En text,1234567890n'ocuparia 10. - É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:
- 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
readDoublesobre el que era uninti unfloatretorna un nombre perfectament vàlid i completament fals. - No és llegible. No es pot obrir amb un editor ni comparar amb
diff. - É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 | Sí |
| 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.
- Fluxos en memòria:
ByteArrayInputStream i ByteArrayOutputStream
ByteArrayInputStream i ByteArrayOutputStreamAquests 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
InputStreames pot provar amb un array en memòria; un que repString camiobliga 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();
transferTo: la forma moderna de copiar
transferTo: la forma moderna de copiarJava 9 va afegir un mètode que redueix tot el bucle de còpia a una línia:
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 | Sí | 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:
- 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.
- 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çó:
- El node va dins; els filtres, fora.
- La memòria intermèdia va tan a prop com sigui possible del node.
- El charset s'especifica al pont, que és l'únic lloc on es tradueix.
- Tanca la capa exterior: el tancament es propaga en cascada.
Errors Comuns i Consells
- Fer servir
Reader/Writeramb 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/OutputStreamamb 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 dewrite(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èbyteté 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
DataInputStreamen un ordre diferent de l'escrit. No dona error: retorna valors perfectament vàlids i completament falsos. - Fer servir
writeUTFper intercanviar amb altres llenguatges. No és UTF-8 estàndard, és l'UTF-8 modificat de Java. - Tancar
ByteArrayOutputStreamabans detoByteArray(). No és fatal —el seuclose()no fa res— però delata que no s'ha entès per a què serveix. - Oblidar que
System.outés unPrintStreamde bytes. Per això els accents surten malament en algunes consoles. Embolcalla'l en unOutputStreamWriteramb 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ó dejava.iono torna a ser intimidant. - Consell: accepta
InputStream, noString 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:
- Desplaçament en hexadecimal de 8 dígits.
- 16 bytes per línia, agrupats de 8 en 8.
- Columna ASCII: els bytes imprimibles (32 a 126) com a caràcter, la resta com a
.. - Lectura per blocs, mai byte a byte.
- Un
mainque 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:
FileInputStream.read()byte a byte sense memòria intermèdia.- Amb
BufferedInputStream/BufferedOutputStream, byte a byte. - Blocs de 512 bytes.
- Blocs de 8192 bytes.
transferTo.
Requisits:
- Genera primer un fitxer de prova de 5 MB amb dades pseudoaleatòries.
- Verifica en cada cas que la còpia té la mateixa mida que l'original.
- Mostra una taula amb el temps i la millora relativa respecte al primer mètode.
- 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:
- Una capçalera amb un nombre màgic (
0x424C4942, que ésBLIB), la versió del format (1) i el nombre de registres. - 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. - En llegir, validi el nombre màgic i llanci
IOExceptionsi no coincideix, per no interpretar un fitxer aliè com a propi. - Validi també la versió, i rebutgi les versions futures amb un missatge clar.
- Faci servir
BufferedInputStream/BufferedOutputStreami escriptura sobre fitxer temporal amb reanomenat (07-02). - Un
mainque 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:
- 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 unaEOFExceptionincomprensible—. 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. - 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.
- L'
EOFExceptiones 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. - 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
recordamb 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
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
