Portes tres lliçons sentint a parlar de la memòria intermèdia. A 07-01 explicava per què llegir caràcter a caràcter és cent vegades més lent del que cal. A 07-02 explicava per què un fitxer acabat d'escriure té zero bytes. A 07-03 apareixia a totes les cadenes de decoradors i a la taula de rendiment amb un factor de seixanta.
Aquesta lliçó és la de la memòria intermèdia. Veuràs què fa exactament per dins, per què aquesta millora és tan gran, i les dues classes que l'aporten al món del text: BufferedReader, amb el readLine() que és el mètode d'E/S més usat de Java, i BufferedWriter, amb el seu newLine() portable.
I veuràs una cosa més important que el rendiment: el processament en flux. Un BufferedReader permet recórrer un fitxer d'un gigabyte amb un consum de memòria constant, línia a línia, sense carregar res més que la línia actual. Aquesta capacitat és la que converteix BiblioTech en un sistema capaç d'importar un inventari complet, i és el que construiràs al cas pràctic final: ImportadorCataleg, que llegeix milers de línies, valida cadascuna, compta les bones i les dolentes, registra els motius i retorna un informe.
try-with-resourcesa tot, com sempre. Amb memòries intermèdies d'escriptura hi ha un motiu afegit per no oblidar-ho: elclose()és el que buida la memòria intermèdia al disc. Sense ell, no has escrit res.
Contingut
- Què fa exactament una memòria intermèdia
- Sense memòria intermèdia i amb memòria intermèdia: el diagrama
- Les xifres que justifiquen la diferència
- Construcció per decoració i mida de la memòria intermèdia
BufferedReader.readLine()i el seu contracte- El bucle canònic de lectura
ready()i per què no serveix com a condició de finalmarkireset: mirar sense consumirBufferedWriter:write,newLineiflush- El trio
PrintWriter+BufferedWriter+FileWriter Files.newBufferedReaderinewBufferedWriter- Processar un fitxer gran amb memòria constant
- Llegir de consola amb
BufferedReaderdavant deScanner - BiblioTech:
ImportadorCatalegamb informe complet - Errors Comuns i Consells
- Exercicis
- Què fa exactament una memòria intermèdia
Una memòria intermèdia és un array intermedi a la RAM que agrupa les operacions per reduir el nombre de crides al sistema operatiu.
Recorda el diagrama de 07-01: entre el teu codi i el disc hi ha una frontera cara, la crida al sistema. Cada vegada que la creues, el processador canvia de context, salta al nucli, fa la seva feina i torna. Aquest viatge costa ordres de magnitud més que executar unes quantes instruccions en el teu propi procés.
El que fa un BufferedReader:
- Quan li demanes un caràcter, mira el seu array intern.
- Si té dades, te'n retorna un sense sortir de la memòria del procés.
- Si és buit, demana 8192 caràcters de cop al flux que embolcalla, els desa al seu array i et retorna el primer.
El resultat és que una lectura de cada 8192 arriba al sistema operatiu; les altres 8191 són accessos a un array.
El mateix principi, invertit, en escriptura:
- Quan li demanes escriure, desa al seu array.
- Quan l'array s'omple —o quan algú fa
flush()oclose()— el bolca d'una sola vegada.
D'aquí surt l'explicació completa de dues coses que ja has vist i que ara encaixen:
- Per què
FileReader.read()és tan lent: no té memòria intermèdia. Cada crida és una crida al sistema. - Per què un fitxer acabat d'escriure té 0 bytes (07-02): les dades són a l'array de la memòria intermèdia, no al disc.
- Sense memòria intermèdia i amb memòria intermèdia: el diagrama
flowchart TD
subgraph SENSE["SENSE memoria intermedia: FileReader.read()"]
A1["read() numero 1"] --> S1["crida al sistema"]
A2["read() numero 2"] --> S2["crida al sistema"]
A3["read() numero 3"] --> S3["crida al sistema"]
A4["... 8192 vegades"] --> S4["... 8192 crides"]
S1 --> D1["DISC"]
S2 --> D1
S3 --> D1
S4 --> D1
end
style D1 fill:#ffcdd2
flowchart TD
subgraph AMB["AMB memoria intermedia: BufferedReader.read()"]
B1["read() numero 1"] --> BUF["array intern<br/>de 8192 caracters"]
B2["read() numero 2"] --> BUF
B3["read() numero 3"] --> BUF
B4["... 8192 vegades"] --> BUF
BUF -->|"UNA sola vegada,<br/>quan es buit"| SYS["crida al sistema"]
SYS --> D2["DISC"]
end
style BUF fill:#c8e6c9
style D2 fill:#c8e6c9
La comparació numèrica del cas complet, amb un fitxer de 5 MB:
| Sense memòria intermèdia | Amb memòria intermèdia de 8 KB | |
|---|---|---|
Crides a read() del teu codi |
5 242 880 | 5 242 880 |
| Crides al sistema operatiu | 5 242 880 | 640 |
| Memòria addicional usada | 0 | 16 KB |
| Temps orientatiu | ~4 500 ms | ~60 ms |
El codi fa el mateix nombre de crides; el que canvia és quantes creuen la frontera. Vuit mil vegades menys, a canvi de setze kilobytes de memòria. És probablement el millor intercanvi de memòria per temps que existeix en programació.
- Les xifres que justifiquen la diferència
Un experiment que pots executar:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class MesurarBuffer {
/** Sense memoria intermedia: una crida al sistema per caracter. */
static long senseBuffer(String cami) throws IOException {
long n = 0;
try (FileReader lector = new FileReader(cami, StandardCharsets.UTF_8)) {
while (lector.read() != -1) {
n++;
}
}
return n;
}
/** Amb memoria intermedia, mateix bucle: absorbeix les crides. */
static long ambBuffer(String cami) throws IOException {
long n = 0;
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
while (lector.read() != -1) {
n++;
}
}
return n;
}
/** Amb memoria intermedia i per linies: la forma idiomatica. */
static long perLinies(String cami) throws IOException {
long n = 0;
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
n += linia.length();
}
}
return n;
}
public static void main(String[] args) throws IOException {
String cami = "dades/cataleg-gran.txt"; // uns 5 MB
mesurar("Sense buffer, caracter a caracter", () -> senseBuffer(cami));
mesurar("Amb buffer, caracter a caracter", () -> ambBuffer(cami));
mesurar("Amb buffer, per linies", () -> perLinies(cami));
}
interface Mesurable { long executar() throws IOException; }
static void mesurar(String nom, Mesurable m) throws IOException {
long inici = System.nanoTime();
long resultat = m.executar();
long ms = (System.nanoTime() - inici) / 1_000_000;
System.out.printf("%-35s %6d ms (%d caracters)%n", nom, ms, resultat);
}
}Resultat orientatiu:
Sense buffer, caracter a caracter 4512 ms (5242880 caracters)
Amb buffer, caracter a caracter 68 ms (5242880 caracters)
Amb buffer, per linies 41 ms (5158400 caracters)Tres observacions:
- La diferència entre la primera i la segona és una sola paraula al codi:
new BufferedReader(...). Factor 66. - La tercera és encara millor perquè
readLine()treballa directament sobre l'array de la memòria intermèdia, buscant el salt de línia, sense cridarread()una vegada per caràcter. - El recompte de la tercera és menor perquè
readLine()no inclou el salt de línia. Aquest detall és part del seu contracte i s'explica a l'apartat 5.
- Construcció per decoració i mida de la memòria intermèdia
BufferedReader i BufferedWriter són filtres en el sentit de 07-03: embolcallen un altre Reader o Writer.
import java.io.*;
import java.nio.charset.StandardCharsets;
// Lectura, mida per defecte (8192 caracters)
BufferedReader lector = new BufferedReader(
new FileReader("dades/cataleg.txt", StandardCharsets.UTF_8));
// Lectura, mida explicita
BufferedReader gran = new BufferedReader(
new FileReader("dades/cataleg.txt", StandardCharsets.UTF_8), 65536);
// Escriptura
BufferedWriter escriptor = new BufferedWriter(
new FileWriter("dades/informe.txt", StandardCharsets.UTF_8));
// Sobre un pont, quan la font no es un fitxer (07-03)
BufferedReader deConsola = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));Sobre la mida:
| Mida | Quan |
|---|---|
| 8192 (per defecte) | Gairebé sempre. Coincideix amb el bloc del sistema de fitxers |
| 32768 – 65536 | Fitxers molt grans llegits seqüencialment, si has mesurat millora |
| 1024 o menys | Mai, tret de memòria molt restringida |
| Més d'1 MB | Contraproduent: deixa de cabre a la memòria cau i pressiona el recol·lector |
I l'advertiment de 07-03, que aquí s'aplica igual: si obres cent fluxos alhora amb memòries intermèdies d'1 MB, són 100 MB de heap. La mida per defecte està molt ben triada; canvia-la només amb una mesura al davant.
Un detall que convé assenyalar: embolcallar una cosa que ja té memòria intermèdia no aporta res. new BufferedReader(new BufferedReader(...)) és redundant, i new BufferedInputStream(new ByteArrayInputStream(...)) també, perquè un array a la RAM no fa crides al sistema. La memòria intermèdia serveix on hi ha una frontera cara per creuar.
BufferedReader.readLine() i el seu contracte
BufferedReader.readLine() i el seu contractereadLine() és, de bon tros, el mètode d'E/S més usat de Java. El seu contracte té quatre punts, i tots quatre importen:
1. Retorna la línia sense el terminador. Si el fitxer té Java Eficac\n, readLine() retorna "Java Eficac", d'11 caràcters. El \n es consumeix però no s'inclou. Per això el recompte de l'experiment de l'apartat 3 sortia menor.
2. Reconeix les tres convencions. \n, \r\n i \r es tracten com a final de línia, independentment del sistema. Un fitxer escrit a Windows es llegeix correctament a Linux sense fer res. Aquesta és la raó per la qual l'apartat 8 de 07-02 concloïa que, per al teu propi programa, el separador és igual.
3. Retorna null al final del fitxer. No "", no una excepció: null. I aquesta distinció és essencial, perquè una línia buida retorna "", que no és el mateix:
| Contingut restant | readLine() retorna |
|---|---|
Java Eficac\n |
"Java Eficac" |
\n (línia buida) |
"" (cadena buida, longitud 0) |
| Res: final de fitxer | null |
Sense salt final (última línia sense \n) |
"Sense salt final" |
4. Bloqueja fins a tenir una línia completa. Amb un fitxer és instantani. Amb la consola o un socket, espera fins que arribi un salt de línia o es tanqui el flux. És el que fa que la consola esperi que premis Retorn.
Un cas límit que convé conèixer: una línia sense terminador al final del fitxer es retorna igualment. La crida següent retorna null. És correcte i evita perdre l'última línia de fitxers generats per eines que no posen salt final.
I un avís de dimensionament: readLine() carrega la línia sencera en memòria. Amb un fitxer el contingut del qual sigui una única línia de 2 GB —n'hi ha: bolcats de bases de dades, JSON en una línia—, readLine() intenta construir un String de 2 GB i provoca OutOfMemoryError. És rar, però quan passa desconcerta, perquè el codi "processa línia a línia" i tot i així es queda sense memòria.
- El bucle canònic de lectura
Aquesta és la forma idiomàtica, i cal escriure-la sempre igual:
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
processar(linia);
}
}Desglossat:
String linia;es declara fora del bucle, perquè la condició necessita veure-la.linia = lector.readLine()llegeix i assigna. L'assignació és una expressió, com a 07-01.- Els parèntesis interns són obligatoris: sense ells,
linia = lector.readLine() != nullintentaria assignar unbooleana unStringi no compila. != nullés el final. No!linia.isEmpty(), que s'aturaria a la primera línia en blanc.
Els tres errors que substitueixen aquest bucle i tots fallen:
// MALAMENT 1: s'atura a la primera linia buida
while (!(linia = lector.readLine()).isEmpty()) { }
// A mes: NullPointerException en arribar al final, perque readLine retorna null
// MALAMENT 2: llegeix cada linia DUES vegades i se'n salta una de cada dues
while (lector.readLine() != null) {
processar(lector.readLine()); // aquesta es la SEGUENT linia
}
// MALAMENT 3: ready() no significa "queden dades". Apartat 7
while (lector.ready()) {
processar(lector.readLine());
}El segon és especialment traïdor perquè funciona a mitges: processa les línies parelles i se salta les senars, i amb un fitxer de prova petit pot semblar correcte.
Amb comptador de línies, que és l'habitual en codi real:
int numeroLinia = 0;
String linia;
while ((linia = lector.readLine()) != null) {
numeroLinia++;
String neta = linia.trim();
if (neta.isEmpty() || neta.startsWith("#")) {
continue; // buides i comentaris: 02-04
}
processar(neta, numeroLinia); // el numero, per poder informar de l'error
}Aquest numeroLinia no és decoratiu: és el que permet dir "línia 4 217: l'any no és un nombre" en lloc de "error en importar". És l'aplicació directa de 06-04 —l'excepció ha de portar el context que qui la llegeix necessita— a la importació de fitxers.
ready() i per què no serveix com a condició de final
ready() i per què no serveix com a condició de finalready() existeix i el seu nom convida al malentès:
Retorna true si una lectura no es bloquejaria, és a dir, si hi ha dades disponibles a la memòria intermèdia o llestes a la font. I això no és el mateix que "queda alguna cosa per llegir".
// MALAMENT: us incorrecte de ready()
try (BufferedReader lector = new BufferedReader(new FileReader(cami, UTF_8))) {
while (lector.ready()) { // <-- BUG
System.out.println(lector.readLine());
}
}Per què falla, en tres escenaris reals:
| Escenari | Què passa |
|---|---|
| Fitxer local petit | Sol funcionar. Per això el bug sobreviu a les proves |
| Fitxer gran, memòria intermèdia buida en aquell instant | ready() retorna false i el bucle acaba a mitges |
| Consola | ready() és false mentre l'usuari pensa: el bucle no arriba a llegir res |
| Socket de xarxa (mòdul 9) | ready() és false entre paquets: es perden dades |
| Fitxer en unitat de xarxa lenta | Falla de forma intermitent i irreproduïble |
És el pitjor tipus de bug: funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes, de forma no determinista.
L'única condició de final correcta és
readLine() != null.ready()serveix per a una altra cosa: comprovar si pots llegir sense bloquejar-te, en un programa que té una altra feina a fer mentre espera. Aquest cas pertany al mòdul 8.
mark i reset: mirar sense consumir
mark i reset: mirar sense consumirUn flux no permet recular... tret que la memòria intermèdia ho recordi. BufferedReader ofereix aquesta capacitat limitada:
public void mark(int limitDeLectura) throws IOException
public void reset() throws IOException
public boolean markSupported()mark(n) marca la posició actual i promet poder tornar-hi sempre que no es llegeixin més de n caràcters. reset() torna a la marca.
Ús típic: inspeccionar el principi d'un fitxer per decidir com processar-lo, sense perdre aquella primera línia.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DeteccioDeCapcalera {
/**
* Detecta si el fitxer te capcalera; si no en te, la primera
* linia es una dada i NO s'ha de perdre.
*/
public static void processar(String cami) throws IOException {
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
lector.mark(8192); // marge ampli
String primera = lector.readLine();
boolean teCapcalera = primera != null
&& (primera.startsWith("#") || primera.toLowerCase().contains("titol"));
if (!teCapcalera) {
lector.reset(); // tornar la linia al flux
System.out.println("Sense capcalera: la primera linia es una dada");
} else {
System.out.println("Capcalera detectada: " + primera);
}
String linia;
while ((linia = lector.readLine()) != null) {
System.out.println(" dada: " + linia);
}
}
}
}Els seus límits, que cal respectar:
markpot fallar si elreadAheadLimités massa gran. La memòria intermèdia ha de créixer fins a aquesta mida; demanarmark(Integer.MAX_VALUE)intenta reservar 2 GB.- Llegir més de
ncaràcters invalida la marca ireset()llançaIOException. - No tots els
Readerho suporten.markSupported()ho diu.BufferedReadersí;FileReadera seques, no.
A la pràctica es fa servir poc: gairebé sempre és més simple llegir la primera línia i decidir què fer-ne en una variable. Val la pena conèixer-ho perquè apareix en codi d'anàlisi de formats, i perquè explica per què existeix el paràmetre de mida de la memòria intermèdia.
BufferedWriter: write, newLine i flush
BufferedWriter: write, newLine i flushLa contrapartida d'escriptura. La seva API és curta:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class EscripturaAmbBuffer {
public static void escriure(String cami) throws IOException {
try (BufferedWriter escriptor = new BufferedWriter(
new FileWriter(cami, StandardCharsets.UTF_8))) {
escriptor.write("# Cataleg de BiblioTech");
escriptor.newLine(); // separador DEL SISTEMA
escriptor.write("LLIBRE;978-0000000001;Java Eficac");
escriptor.newLine();
escriptor.write("LLIBRE;978-0000000002;Patrons de Disseny");
escriptor.newLine();
}
// close() -> flush() -> les dades arriben al disc (07-02)
}
}| Mètode | Què fa |
|---|---|
write(String) |
Escriu la cadena a la memòria intermèdia |
write(String, int, int) |
Escriu una subcadena |
write(char[], int, int) |
Escriu part d'un array |
write(int) |
Escriu un caràcter |
newLine() |
Escriu System.lineSeparator() |
flush() |
Bolca la memòria intermèdia al flux embolcallat |
close() |
flush() + tancament en cascada |
Dues coses a destacar:
newLine() davant de write("\n"). newLine() fa servir el separador del sistema; write("\n") escriu sempre LF. És la decisió de l'apartat 8 de 07-02: newLine() per a informes, "\n" fix per a fitxers de dades que es comparen o versionen.
A diferència de PrintWriter, BufferedWriter sí que llança IOException. Tots els seus mètodes la declaren. Aquest és un argument seriós al seu favor en codi on el contingut del fitxer importa: no hi ha cap checkError() que puguis oblidar.
Comparació de les dues formes d'escriure text amb memòria intermèdia:
BufferedWriter |
PrintWriter sobre BufferedWriter |
|
|---|---|---|
| Errors d'E/S | Llança IOException |
Els desa: cal cridar checkError() |
| Formatatge | No en té | printf, format |
| Salt de línia | newLine() explícit |
println() el posa |
| Escriure objectes | Només String i char[] |
print(Object) fa servir toString() |
| Quan fer-la servir | Dades: la fallada s'ha de detectar | Informes i sortida llegible |
- El trio
PrintWriter + BufferedWriter + FileWriter
PrintWriter + BufferedWriter + FileWriterAquesta cadena de tres capes és la més habitual per escriure text en Java, i ara pots explicar exactament què aporta cadascuna:
try (PrintWriter sortida = new PrintWriter( // 3. formatatge
new BufferedWriter( // 2. memoria intermedia
new FileWriter(cami, // 1. fitxer + charset
StandardCharsets.UTF_8)))) {
sortida.printf("%-25s %8.2f%n", "Java Eficac", 3.75);
}| Capa | Tipus (07-03) | Què aporta | Què passa si la treus |
|---|---|---|---|
FileWriter |
Node | Connexió amb el fitxer i codificació | No hi ha fitxer. És imprescindible |
BufferedWriter |
Filtre | Agrupa les escriptures; newLine() |
Funciona, però molt més lent |
PrintWriter |
Filtre | println, printf, print(Object) |
Funciona, però cal formatar a mà |
Un matís honest que convé saber: PrintWriter ja té la seva pròpia memòria intermèdia interna, així que la capa intermèdia aporta menys del que la gent es pensa. En molts casos, new PrintWriter(new FileWriter(cami, UTF_8)) rendeix pràcticament igual. La cadena de tres s'escriu per costum i perquè és explícita, no perquè la millora sigui gran.
El que sí que canvia segons la capa exterior és la gestió d'errors, i aquesta és la decisió de veritat:
// Opcio A: PrintWriter. Comode, pero els errors cal preguntar-los.
try (PrintWriter sortida = new PrintWriter(
new BufferedWriter(new FileWriter(cami, UTF_8)))) {
sortida.printf("%-25s %8.2f%n", titol, multa);
if (sortida.checkError()) {
throw new IOException("Fallada en escriure " + cami);
}
}
// Opcio B: BufferedWriter. Mes verbos, pero els errors es propaguen sols.
try (BufferedWriter sortida = new BufferedWriter(new FileWriter(cami, UTF_8))) {
sortida.write(String.format("%-25s %8.2f", titol, multa));
sortida.newLine();
}L'opció B fa servir String.format per conservar el formatatge i BufferedWriter per conservar les excepcions. És la que farem servir a BiblioTech per als fitxers de dades; l'A queda per als informes de consulta.
Files.newBufferedReader i newBufferedWriter
Files.newBufferedReader i newBufferedWriterDes de Java 7 existeix una forma més curta de construir aquestes cadenes, que és la que veuràs en codi modern:
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
// Lectura: equival a new BufferedReader(new InputStreamReader(
// new FileInputStream(...), UTF_8))
try (BufferedReader lector = Files.newBufferedReader(
Path.of("dades/cataleg.txt"), StandardCharsets.UTF_8)) {
String linia;
while ((linia = lector.readLine()) != null) {
processar(linia);
}
}
// Escriptura, amb opcions explicites
try (BufferedWriter escriptor = Files.newBufferedWriter(
Path.of("dades/informe.txt"), StandardCharsets.UTF_8)) {
escriptor.write("...");
escriptor.newLine();
}Avantatges sobre la construcció manual:
| Manual | Files.newBufferedReader |
|
|---|---|---|
| Longitud | Tres constructors imbricats | Una crida |
| Charset | Es pot oblidar | Paràmetre obligatori a la pràctica |
| Excepció si no existeix | FileNotFoundException |
NoSuchFileException, més específica |
| Opcions d'obertura | Només append |
StandardOpenOption completes |
Aquesta és la forma recomanada en codi nou. Path, Files i StandardOpenOption són l'API NIO.2, que es desenvolupa sencera a 07-06. Aquí queda presentada perquè retorna exactament el BufferedReader d'aquesta lliçó: l'objecte és el mateix, només canvia com es construeix.
- Processar un fitxer gran amb memòria constant
Aquesta és la capacitat que fa important BufferedReader, més enllà del rendiment.
El problema de 07-01: carregar un fitxer sencer en memòria consumeix tanta memòria com el fitxer, i amb fitxers grans provoca OutOfMemoryError. La solució: processar en flux.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ProcessamentEnFlux {
/**
* Recorre un fitxer de QUALSEVOL mida amb memoria constant.
*
* En cada instant hi ha en memoria: la memoria intermedia (8 KB), la linia
* actual i els acumuladors. Tant li fa que el fitxer tingui 1 MB o 50 GB.
*/
public static Resum analitzar(String cami) throws IOException {
long linies = 0;
long caracters = 0;
long liniesBuides = 0;
int maxLongitud = 0;
String liniaMesLlarga = "";
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
linies++;
caracters += linia.length();
if (linia.isBlank()) {
liniesBuides++;
}
if (linia.length() > maxLongitud) {
maxLongitud = linia.length();
liniaMesLlarga = linia; // se'n desa UNA, no totes
}
// 'linia' queda disponible per al recollector a la volta seguent
}
}
return new Resum(linies, caracters, liniesBuides, maxLongitud, liniaMesLlarga);
}
public record Resum(long linies, long caracters, long liniesBuides,
int maxLongitud, String liniaMesLlarga) { }
}Comparació de memòria amb un fitxer de 2 GB:
| Enfocament | Memòria al pic | Resultat |
|---|---|---|
Files.readString() |
> 4 GB | OutOfMemoryError |
Files.readAllLines() |
> 5 GB (llista de milions d'String) |
OutOfMemoryError |
BufferedReader línia a línia |
~50 KB | Funciona |
La regla, que ja apuntava 07-01 i ara té la seva eina:
Si el fitxer pot créixer amb l'ús, processa'l en flux. Configuració, plantilles i fitxers de pocs KB es poden carregar sencers. Dades, registres, exportacions i importacions, mai.
I el matís important: processar en flux obliga a dissenyar l'algorisme d'una altra manera. Només pots fer un recorregut i només veus una línia cada vegada. Comptar, sumar, buscar el màxim o filtrar i escriure en un altre fitxer funcionen perfectament. Ordenar el fitxer sencer, no: això requereix una ordenació externa, que és un altre problema.
- Llegir de consola amb
BufferedReader davant de Scanner
BufferedReader davant de ScannerDes del mòdul 1 fas servir Scanner per a la consola. BufferedReader és l'alternativa:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class ConsolaAmbBufferedReader {
public static void main(String[] args) throws IOException {
// System.in es un InputStream (bytes): cal el PONT (07-03)
BufferedReader consola = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
System.out.print("Referencia del material: ");
String referencia = consola.readLine();
System.out.print("Dies de prestec: ");
String text = consola.readLine();
// BufferedReader NO analitza: cal convertir a ma
int dies;
try {
dies = Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
System.out.println("'" + text + "' no es un numero valid");
return;
}
System.out.printf("Prestec de %s per %d dies%n", referencia, dies);
// ATENCIO: NO es tanca el BufferedReader. Tancar-lo tancaria System.in
// per a tota l'aplicacio, i no hi hauria forma de reobrir-lo (06-06).
}
}La comparació completa:
Scanner |
BufferedReader |
|
|---|---|---|
| Anàlisi de tipus | nextInt, nextDouble, nextBoolean |
Cap: Integer.parseInt a mà |
| Llegir una línia | nextLine() |
readLine() |
| Memòria intermèdia | 1024 caràcters | 8192 caràcters |
| Velocitat | Més lenta: fa servir expressions regulars | Notablement més ràpida |
| Excepcions | No comprovades (InputMismatchException) |
Comprovada (IOException) |
| Final d'entrada | hasNextLine() retorna false |
readLine() retorna null |
Sensible al Locale |
Sí: coma o punt decimal | No: no analitza |
| Parany del salt de línia | Sí: nextInt() no el consumeix |
No: readLine() consumeix la línia sencera |
| Expressions regulars | useDelimiter, hasNext(patro) |
No en té |
Tancar System.in |
Perillós | Perillós |
Quan fer servir cadascun:
Scannerper a menús interactius i entrada per consola, que és el que faMenuBiblioTechdes del mòdul 2. La seva anàlisi compensa i el rendiment és irrellevant quan esperes que algú teclegi.BufferedReaderper a fitxers, sempre. En un fitxer de cent mil línies, la diferència de velocitat és de diversos segons.BufferedReaderper a entrada massiva per canonada, quan el programa rep dades perstdindes d'un altre procés.
I l'advertiment que ja coneixes de 06-07, aplicable a tots dos: no tanquis mai System.in. És un recurs global de l'aplicació; tancar-lo deixa el programa sense entrada per sempre. És l'excepció explícita a la regla de tancar-ho tot que 06-06 documentava.
- BiblioTech:
ImportadorCataleg amb informe complet
ImportadorCataleg amb informe completHora del cas pràctic gran de la lliçó, i el segon deute que se salda: a 06-06 vas declarar ImportadorCataleg com a esbós; aquí s'escriu sencer.
Els requisits, que són els d'una importació real:
- Llegir un fitxer de milers de línies amb memòria constant.
- Validar cada línia sense avortar la importació a la primera dolenta.
- Comptar correctes i descartades, amb el motiu de cada descart.
- Registrar els errors amb el logger, no en consola.
- Retornar un informe que qui crida pugui mostrar o desar.
- Ser transaccional en el que és raonable: no deixar el catàleg a mitges si falla el fitxer sencer.
package com.nexussoftware.bibliotech.servei;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.ReferenciaDuplicadaException;
import com.nexussoftware.bibliotech.infraestructura.FormatFitxers;
/**
* Importacio massiva del cataleg de BiblioTech des d'un fitxer de text.
*
* Format esperat (una linia per material):
* LLIBRE;isbn;titol;autor;any
*
* Politica (06-07):
* - Una linia dolenta NO avorta la importacio: es descarta i es compta.
* - El fitxer absent o illegible SI que avorta: no hi ha res per importar.
* - Els materials s'acumulen i es bolquen al cataleg AL FINAL, per no
* deixar-lo a mitges si el fitxer resulta estar corrupte del tot.
*
* Processa en FLUX: memoria constant sigui quina sigui la mida del fitxer.
*/
public class ImportadorCataleg {
private static final Logger LOG = Logger.getLogger(ImportadorCataleg.class.getName());
private static final int CAMPS_ESPERATS = 5;
private static final int ANY_MINIM = 1450;
private static final int ANY_MAXIM = 2100;
/** Si mes d'aquesta fraccio de linies falla, el fitxer es sospitos. */
private static final double LLINDAR_FITXER_CORRUPTE = 0.5;
/** Errors detallats que es desen; la resta nomes es compten. */
private static final int MAX_ERRORS_DETALLATS = 100;
private final File fitxer;
public ImportadorCataleg(String cami) {
this.fitxer = new File(Objects.requireNonNull(cami, "El cami no pot ser nul"));
}
// ------------------------- L'INFORME -------------------------
/** Motiu pel qual es descarta una línia. Enum (04-07): sense cadenes magiques. */
public enum MotiuDescart {
NUMERO_DE_CAMPS("Nombre de camps incorrecte"),
TIPUS_DESCONEGUT("Tipus de material no reconegut"),
CAMP_BUIT("Camp obligatori buit"),
ANY_NO_NUMERIC("L'any no es un numero"),
ANY_FORA_DE_RANG("Any fora del rang admes"),
ISBN_INVALID("L'ISBN no te el format esperat"),
DUPLICAT("Referencia o ISBN ja presents");
private final String descripcio;
MotiuDescart(String descripcio) { this.descripcio = descripcio; }
public String getDescripcio() { return descripcio; }
}
/** Una linia descartada, amb tot el necessari per corregir-la. */
public record LiniaDescartada(int numero, MotiuDescart motiu, String detall,
String contingut) {
@Override
public String toString() {
return String.format("Linia %d [%s]: %s | %s",
numero, motiu.name(), detall, retallar(contingut));
}
private static String retallar(String s) {
return (s.length() <= 60) ? s : s.substring(0, 57) + "...";
}
}
/** Resultat complet de la importacio. */
public static class Informe {
private final List<Material> importats = new ArrayList<>();
private final List<LiniaDescartada> descartades = new ArrayList<>();
private final Map<MotiuDescart, Integer> perMotiu = new LinkedHashMap<>();
private int liniesLlegides = 0;
private int liniesIgnorades = 0; // buides i comentaris
private int descartsTotals = 0; // inclou els no detallats
private long millisegons = 0;
private boolean avortada = false;
private String motiuAvortament = null;
void comptarLinia() { liniesLlegides++; }
void comptarIgnorada() { liniesIgnorades++; }
void afegir(Material m) { importats.add(m); }
void descartar(LiniaDescartada d) {
descartsTotals++;
perMotiu.merge(d.motiu(), 1, Integer::sum); // 05-05
if (descartades.size() < MAX_ERRORS_DETALLATS) {
descartades.add(d);
}
}
void avortar(String motiu) {
avortada = true;
motiuAvortament = motiu;
}
public List<Material> getImportats() { return List.copyOf(importats); }
public List<LiniaDescartada> getDescartades() { return List.copyOf(descartades); }
public Map<MotiuDescart, Integer> getPerMotiu() { return Map.copyOf(perMotiu); }
public int getLiniesLlegides() { return liniesLlegides; }
public int getLiniesIgnorades() { return liniesIgnorades; }
public int getImportatsTotal() { return importats.size(); }
public int getDescartsTotals() { return descartsTotals; }
public long getMillisegons() { return millisegons; }
public boolean estaAvortada() { return avortada; }
public String getMotiuAvortament() { return motiuAvortament; }
public double taxaExit() {
int processades = importats.size() + descartsTotals;
return (processades == 0) ? 0.0 : (importats.size() * 100.0) / processades;
}
/** Informe llegible per a la capa de presentacio. */
public String resum() {
StringBuilder sb = new StringBuilder();
sb.append("=== INFORME D'IMPORTACIO ===\n");
if (avortada) {
sb.append(" IMPORTACIO AVORTADA: ").append(motiuAvortament).append('\n');
return sb.toString();
}
sb.append(String.format(" Linies llegides : %d%n", liniesLlegides));
sb.append(String.format(" Ignorades : %d (buides i comentaris)%n",
liniesIgnorades));
sb.append(String.format(" Importades : %d%n", importats.size()));
sb.append(String.format(" Descartades : %d%n", descartsTotals));
sb.append(String.format(" Taxa d'exit : %.1f%%%n", taxaExit()));
sb.append(String.format(" Temps : %d ms%n", millisegons));
if (!perMotiu.isEmpty()) {
sb.append(" --- Descarts per motiu ---\n");
for (Map.Entry<MotiuDescart, Integer> e : perMotiu.entrySet()) {
sb.append(String.format(" %-22s %4d (%s)%n",
e.getKey().name(), e.getValue(),
e.getKey().getDescripcio()));
}
}
if (!descartades.isEmpty()) {
sb.append(" --- Primeres linies descartades ---\n");
int mostrades = Math.min(10, descartades.size());
for (int i = 0; i < mostrades; i++) {
sb.append(" ").append(descartades.get(i)).append('\n');
}
if (descartsTotals > mostrades) {
sb.append(String.format(" ... i %d mes%n",
descartsTotals - mostrades));
}
}
return sb.toString();
}
}
// ------------------------- LA IMPORTACIO -------------------------
/**
* Importa el fitxer al cataleg donat.
*
* @return l'informe. Mai null, ni tan sols si avorta.
*/
public Informe importar(Cataleg cataleg) {
Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
Informe informe = new Informe();
long inici = System.nanoTime();
LOG.info(() -> "Iniciant importacio des de " + fitxer.getAbsolutePath());
// BufferedReader: memoria constant encara que el fitxer tingui milions
// de linies. Charset explicit, el mateix que fa servir l'exportador.
try (BufferedReader lector = new BufferedReader(
new FileReader(fitxer, FormatFitxers.CHARSET))) {
String linia;
int numero = 0;
while ((linia = lector.readLine()) != null) { // el bucle canonic
numero++;
informe.comptarLinia();
processarLinia(linia, numero, informe);
}
} catch (java.io.FileNotFoundException e) {
informe.avortar("No existeix el fitxer " + fitxer.getAbsolutePath());
LOG.log(Level.WARNING, "Importacio avortada: fitxer no trobat", e);
return finalitzar(informe, inici);
} catch (IOException e) {
informe.avortar("Fallada d'E/S llegint " + fitxer.getAbsolutePath()
+ ": " + e.getMessage());
LOG.log(Level.SEVERE, "Importacio avortada per fallada d'E/S", e);
return finalitzar(informe, inici);
}
// Comprovacio de seny: si falla mes de la meitat, alguna cosa va malament
// amb el fitxer sencer (charset equivocat, format diferent, columnes
// canviades). Millor rebutjar-lo que ficar brossa al cataleg.
int processades = informe.getImportatsTotal() + informe.getDescartsTotals();
if (processades > 10
&& informe.getDescartsTotals() > processades * LLINDAR_FITXER_CORRUPTE) {
informe.avortar(String.format(
"Ha fallat el %.0f%% de les linies (%d de %d). El fitxer no sembla "
+ "tenir el format esperat; no s'importa res.",
100 - informe.taxaExit(), informe.getDescartsTotals(), processades));
LOG.severe(() -> "Importacio rebutjada: " + informe.getMotiuAvortament());
return finalitzar(informe, inici);
}
// Bolcat al cataleg AL FINAL: fins aqui no s'ha tocat res
bolcar(cataleg, informe);
return finalitzar(informe, inici);
}
/** Valida i construeix el material d'una linia. No llanca mai cap amunt. */
private void processarLinia(String linia, int numero, Informe informe) {
String neta = linia.trim();
if (neta.isEmpty() || neta.startsWith(FormatFitxers.COMENTARI)) {
informe.comptarIgnorada();
return;
}
// El -1 conserva els camps buits del final (07-01, solucio 3)
String[] camps = neta.split(FormatFitxers.SEPARADOR_CAMPS, -1);
if (camps.length != CAMPS_ESPERATS) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.NUMERO_DE_CAMPS,
"n'hi ha " + camps.length + " i se n'esperaven " + CAMPS_ESPERATS,
neta));
return;
}
String tipus = camps[0].trim().toUpperCase();
String isbn = camps[1].trim();
String titol = camps[2].trim();
String autor = camps[3].trim();
String anyTx = camps[4].trim();
if (!"LLIBRE".equals(tipus)) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.TIPUS_DESCONEGUT,
"'" + tipus + "'", neta));
return;
}
if (titol.isEmpty() || isbn.isEmpty()) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.CAMP_BUIT,
titol.isEmpty() ? "titol" : "isbn", neta));
return;
}
if (!isbn.matches("\\d{3}-\\d{10}")) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.ISBN_INVALID,
"'" + isbn + "' no compleix NNN-NNNNNNNNNN", neta));
return;
}
int any;
try {
any = Integer.parseInt(anyTx);
} catch (NumberFormatException e) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.ANY_NO_NUMERIC,
"'" + anyTx + "'", neta));
return;
}
if (any < ANY_MINIM || any > ANY_MAXIM) {
informe.descartar(new LiniaDescartada(numero, MotiuDescart.ANY_FORA_DE_RANG,
any + " fora de [" + ANY_MINIM + ", " + ANY_MAXIM + "]", neta));
return;
}
informe.afegir(new Llibre(titol, autor.isEmpty() ? "Desconegut" : autor, isbn, any));
}
/**
* Bolca al cataleg el que s'ha importat.
*
* Els duplicats es detecten AQUI, perque nomes el cataleg els coneix.
* Un duplicat no invalida la importacio sencera: es descarta i es continua.
*/
private void bolcar(Cataleg cataleg, Informe informe) {
List<Material> pendents = informe.getImportats();
List<Material> acceptats = new ArrayList<>(pendents.size());
for (Material m : pendents) {
try {
cataleg.registrar(m);
acceptats.add(m);
} catch (ReferenciaDuplicadaException e) {
informe.descartar(new LiniaDescartada(0, MotiuDescart.DUPLICAT,
e.getMessage(), m.getReferencia()));
}
}
informe.importats.clear();
informe.importats.addAll(acceptats); // nomes el realment registrat
}
private Informe finalitzar(Informe informe, long inici) {
informe.millisegons = (System.nanoTime() - inici) / 1_000_000;
if (informe.estaAvortada()) {
LOG.warning(() -> "Importacio avortada: " + informe.getMotiuAvortament());
} else {
LOG.info(() -> String.format(
"Importacio acabada: %d importats, %d descartats, %d ms",
informe.getImportatsTotal(), informe.getDescartsTotals(),
informe.getMillisegons()));
}
return informe;
}
}I el seu ús des de la presentació:
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.ImportadorCataleg;
public class DemoImportacio {
public static void main(String[] args) {
Cataleg cataleg = new Cataleg();
ImportadorCataleg importador =
new ImportadorCataleg("dades/cataleg-complet.txt");
ImportadorCataleg.Informe informe = importador.importar(cataleg);
System.out.println(informe.resum());
System.out.println("Materials al cataleg: " + cataleg.mida());
}
}Sortida amb un fitxer de 5 000 línies amb alguns errors:
=== INFORME D'IMPORTACIO ===
Linies llegides : 5003
Ignorades : 3 (buides i comentaris)
Importades : 4962
Descartades : 38
Taxa d'exit : 99.2%
Temps : 87 ms
--- Descarts per motiu ---
ANY_NO_NUMERIC 14 (L'any no es un numero)
ISBN_INVALID 11 (L'ISBN no te el format esperat)
NUMERO_DE_CAMPS 7 (Nombre de camps incorrecte)
DUPLICAT 4 (Referencia o ISBN ja presents)
CAMP_BUIT 2 (Camp obligatori buit)
--- Primeres linies descartades ---
Linia 47 [ANY_NO_NUMERIC]: 'mil nou-cents' | LLIBRE;978-0000000047;Un llibre;Autor;mil...
Linia 112 [NUMERO_DE_CAMPS]: n'hi ha 4 i se n'esperaven 5 | LLIBRE;978-0000000112;Altre;A
Linia 340 [ISBN_INVALID]: '97800000340' no compleix NNN-NNNNNNNNNN | LLIBRE;97800000340;
... i 35 mesLes set decisions de disseny que cal entendre d'aquesta classe:
BufferedReaderi bucle canònic. Memòria constant: el fitxer podria tenir cinc milions de línies i el consum seria el mateix.- Una línia dolenta no avorta. Es descarta amb el seu motiu i el seu número de línia. Qui importa vol veure tots els problemes de cop.
- Un fitxer majoritàriament dolent sí que avorta. Si falla més de la meitat, el fitxer no és el que s'esperava —charset equivocat, columnes canviades, format diferent— i ficar aquella brossa al catàleg seria pitjor que no importar. És la distinció recuperable/irrecuperable de 06-07 aplicada a la importació.
- El bolcat al catàleg va al final. Fins aquell moment el catàleg no s'ha tocat, així que un avortament el deixa exactament com estava. És la coherència d'estat de 06-05, en versió importació.
- Els detalls es limiten a 100. Un fitxer corrupte amb un milió de línies dolentes generaria un milió d'objectes
LiniaDescartadai esgotaria la memòria. Es compten totes i es detallen les cent primeres: és informació suficient per corregir, i acotada. MotiuDescartés unenum, no una cadena. Permet agrupar, comptar i traduir, i elimina les cadenes màgiques. És 04-07 aplicat.- Tot el diagnòstic va al logger; l'informe es retorna. La classe de servei no imprimeix: retorna un objecte i qui la crida decideix si el mostra per consola, el desa o l'envia per correu. És la separació de capes de 06-07.
Errors Comuns i Consells
- Fer servir
ready()com a condició de final. El bug que funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes. La condició correcta és semprereadLine() != null. - Cridar
readLine()dues vegades per volta. Processa les línies parelles i se salta les senars. Amb un fitxer de prova petit pot semblar correcte. - Confondre
nullamb"".nullés final de fitxer;""és una línia buida. Fer servirisEmpty()com a condició de final atura la lectura al primer buit i després llançaNullPointerException. - Oblidar els parèntesis a
while ((linia = lector.readLine()) != null). No compila, i és la forma idiomàtica que cal memoritzar. - Esperar que
readLine()inclogui el salt de línia. No l'inclou. Si estàs reescrivint el fitxer, l'has de reposar ambnewLine(). - No tancar el
BufferedWriter. El fitxer queda buit: les dades són a la memòria intermèdia. En escriptura, no tancar és perdre dades (07-02). - Creure que
BufferedWriterescriu immediatament. No ho fa, i per disseny. Si necessites que un altre procés vegi les dades ja,flush(). - Tancar
System.in. Deixa l'aplicació sense entrada per sempre. És l'excepció documentada a la regla de tancar-ho tot. - Embolcallar una cosa que ja té memòria intermèdia.
new BufferedReader(new BufferedReader(...))no aporta res; sobre un array a la RAM, tampoc. - Posar una memòria intermèdia enorme "per si de cas". Cent fluxos amb memòries intermèdies d'1 MB són 100 MB de heap, i a partir de 64 KB la millora és menyspreable.
- Fer servir
PrintWritersensecheckError()per a dades que importen. S'empassa lesIOException. Per a fitxers de dades,BufferedWriter, els mètodes del qual sí que les declaren. - Fer servir
Scannerper llegir un fitxer de cent mil línies. Funciona i triga diversos segons de més.BufferedReaderper a fitxers,Scannerper a menús. - Acumular totes les línies en una llista "per processar-les després". Anul·la l'avantatge del processament en flux i reintrodueix l'
OutOfMemoryError. - Desar un objecte d'error per cada línia dolenta sense límit. Un fitxer corrupte d'un milió de línies esgota la memòria mentre informa que hi ha un problema.
- Consell: porta sempre el número de línia. "Línia 4 217: l'any no és un nombre" és accionable; "error en importar" no ho és. És 06-04 aplicat a fitxers.
- Consell: posa un llindar de seny a tota importació. Si falla més de la meitat, el problema és el fitxer, no les línies. Rebutjar-lo sencer és més correcte que importar brossa.
- Consell: que el servei retorni l'informe, no el imprimeixi. Qui crida decideix si el mostra, el desa o l'envia. La capa de servei no sap qui hi ha a l'altre costat.
- Consell: fes servir
Files.newBufferedReaderen codi nou. És més curt, obliga al charset i dona excepcions més específiques. El veuràs sencer a 07-06.
Exercicis
Exercici 1: comptador d'estadístiques d'un fitxer
Escriu EstadistiquesFitxer que analitzi un fitxer de text en un sol recorregut i amb memòria constant, produint:
- Nombre de línies, de paraules i de caràcters.
- Línies buides i línies de comentari (que comencen per
#). - Longitud mitjana, mínima i màxima de línia.
- Les tres paraules més freqüents (fes servir un
HashMapdel mòdul 5; no facis servir l'API de Streams, que és 10-04). - El número de la línia més llarga i els seus primers 60 caràcters.
Requisits: BufferedReader, charset explícit, try-with-resources, i un record per al resultat. Ha de funcionar igual amb un fitxer d'1 KB que amb un d'1 GB.
Exercici 2: filtre de línies amb reescriptura
Escriu FiltreCataleg que llegeixi un fitxer de catàleg i escrigui un altre fitxer només amb les línies que compleixin un criteri, sense carregar cap dels dos en memòria:
filtrar(String origen, String desti, Predicat criteri)onPredicatés una interfície funcional pròpia (04-06) ambboolean compleix(String linia, int numero).- Conserva les línies de comentari i la capçalera.
- Compta línies llegides, escrites i descartades, i retorna el resultat en un
record. - Fes servir escriptura atòmica (07-02): escriu a un temporal i reanomena al final.
- Un
mainque filtri per any de publicació posterior a 2000 i per autor que contingui un text donat.
Exercici 3: comparador de dos catàlegs
Escriu ComparadorCatalegs que compari dos fitxers de catàleg i informi de les diferències:
- Materials només al primer (baixes).
- Materials només al segon (altes).
- Materials a tots dos però amb dades diferents (modificacions), indicant quin camp ha canviat.
- Materials idèntics (només el recompte).
Requisits: llegeix cada fitxer una sola vegada amb BufferedReader, indexa per ISBN amb un HashMap (05-05), i produeix un informe amb printf. Explica en un comentari per què aquest algorisme sí que necessita carregar un dels dos fitxers en memòria i quin convé que sigui, i què faries si cap dels dos no hi cabés.
Solucions
Solució 1
package com.nexussoftware.bibliotech.util;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Estadistiques d'un fitxer de text en UN SOL RECORREGUT i amb memoria
* constant respecte a la mida del fitxer.
*
* L'unic que creix es el mapa de frequencies, que depen del nombre de
* paraules DIFERENTS, no de la mida del fitxer. Un fitxer de 10 GB en
* catala te de l'ordre de cent mil paraules diferents: perfectament
* manejable.
*/
public class EstadistiquesFitxer {
private static final int PARAULES_TOP = 3;
private static final int LONGITUD_MOSTRA = 60;
public record Estadistiques(
long linies, long paraules, long caracters,
long liniesBuides, long liniesComentari,
double longitudMitjana, int longitudMinima, int longitudMaxima,
int numeroLiniaMesLlarga, String mostraLiniaMesLlarga,
List<Map.Entry<String, Integer>> paraulesFrequents) {
public String informe() {
StringBuilder sb = new StringBuilder();
sb.append("=== ESTADISTIQUES DEL FITXER ===\n");
sb.append(String.format(" Linies : %d%n", linies));
sb.append(String.format(" buides : %d%n", liniesBuides));
sb.append(String.format(" comentaris : %d%n", liniesComentari));
sb.append(String.format(" Paraules : %d%n", paraules));
sb.append(String.format(" Caracters : %d%n", caracters));
sb.append(String.format(" Longitud mitjana : %.1f%n", longitudMitjana));
sb.append(String.format(" Longitud minima : %d%n", longitudMinima));
sb.append(String.format(" Longitud maxima : %d (linia %d)%n",
longitudMaxima, numeroLiniaMesLlarga));
sb.append(String.format(" mostra : %s%n", mostraLiniaMesLlarga));
sb.append(" Paraules mes frequents:\n");
for (Map.Entry<String, Integer> e : paraulesFrequents) {
sb.append(String.format(" %-20s %6d%n", e.getKey(), e.getValue()));
}
return sb.toString();
}
}
public static Estadistiques analitzar(String cami) throws IOException {
long linies = 0, paraules = 0, caracters = 0;
long buides = 0, comentaris = 0;
int minima = Integer.MAX_VALUE, maxima = 0;
int numeroMesLlarga = 0;
String mostraMesLlarga = "";
// Nomes aixo creix, i amb el nombre de paraules DIFERENTS
Map<String, Integer> frequencies = new HashMap<>();
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) { // bucle canonic
linies++;
caracters += linia.length();
int longitud = linia.length();
if (longitud < minima) { minima = longitud; }
if (longitud > maxima) {
maxima = longitud;
numeroMesLlarga = (int) linies;
mostraMesLlarga = longitud <= LONGITUD_MOSTRA
? linia
: linia.substring(0, LONGITUD_MOSTRA) + "...";
}
String neta = linia.trim();
if (neta.isEmpty()) {
buides++;
continue;
}
if (neta.startsWith("#")) {
comentaris++;
continue;
}
// Separa per qualsevol cosa que no sigui lletra o digit
for (String paraula : neta.toLowerCase().split("[^\\p{L}\\p{N}]+")) {
if (paraula.length() > 2) { // ignora articles curts
paraules++;
frequencies.merge(paraula, 1, Integer::sum); // 05-05
}
}
// 'linia' es descarta aqui: memoria constant
}
}
if (minima == Integer.MAX_VALUE) {
minima = 0; // fitxer buit
}
return new Estadistiques(
linies, paraules, caracters, buides, comentaris,
linies == 0 ? 0.0 : (double) caracters / linies,
minima, maxima, numeroMesLlarga, mostraMesLlarga,
mesFrequents(frequencies, PARAULES_TOP));
}
/**
* Les n entrades amb valor mes gran.
*
* Sense l'API de Streams (10-04): llista + Comparator + sort, que es
* exactament el de 05-09.
*/
private static List<Map.Entry<String, Integer>> mesFrequents(
Map<String, Integer> frequencies, int n) {
List<Map.Entry<String, Integer>> entrades = new ArrayList<>(frequencies.entrySet());
// Descendent per valor; a igualtat, alfabetic per clau
entrades.sort(Comparator
.comparing(Map.Entry<String, Integer>::getValue).reversed()
.thenComparing(Map.Entry::getKey));
return List.copyOf(entrades.subList(0, Math.min(n, entrades.size())));
}
public static void main(String[] args) throws IOException {
String cami = (args.length > 0) ? args[0] : "dades/cataleg.txt";
System.out.println(analitzar(cami).informe());
}
}Sortida:
=== ESTADISTIQUES DEL FITXER ===
Linies : 5003
Linies buides : 1
Comentaris : 2
Paraules : 19842
Caracters : 248915
Longitud mitjana : 49.8
Longitud minima : 0
Longitud maxima : 87 (linia 1204)
mostra : LLIBRE;978-0000001204;Introduccio als sistemes distribuits...
Paraules mes frequents:
llibre 5000
programacio 412
sistemes 287El punt clau és al comentari de capçalera: l'única dada que creix amb el contingut és el mapa de freqüències, i creix amb el nombre de paraules diferents, no amb la mida del fitxer. Tota la resta són acumuladors de mida fixa. Per això aquesta anàlisi funciona amb un fitxer de 10 GB. Fixa't també que la "línia més llarga" es desa retallada: desar-la sencera seria un risc si aquella línia tingués 100 MB.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
* Filtra un fitxer de cataleg escrivint-ne un altre amb les linies que
* compleixin un criteri, SENSE carregar cap dels dos en memoria.
*
* Llegeix i escriu en flux simultaniament: en cada instant hi ha en memoria
* una linia i dues memories intermedies de 8 KB.
*/
public class FiltreCataleg {
private static final Logger LOG = Logger.getLogger(FiltreCataleg.class.getName());
private static final String COMENTARI = "#";
/**
* Criteri de filtratge. Interficie funcional propia (04-06): rep la
* linia i el seu numero, per si el criteri depen de la posicio.
*/
@FunctionalInterface
public interface Predicat {
boolean compleix(String linia, int numero);
}
public record ResultatFiltre(int llegides, int escrites, int descartades,
int comentarisConservats, long millisegons) {
public String resum() {
return String.format(
"Filtrat: %d llegides, %d escrites, %d descartades, "
+ "%d comentaris conservats (%d ms)",
llegides, escrites, descartades, comentarisConservats, millisegons);
}
}
/**
* Filtra origen cap a desti de forma ATOMICA (07-02).
*
* Si alguna cosa falla a mitges, el desti anterior queda intacte.
*/
public ResultatFiltre filtrar(String origen, String desti, Predicat criteri)
throws IOException {
long inici = System.nanoTime();
File fitxerDesti = new File(desti);
File temporal = new File(fitxerDesti.getAbsolutePath() + ".tmp");
int llegides = 0, escrites = 0, descartades = 0, comentaris = 0;
boolean completat = false;
try {
// Lectura i escriptura simultanies, totes dues amb memoria intermedia.
// Es declaren els dos recursos al mateix try-with-resources:
// es tanquen en ordre invers, l'escriptor primer (06-06).
try (BufferedReader lector = new BufferedReader(
new FileReader(origen, StandardCharsets.UTF_8));
BufferedWriter escriptor = new BufferedWriter(
new FileWriter(temporal, StandardCharsets.UTF_8))) {
String linia;
int numero = 0;
while ((linia = lector.readLine()) != null) {
numero++;
llegides++;
String neta = linia.trim();
// Comentaris i capcaleres: es conserven sempre
if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
escriptor.write(linia);
escriptor.newLine();
comentaris++;
continue;
}
if (criteri.compleix(linia, numero)) {
escriptor.write(linia);
escriptor.newLine(); // readLine NO retorna el salt
escrites++;
} else {
descartades++;
}
}
// BufferedWriter llanca IOException de veritat: sense checkError
}
// Substitucio atomica
if (fitxerDesti.exists() && !fitxerDesti.delete()) {
throw new IOException("No s'ha pogut substituir " + desti);
}
if (!temporal.renameTo(fitxerDesti)) {
throw new IOException("No s'ha pogut reanomenar " + temporal.getName());
}
completat = true;
} finally {
if (!completat && temporal.exists() && !temporal.delete()) {
LOG.warning(() -> "Temporal sense esborrar: " + temporal.getAbsolutePath());
}
}
ResultatFiltre resultat = new ResultatFiltre(llegides, escrites, descartades,
comentaris, (System.nanoTime() - inici) / 1_000_000);
LOG.info(resultat::resum);
return resultat;
}
// ------------------------- CRITERIS -------------------------
/** Any de publicacio posterior al donat. Camp 5 del format. */
public static Predicat publicatDespresDe(int any) {
return (linia, numero) -> {
String[] camps = linia.split(";", -1);
if (camps.length < 5) {
return false;
}
try {
return Integer.parseInt(camps[4].trim()) > any;
} catch (NumberFormatException e) {
return false; // any illegible: no compleix
}
};
}
/** L'autor conte el text donat, sense distingir majuscules. */
public static Predicat autorConte(String text) {
String buscat = text.toLowerCase();
return (linia, numero) -> {
String[] camps = linia.split(";", -1);
return camps.length >= 4 && camps[3].toLowerCase().contains(buscat);
};
}
/** Combinacio de dos criteris (composicio de lambdes, 04-05). */
public static Predicat i(Predicat a, Predicat b) {
return (linia, numero) -> a.compleix(linia, numero) && b.compleix(linia, numero);
}
public static void main(String[] args) throws IOException {
FiltreCataleg filtre = new FiltreCataleg();
System.out.println(filtre.filtrar(
"dades/cataleg.txt",
"dades/cataleg-recents.txt",
publicatDespresDe(2000)).resum());
System.out.println(filtre.filtrar(
"dades/cataleg.txt",
"dades/cataleg-bloch.txt",
autorConte("bloch")).resum());
System.out.println(filtre.filtrar(
"dades/cataleg.txt",
"dades/cataleg-recents-fowler.txt",
i(publicatDespresDe(1995), autorConte("fowler"))).resum());
}
}Sortida:
Filtrat: 5003 llegides, 3418 escrites, 1582 descartades, 3 comentaris conservats (61 ms)
Filtrat: 5003 llegides, 12 escrites, 4988 descartades, 3 comentaris conservats (48 ms)
Filtrat: 5003 llegides, 7 escrites, 4993 descartades, 3 comentaris conservats (52 ms)Els quatre punts didàctics:
- Lectura i escriptura simultànies en flux. Cap dels dos fitxers no es carrega: el consum és d'una línia i dues memòries intermèdies, sigui quina sigui la mida. És el patró que fan servir les eines de línia d'ordres de tota la vida.
- Els dos recursos van al mateix
try-with-resources. Es tanquen en ordre invers —l'escriptor primer— i les excepcions de tancament es registren com a suprimides (06-06). Sense aquest tancament, la memòria intermèdia de l'escriptor no es buidaria. escriptor.newLine()és obligatori.readLine()no retorna el terminador, així que si no el reposes, el fitxer de sortida surt amb totes les línies enganxades. És l'error més freqüent en reescriure fitxers.- La interfície funcional pròpia permet compondre criteris amb lambdes i combinar-los amb
i(...), exactament comReglaTarifaiFiltreMaterialde 04-06. A 10-04 veuràs que l'API de Streams té això mateix ambPredicateiand.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Compara dos fitxers de cataleg i informa d'altes, baixes i canvis.
*
* PER QUE AQUEST ALGORISME SI QUE NECESSITA MEMORIA:
* Per saber si un material del fitxer B existeix a A, cal poder
* consultar-lo. Com que els fluxos son SEQUENCIALS i no permeten tornar
* enrere (07-03), l'unica forma de consultar A mentre es recorre B es tenir
* A indexat en memoria.
*
* QUIN CONVE CARREGAR: el mes petit. Es recorre en flux el gran.
*
* SI CAP NO HI CAP: hi ha dues sortides classiques. Ordenar tots dos fitxers
* per clau i fer-ne una "fusio" recorrent-los en parallel, que necessita
* memoria constant; o carregar nomes les CLAUS (els ISBN) amb un hash de
* cada linia, que ocupa una fraccio minima. La segona es l'habitual.
*/
public class ComparadorCatalegs {
private static final String SEPARADOR = ";";
private static final String COMENTARI = "#";
/** Dades d'un material tal com apareixen al fitxer. */
private record Registre(String isbn, String titol, String autor, int any) {
/** Quins camps difereixen respecte a un altre registre amb el mateix ISBN. */
List<String> diferenciesAmb(Registre altre) {
List<String> canvis = new ArrayList<>();
if (!titol.equals(altre.titol)) {
canvis.add("titol: '" + titol + "' -> '" + altre.titol + "'");
}
if (!autor.equals(altre.autor)) {
canvis.add("autor: '" + autor + "' -> '" + altre.autor + "'");
}
if (any != altre.any) {
canvis.add("any: " + any + " -> " + altre.any);
}
return canvis;
}
}
public record Diferencies(List<String> baixes, List<String> altes,
List<String> modificacions, int identics) {
public String informe() {
StringBuilder sb = new StringBuilder();
sb.append("=== COMPARACIO DE CATALEGS ===\n");
sb.append(String.format(" Identics : %d%n", identics));
sb.append(String.format(" Altes : %d%n", altes.size()));
sb.append(String.format(" Baixes : %d%n", baixes.size()));
sb.append(String.format(" Modificacions : %d%n", modificacions.size()));
imprimirBloc(sb, "ALTES (nomes al nou)", altes);
imprimirBloc(sb, "BAIXES (nomes a l'antic)", baixes);
imprimirBloc(sb, "MODIFICACIONS", modificacions);
return sb.toString();
}
private void imprimirBloc(StringBuilder sb, String titol, List<String> linies) {
if (linies.isEmpty()) {
return;
}
sb.append(" --- ").append(titol).append(" ---\n");
int mostrades = Math.min(10, linies.size());
for (int i = 0; i < mostrades; i++) {
sb.append(" ").append(linies.get(i)).append('\n');
}
if (linies.size() > mostrades) {
sb.append(String.format(" ... i %d mes%n", linies.size() - mostrades));
}
}
}
/**
* Compara dos catalegs.
*
* @param camiAntic el que es carrega EN MEMORIA (conve el mes petit)
* @param camiNou el que es recorre EN FLUX
*/
public Diferencies comparar(String camiAntic, String camiNou) throws IOException {
// FASE 1: indexar l'antic. Un sol recorregut.
Map<String, Registre> antics = indexar(camiAntic);
List<String> altes = new ArrayList<>();
List<String> modificacions = new ArrayList<>();
int identics = 0;
// FASE 2: recorrer el nou EN FLUX, consultant l'index
try (BufferedReader lector = new BufferedReader(
new FileReader(camiNou, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
Registre nou = parsejar(linia);
if (nou == null) {
continue; // comentari o linia dolenta
}
// remove() en lloc de get(): el que quedi al final son les BAIXES.
// Estalvia un segon recorregut i una estructura addicional.
Registre antic = antics.remove(nou.isbn());
if (antic == null) {
altes.add(nou.isbn() + " - " + nou.titol());
continue;
}
List<String> canvis = antic.diferenciesAmb(nou);
if (canvis.isEmpty()) {
identics++;
} else {
modificacions.add(nou.isbn() + " - " + String.join("; ", canvis));
}
}
}
// FASE 3: el que sobra de l'index son les baixes
List<String> baixes = new ArrayList<>();
for (Registre r : antics.values()) {
baixes.add(r.isbn() + " - " + r.titol());
}
baixes.sort(null); // ordre natural (05-09)
altes.sort(null);
modificacions.sort(null);
return new Diferencies(baixes, altes, modificacions, identics);
}
/** Carrega un cataleg indexat per ISBN. Un sol recorregut, amb memoria intermedia. */
private Map<String, Registre> indexar(String cami) throws IOException {
Map<String, Registre> index = new HashMap<>();
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
Registre r = parsejar(linia);
if (r != null) {
index.put(r.isbn(), r);
}
}
}
return index;
}
/** Retorna null si la linia no es un registre valid. */
private Registre parsejar(String linia) {
String neta = linia.trim();
if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
return null;
}
String[] c = neta.split(SEPARADOR, -1);
if (c.length != 5) {
return null;
}
try {
return new Registre(c[1].trim(), c[2].trim(), c[3].trim(),
Integer.parseInt(c[4].trim()));
} catch (NumberFormatException e) {
return null;
}
}
public static void main(String[] args) throws IOException {
Diferencies d = new ComparadorCatalegs()
.comparar("dades/cataleg-2025.txt", "dades/cataleg-2026.txt");
System.out.println(d.informe());
}
}Sortida:
=== COMPARACIO DE CATALEGS ===
Identics : 4871
Altes : 92
Baixes : 37
Modificacions : 14
--- ALTES (nomes al nou) ---
978-0000005001 - Arquitectura hexagonal
978-0000005002 - Java modern
... i 90 mes
--- BAIXES (nomes a l'antic) ---
978-0000000341 - Manual de COBOL
... i 36 mes
--- MODIFICACIONS ---
978-0000000002 - titol: 'Patrons de Disseny' -> 'Patrons de Disseny i Refactoritzacio'
978-0000000047 - any: 2018 -> 2019
... i 12 mesEls tres punts que cal extreure'n:
- El comentari de capçalera respon a la pregunta de l'enunciat. Un flux és seqüencial i no permet tornar enrere, així que consultar un fitxer mentre en recorres un altre obliga a tenir-ne un en memòria. Carrega el petit, recorre el gran. I si cap no hi cap, les dues sortides clàssiques són la fusió de fitxers ordenats —memòria constant— o carregar només les claus amb un hash de cada línia, que és una fracció mínima del contingut.
remove()en lloc deget(). Un truc que estalvia feina: en treure de l'índex el que es troba, el que queda al final són exactament les baixes. Sense ell calarien un segon recorregut i una estructura addicional.- Fixa't en la primera modificació de la sortida: un títol que canvia perquè algú l'ha completat a mà en una de les dues versions del fitxer. És exactament el tipus de diferència que un comparador de catàlegs treu a la llum, i una raó més per mantenir una única font de veritat del catàleg.
Conclusió
Ja saps què és una memòria intermèdia i per què és a tot arreu.
Entens que és un array intermedi que agrupa les operacions per reduir les crides al sistema: una de cada 8192 en lloc de totes. Tens les xifres que ho justifiquen —de 4 500 ms a 68 ms només embolcallant el flux, un factor de seixanta-sis— i saps que el preu són setze kilobytes de memòria, probablement el millor intercanvi de memòria per temps que existeix. I ara tens també l'explicació completa de dues coses que vas acceptar abans: per què FileReader.read() és tan lent, i per què un fitxer acabat d'escriure té zero bytes.
Saps construir-les per decoració sobre FileReader/FileWriter o sobre un pont, i saps triar la mida: 8192 tret que hagis mesurat, amb l'advertiment que cent fluxos amb memòries intermèdies d'un megabyte són cent megabytes de heap, i que embolcallar una cosa que ja en té no aporta res.
Domines readLine() i els seus quatre punts de contracte: retorna la línia sense el terminador, reconeix les tres convencions de final de línia —per això un fitxer de Windows es llegeix sense problema a Linux—, retorna null al final i no "", i bloqueja fins a tenir una línia completa. Escrius el bucle canònic sense dubtar, amb els parèntesis obligatoris i != null com a única condició correcta, i reconeixes a l'instant els tres errors que el substitueixen: l'isEmpty() que s'atura a la primera línia buida, el doble readLine() que processa una de cada dues, i el ready() que funciona en desenvolupament i falla en producció amb fitxers grans o fonts lentes. Saps també que readLine() carrega la línia sencera en memòria, i què significa això amb un fitxer d'una sola línia gegant.
Coneixes mark/reset per inspeccionar el principi d'un flux sense perdre'l, amb els seus límits reals. I coneixes BufferedWriter amb el seu newLine() portable i —l'argument que més pesa— els seus mètodes sí que declaren IOException, a diferència de PrintWriter, que se les empassa. D'aquí la regla: PrintWriter per a informes, BufferedWriter per a dades la fallada de les quals cal detectar. Saps què aporta exactament cada capa del trio PrintWriter + BufferedWriter + FileWriter, inclosa l'observació honesta que la capa intermèdia aporta menys del que es creu, perquè PrintWriter ja té la seva pròpia memòria intermèdia.
Saps que Files.newBufferedReader i newBufferedWriter construeixen exactament aquests objectes de forma més curta, obligant al charset i amb excepcions més específiques, i que aquesta és la forma recomanada en codi nou. És NIO.2, i és la lliçó següent.
I tens la capacitat que va més enllà del rendiment: el processament en flux. Un fitxer de dos gigabytes que peta amb readString() o readAllLines() es recorre amb BufferedReader fent servir cinquanta kilobytes. Saps que això obliga a dissenyar l'algorisme d'una altra manera —un sol recorregut, una línia cada vegada— i què es pot i què no es pot fer així. Coneixes a més la comparació completa entre BufferedReader i Scanner, amb el criteri per triar: Scanner per a menús interactius, BufferedReader per a fitxers i entrada massiva; i l'advertiment, un altre cop, de no tancar mai System.in.
BiblioTech importa de veritat. ImportadorCataleg recorre milers de línies amb memòria constant, valida cadascuna contra set regles, descarta les dolentes amb el seu número de línia i el seu motiu tipat en un enum, compta totes i detalla les cent primeres per no esgotar la memòria informant d'un problema, avorta la importació sencera si falla més de la meitat —perquè llavors el problema és el fitxer, no les línies—, bolca al catàleg només al final per no deixar-lo a mitges, i retorna un informe en lloc d'imprimir res, perquè una classe de servei no sap qui hi ha a l'altre costat. És el mòdul 5, el mòdul 6 i aquest, funcionant junts en una sola classe.
Fins ara, tot el que BiblioTech desa és text que tu has decidit com formatar: LLIBRE;isbn;titol;autor;any. Funciona, però té un límit evident. I si volguessis desar l'estat complet del sistema —els préstecs amb les seves referències a materials i empleats, les reserves, l'historial, les incidències imbricades— amb totes les seves relacions intactes? Escriure aquest graf d'objectes a mà, amb les seves referències creuades i els seus objectes compartits, seria una feina enorme i plena de detalls.
A la lliçó 07-05, Serialització, veuràs que Java ho sap fer sol. Coneixeràs què significa convertir un graf d'objectes en bytes conservant les referències compartides; la interfície marcadora Serializable, que reprèn les interfícies marcadores de 04-01, amb ObjectOutputStream i ObjectInputStream; què es desa i què no, amb transient i static; el serialVersionUID que ja vas declarar a les teves excepcions del mòdul 6 sense saber del tot per a què servia, i la InvalidClassException que apareix quan canvia la classe; quins canvis d'una classe són compatibles i quins trenquen els fitxers existents; i —el més important de la lliçó— els riscos de seguretat reals de deserialitzar dades que no controles, que és una de les vies d'atac més explotades de la història de Java. En acabar-la, BiblioTech desarà i restaurarà una sessió completa entre execucions, i sabràs exactament quan no has de fer servir aquesta eina.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- 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
