La lliçó anterior va acabar amb una frase que convé prendre's seriosament: llegir és segur, escriure destrueix. Si t'equivoques llegint, obtens dades incorrectes i ho notes. Si t'equivoques escrivint, el fitxer anterior ja no existeix, i pot ser que no ho notis fins que algú el necessiti.
En aquesta lliçó BiblioTech desa per primera vegada. I perquè desi bé, hi ha tres coses que cal entendre abans d'escriure una línia: el paràmetre append, que decideix entre afegir al final del fitxer i buidar-lo del tot; la memòria intermèdia, que explica per què el que has escrit pot no ser encara al disc; i l'escriptura atòmica, el patró que impedeix deixar un fitxer a mitges quan el procés falla a mig camí.
Aquest tercer punt és el mateix problema que vas resoldre a 06-05 amb la compensació al finally: mantenir el sistema en un estat coherent passi el que passi. Allà l'estat era a la memòria; aquí és al disc, i les conseqüències duren més.
Com a tota la lliçó anterior, tot recurs s'obre amb
try-with-resources(06-06). Es dona per sabut i no es torna a justificar. En escriptura importa encara més que en lectura, perquè elclose()d'un escriptor buida la memòria intermèdia al disc: si no tanques, no has escrit.
Contingut
FileWriter: crear, sobreescriure i afegir- El paràmetre
append, o com perdre un fitxer sencer - Escriure text amb
write PrintWriter: l'embolcall còmode- La memòria intermèdia i el buidatge:
flushdavant declose - Què passa si el programa acaba sense tancar
- Codificació explícita en escriure
- El separador de línia del sistema
- Permisos, fitxers de només lectura i
IOException - Escriptura atòmica: temporal i reanomenat
- Fitxers de bloqueig i sobreescriptura accidental
- BiblioTech:
ExportadorCatalegescriu de veritat - Errors Comuns i Consells
- Exercicis
FileWriter: crear, sobreescriure i afegir
FileWriter: crear, sobreescriure i afegirFileWriter és la contrapartida exacta de FileReader: escriu text en un fitxer. El seu comportament en l'obertura és el primer que cal fixar:
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class PrimeraEscriptura {
public static void main(String[] args) throws IOException {
try (FileWriter escriptor = new FileWriter("sortida.txt", StandardCharsets.UTF_8)) {
escriptor.write("Cataleg de BiblioTech\n");
escriptor.write("Nexus Software\n");
}
System.out.println("Escrit.");
}
}Què passa exactament en obrir:
| Situació prèvia | Comportament per defecte |
|---|---|
| El fitxer no existeix | Es crea, buit, i s'hi escriu |
| El fitxer existeix | Es trunca a zero bytes i s'escriu des del principi |
| El directori pare no existeix | IOException: FileWriter no crea directoris |
| No hi ha permís d'escriptura | IOException (FileNotFoundException, que l'estén) |
Fixa't en la segona fila, perquè és la que t'arruïna el dia: obrir un FileWriter sobre un fitxer existent el buida immediatament, en el moment de la construcció, abans que escriguis res. Si obres el fitxer i després llances una excepció abans d'escriure, et quedes amb un fitxer buit i sense les dades anteriors.
I fixa't també en la tercera: FileWriter no crea el directori. Si escrius a dades/cataleg.txt i no existeix dades/, obtens una IOException el missatge típic de la qual és "No such file or directory", que molts interpreten com a "no troba el fitxer" quan en realitat falta la carpeta. Crear directoris és tasca de mkdirs() a l'API antiga o de Files.createDirectories() a NIO.2 (07-06).
Els constructors disponibles:
// Sobreescriu. Charset de la plataforma: NO FER SERVIR (07-01, apartat 10)
new FileWriter("sortida.txt");
// Sobreescriu, charset explicit (Java 11+). AQUESTA es la forma correcta
new FileWriter("sortida.txt", StandardCharsets.UTF_8);
// Afegeix al final, charset de la plataforma
new FileWriter("sortida.txt", true);
// Afegeix al final, charset explicit (Java 11+). L'altra forma correcta
new FileWriter("sortida.txt", StandardCharsets.UTF_8, true);Compte amb l'ambigüitat del segon paràmetre.
new FileWriter(cami, true)significa "afegir".new FileWriter(cami, StandardCharsets.UTF_8)significa "sobreescriure amb UTF-8". S'assemblen molt i signifiquen coses diferents. Quan vulguis les dues coses, fes servir la versió de tres paràmetres, i fixa't que elbooleanva l'últim.
- El paràmetre
append, o com perdre un fitxer sencer
append, o com perdre un fitxer sencerAquest és l'apartat que cal llegir dues vegades. La taula és curta i les conseqüències són llargues:
| Constructor | Fitxer nou | Fitxer existent amb dades |
|---|---|---|
new FileWriter(cami, UTF_8) |
Es crea | Es buida. Les dades anteriors es perden |
new FileWriter(cami, UTF_8, false) |
Es crea | Es buida. Idèntic a l'anterior |
new FileWriter(cami, UTF_8, true) |
Es crea | Es conserva i s'escriu al final |
El cas real, i passa constantment:
// BiblioTech registra cada prestec en un fitxer d'auditoria.
// El fitxer porta tres anys acumulant operacions.
public void registrarOperacio(String linia) throws IOException {
// BUG: falta el 'true'. Cada crida ESBORRA tot l'historic
// i deixa nomes l'ultima linia.
try (FileWriter escriptor = new FileWriter("dades/auditoria.txt", StandardCharsets.UTF_8)) {
escriptor.write(linia + System.lineSeparator());
}
}Aquest codi funciona: no llança excepcions, no dona avisos, el fitxer existeix i té contingut. Només que té una línia en lloc de tres-centes mil. I com que cada execució el torna a deixar amb una línia, la fallada és perfectament estable i silenciosa. Es descobreix el dia que algú demana l'històric.
La versió correcta:
public void registrarOperacio(String linia) throws IOException {
// El 'true' final: AFEGIR, no sobreescriure
try (FileWriter escriptor =
new FileWriter("dades/auditoria.txt", StandardCharsets.UTF_8, true)) {
escriptor.write(linia + System.lineSeparator());
}
}Tres defenses professionals contra aquest error:
- Anomena la intenció. No deixis el
truesolt a la crida:
private static final boolean AFEGIR = true;
private static final boolean SOBREESCRIURE = false;
new FileWriter(cami, StandardCharsets.UTF_8, AFEGIR); // es llegeix sol-
Fes servir les opcions explícites de NIO.2 quan arribis a 07-06.
StandardOpenOption.APPENDiStandardOpenOption.TRUNCATE_EXISTINGdiuen literalment el que fan, i no hi ha manera de confondre-les amb un charset. -
Escriu sempre a un fitxer temporal i reanomena quan estiguis regenerant un fitxer complet. És l'apartat 10, i elimina la categoria sencera de problemes.
I un advertiment que va més enllà d'aquest paràmetre:
Obrir un
FileWriterper "comprovar alguna cosa" ja destrueix el fitxer. No existeix un mode "obrir només si puc". Si necessites saber si el fitxer existeix abans de decidir, comprova-ho abans d'obrir, i tot i així recorda l'avís de TOCTOU de 06-07: entre la comprovació i l'obertura, el món pot canviar.
- Escriure text amb
write
writeWriter —la superclasse de FileWriter— ofereix diverses sobrecàrregues de write:
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FormesDeEscriure {
public static void main(String[] args) throws IOException {
try (FileWriter w = new FileWriter("demo.txt", StandardCharsets.UTF_8)) {
w.write("Text complet"); // String sencer
w.write('\n'); // un sol caracter (int)
w.write("Nomes una part", 6, 3); // subcadena: des de 6, 3 caracters -> "una"
w.write('\n');
char[] buffer = { 'B', 'i', 'b', 'l', 'i', 'o' };
w.write(buffer); // array complet
w.write(buffer, 0, 3); // part de l'array -> "Bib"
w.append("Encadenable") // append retorna el Writer
.append(' ')
.append("i comode");
}
}
}| Mètode | Escriu |
|---|---|
write(String) |
La cadena completa |
write(String, int inici, int longitud) |
Una subcadena, sense crear objectes intermedis |
write(int) |
Un sol caràcter, donat pel seu codi |
write(char[]) |
L'array complet |
write(char[], int inici, int longitud) |
Part de l'array |
append(CharSequence) |
Igual que write(String), però retorna el Writer, encadenable |
Un detall que descol·loca: write(int) escriu un caràcter, no el nombre. w.write(65) escriu la lletra A, no el text 65. Si vols escriure el nombre, converteix-lo: w.write(String.valueOf(65)). És la mateixa asimetria que ja vas veure a read() de 07-01, i pel mateix motiu: la unitat de l'API és el caràcter, i l'int hi és perquè hi càpiga el -1.
I la mancança pràctica més evident: FileWriter no té println. No hi ha cap mètode que afegeixi el salt de línia per tu, ni cap que formati. Has d'escriure el separador a mà cada vegada. Això ho resol PrintWriter.
PrintWriter: l'embolcall còmode
PrintWriter: l'embolcall còmodePrintWriter embolcalla un altre Writer i li afegeix tota la comoditat de System.out: els mateixos print, println i printf que fas servir des del mòdul 1.
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public class InformeAmbPrintWriter {
public static void main(String[] args) throws IOException {
try (PrintWriter sortida = new PrintWriter(
new FileWriter("informe.txt", StandardCharsets.UTF_8))) {
sortida.println("INFORME DE MULTES - BiblioTech");
sortida.println("==============================");
sortida.println();
// printf amb la mateixa sintaxi del modul 1
sortida.printf("%-25s %-15s %8s%n", "MATERIAL", "EMPLEAT", "MULTA");
sortida.printf("%-25s %-15s %8.2f%n", "Java Eficac", "Marta Ruiz", 3.75);
sortida.printf("%-25s %-15s %8.2f%n", "Patrons de Disseny", "Diego Alonso", 0.00);
sortida.printf("%-25s %-15s %8.2f%n", "Refactoritzacio", "Nuria Vidal", 20.00);
// Locale explicit: a Catalunya el separador decimal es la coma.
// Per a un fitxer que un altre programa hagi de llegir, fixa Locale.ROOT.
sortida.printf(Locale.ROOT, "%nTOTAL: %.2f EUR%n", 23.75);
}
}
}Resultat:
INFORME DE MULTES - BiblioTech
==============================
MATERIAL EMPLEAT MULTA
Java Eficac Marta Ruiz 3,75
Patrons de Disseny Diego Alonso 0,00
Refactoritzacio Nuria Vidal 20,00
TOTAL: 23.75 EURFixa't en la diferència entre les línies del cos (amb coma decimal, perquè fan servir el Locale de la màquina) i la del total (amb punt, perquè fixa Locale.ROOT). En un fitxer destinat que el llegeixi un altre programa, fixa sempre el Locale; en un destinat que el llegeixi una persona a Catalunya, la coma és el correcte. Aquest mateix conflicte reapareixerà amb els CSV a 07-07 i és més important del que sembla.
El que aporta PrintWriter sobre FileWriter:
| Mètode | Què fa |
|---|---|
println(x) |
Escriu i afegeix el separador de línia del sistema |
print(x) |
Escriu sense salt. Accepta qualsevol tipus, inclosos objectes (fa servir toString()) |
printf(fmt, args...) |
Formata com System.out.printf |
format(fmt, args...) |
Idèntic a printf. Existeix per simetria amb String.format |
write(String) |
Heretat de Writer |
I ara el parany gran de PrintWriter, que cal conèixer sí o sí:
PrintWriters'empassa les excepcions. Els seus mètodesprintlniprintfno declarenIOException. Si el disc s'omple o el dispositiu falla, no ho sabràs: l'error es desa en un marcador intern en lloc de llançar-se.
L'única manera d'assabentar-te'n és preguntar:
try (PrintWriter sortida = new PrintWriter(
new FileWriter("informe.txt", StandardCharsets.UTF_8))) {
sortida.println("linia 1");
sortida.println("linia 2");
// COMPROVACIO OBLIGATORIA en codi serios.
// checkError() buida la memoria intermedia i retorna true si HI HA HAGUT alguna
// fallada en qualsevol moment des que es va obrir.
if (sortida.checkError()) {
throw new IOException("Fallada en escriure l'informe (detectada per checkError)");
}
}Aquest disseny ve del fet que PrintWriter va néixer per a System.out, on una fallada d'escriptura en consola no havia d'obligar a posar try/catch a cada println. Per a un fitxer, aquest compromís és perillós.
Regla pràctica:
- Per a sortida per consola i bolcats de depuració:
PrintWritersense més, amb la seva comoditat. - Per a fitxers el contingut dels quals importa:
PrintWriterambcheckError()abans de tancar, o directamentBufferedWriter(07-04), els mètodes del qual sí que declarenIOException.
Un avís més sobre els constructors de PrintWriter:
// Aquests DOS obren el fitxer pel seu compte, amb el charset de la plataforma
// a les versions antigues. Son comodes i traidors.
new PrintWriter("sortida.txt");
new PrintWriter(new File("sortida.txt"));
// Amb charset explicit (Java 10+): correcte
new PrintWriter("sortida.txt", StandardCharsets.UTF_8);
// Embolcallant un Writer que tu controles: la forma mes clara i flexible
new PrintWriter(new FileWriter("sortida.txt", StandardCharsets.UTF_8, true));L'última és la recomanada: tu decideixes el charset i el mode append, i PrintWriter només aporta el formatatge. És composició, i a 07-03 veuràs que aquest és el principi que organitza tota l'API de fluxos.
- La memòria intermèdia i el buidatge:
flush davant de close
flush davant de closeAquí hi ha el concepte que separa qui escriu fitxers de qui escriu fitxers bé.
Quan crides write, les dades no van al disc. S'acumulen en una memòria intermèdia a la RAM, i només s'envien al sistema operatiu quan aquesta s'omple o quan algú ho demana explícitament. I hi ha més d'un nivell d'acumulació:
flowchart TD
A["sortida.println(...)"] --> B["Memoria intermedia de PrintWriter<br/>o BufferedWriter"]
B -->|"flush o memoria plena"| C["Memoria intermedia interna del FileWriter"]
C -->|"crida al sistema write"| D["Cache de pagina del<br/>sistema operatiu"]
D -->|"fsync o el mateix SO"| E["Disc fisic"]
style B fill:#e3f2fd
style C fill:#e3f2fd
style D fill:#fff3e0
style E fill:#f3e5f5
I aquí hi ha la clau que gairebé ningú no té clara:
| Operació | Garanteix |
|---|---|
write(...) |
Que les dades són a la memòria intermèdia de l'aplicació. Res més |
flush() |
Que les dades han arribat al sistema operatiu. Ja no depenen del teu procés |
close() |
Un flush() més l'alliberament del descriptor de fitxer |
FileDescriptor.sync() |
Que les dades són físicament al disc. L'única cosa que sobreviu a un tall de corrent |
És a dir: close() protegeix davant que el teu programa acabi; només sync() protegeix davant que se'n vagi la llum. Per a la immensa majoria de les aplicacions, close() és suficient i sync() és un cost innecessari. Per a un sistema on perdre l'última operació sigui inacceptable —un cobrament, un assentament comptable—, cal arribar fins al disc.
Demostració de la diferència entre flush i no fer res:
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DemoBuffer {
public static void main(String[] args) throws Exception {
FileWriter w = new FileWriter("demo-buffer.txt", StandardCharsets.UTF_8);
w.write("Primera linia\n");
System.out.println("Despres de write, mida al disc: " + mida()); // 0
w.flush();
System.out.println("Despres de flush, mida al disc: " + mida()); // 14
w.write("Segona linia\n");
System.out.println("Despres del 2n write, mida: " + mida()); // 14
w.close();
System.out.println("Despres de close, mida al disc: " + mida()); // 27
}
private static long mida() {
return new java.io.File("demo-buffer.txt").length();
}
}Sortida:
Despres de write, mida al disc: 0
Despres de flush, mida al disc: 14
Despres del 2n write, mida: 14
Despres de close, mida al disc: 27El fitxer té 0 bytes després del primer write. Les dades existeixen, però són a la memòria del procés. Si en aquell instant algú mira el fitxer des de fora, el veu buit.
Conseqüència pràctica molt comuna: si estàs escrivint un fitxer de registre i el consultes amb tail -f mentre el programa corre, no veuràs res durant molta estona, perquè la memòria intermèdia no s'ha omplert. Això no és una fallada del teu codi; és la memòria intermèdia fent la seva feina. Per a registres que cal poder seguir en viu, es fa flush() després de cada línia, a costa del rendiment —i això és exactament el que fa el FileHandler de java.util.logging que vas configurar a 06-07, i el motiu que el registre tingui un cost apreciable—.
Quan cridar flush() explícitament:
- Quan un altre procés o persona necessita veure les dades ja, sense esperar el tancament.
- Quan mantindràs el fitxer obert molt de temps i vols acotar quant es perdria en una caiguda.
- Abans d'una operació llarga o arriscada, per deixar al disc el que s'ha escrit fins aquell punt.
- Mai just abans de
close(): és redundant,close()ja ho fa.
- Què passa si el programa acaba sense tancar
Aquesta demostració convé executar-la, perquè el resultat sorprèn:
import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
public class SenseTancar {
public static void main(String[] args) throws Exception {
// MALAMENT A PROPOSIT: sense try-with-resources i sense close
FileWriter w = new FileWriter("sense-tancar.txt", StandardCharsets.UTF_8);
w.write("Aquesta linia pot no arribar mai al disc.\n");
System.out.println("Acabant sense tancar...");
// main acaba aqui. No hi ha close(). No hi ha flush().
}
}Resultat habitual: el fitxer existeix i és buit. Les dades es van quedar a la memòria intermèdia del procés, i en acabar el procés aquesta memòria desapareix amb ell.
Preguntes que sorgeixen sempre:
- No el tanca el recol·lector de brossa? El
finalize()d'algunes classes d'E/S feia una cosa semblant, però està obsolet des de Java 9 i eliminat en versions recents. Mai no va ser una garantia: el recol·lector no promet executar-se abans que el procés acabi. No hi comptis mai. - I si el procés el mata el sistema? Amb
kill -9o un tall de corrent, es perd tot el que no hagi passat al sistema operatiu. Ni tan sols els shutdown hooks de 06-05 s'executen ambkill -9. - I si acaba amb excepció? Sense
try-with-resources, es perd igual. Ambtry-with-resources, el tancament està garantit i les dades arriben.
La versió correcta, i l'única que s'escriu en codi real:
try (FileWriter w = new FileWriter("ben-tancat.txt", StandardCharsets.UTF_8)) {
w.write("Aquesta linia SI que arriba al disc.\n");
} // close() garantit: amb exit, amb excepcio i amb return (06-06)Això dona un pes extra al
try-with-resourcesen escriptura. En lectura, no tancar és una fuita de recursos: molest però no destructiu. En escriptura, no tancar és perdre dades. El fitxer queda buit o truncat, i sovint ningú no se n'assabenta fins molt després.
- Codificació explícita en escriure
Tot el de 07-01 sobre codificació s'aplica igual, amb un matís que empitjora les coses:
Una fallada de codificació en llegir es veu i s'arregla. En escriure, es grava. Si escrius un catàleg amb el charset equivocat, el fitxer queda malament al disc. Pots continuar llegint-lo bé mentre facis servir el mateix charset equivocat, i el problema només apareix quan una altra eina —un editor, un full de càlcul, un altre sistema— intenta llegir-lo amb UTF-8. Per llavors el fitxer porta mesos acumulant dades corruptes.
I hi ha un cas pitjor encara: escriure un caràcter que el charset de destinació no pot representar:
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class CaracterNoRepresentable {
public static void main(String[] args) throws IOException {
// US-ASCII no te 'ç' ni '€'
try (FileWriter w = new FileWriter("ascii.txt", StandardCharsets.US_ASCII)) {
w.write("Els Patrons de Disseny costen 45 €");
}
// NO llanca excepcio. Substitueix el que no es representable per '?'.
// Al fitxer queda: "Els Patrons de Disseny costen 45 ?"
}
}Silenci total i dades destruïdes. El comportament per defecte del codificador és CodingErrorAction.REPLACE. Si vols que falli en lloc de destruir, cal baixar al nivell de CharsetEncoder, que és API de java.nio.charset i queda fora de l'abast d'aquesta lliçó; l'important és que sàpigues que el silenci és el comportament per defecte.
La regla és la de 07-01, repetida perquè mereix repetir-se:
// SEMPRE, a cada obertura d'escriptura de text
new FileWriter(cami, StandardCharsets.UTF_8);
new FileWriter(cami, StandardCharsets.UTF_8, AFEGIR);
new PrintWriter(new FileWriter(cami, StandardCharsets.UTF_8));I una simetria que cal respectar sempre: qui escriu i qui llegeix el mateix fitxer han de fer servir el mateix charset. Si ExportadorCataleg escriu en UTF-8, CarregadorCataleg ha de llegir en UTF-8. El millor és que aquest charset estigui declarat una sola vegada en una constant compartida:
package com.nexussoftware.bibliotech.infraestructura;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/** Convencions de format de tots els fitxers de BiblioTech. */
public final class FormatFitxers {
/** Charset unic de tots els fitxers de text del sistema. */
public static final Charset CHARSET = StandardCharsets.UTF_8;
/** Separador de camps dels fitxers tabulats. */
public static final String SEPARADOR_CAMPS = ";";
/** Prefix de les linies de comentari. */
public static final String COMENTARI = "#";
private FormatFitxers() { }
}Un sol lloc per canviar, i cap possibilitat que el lector i l'escriptor discrepin.
- El separador de línia del sistema
Els sistemes operatius no es van posar d'acord en com acabar una línia:
| Sistema | Bytes | Escapada en Java | Nom |
|---|---|---|---|
| Linux, macOS modern | 0x0A |
\n |
LF |
| Windows | 0x0D 0x0A |
\r\n |
CRLF |
| macOS clàssic (fins al 2001) | 0x0D |
\r |
CR |
Java exposa el del sistema actual:
Quan importa? Depèn de qui hagi de llegir el fitxer:
| Consumidor del fitxer | Li molesta \n a Windows? |
|---|---|
El teu propi programa amb BufferedReader.readLine() |
No: accepta les tres formes |
| Un editor modern (VS Code, Notepad++, IntelliJ) | No |
| El Bloc de notes de Windows antic | Sí: ho mostrava tot en una línia |
| Eines de línia d'ordres de Windows | De vegades |
| Un altre sistema que esperi el format natiu | Sí |
A la pràctica:
PrintWriter.println()ja fa servir el separador del sistema. No has de fer res.BufferedWriter.newLine()també. És el mètode correcte (07-04).- Escriure
"\n"a mà en unwriteprodueix sempre LF, sigui quin sigui el sistema.
Ara bé, hi ha un argument a favor d'escriure \n sempre, i és seriós: la reproductibilitat. Si un fitxer generat a Windows porta CRLF i el mateix fitxer generat a Linux porta LF, un control de versions els veurà com a completament diferents encara que el contingut sigui idèntic. Per a fitxers de dades que es versionen o es comparen, molts equips fixen LF a propòsit.
La decisió, resumida:
| Tipus de fitxer | Separador recomanat |
|---|---|
| Informe perquè el llegeixi una persona al seu sistema | System.lineSeparator() (o println) |
| Fitxer de dades que es compara o es versiona | "\n" fix, i documentar-ho |
| Fitxer que un altre sistema defineix | El que aquell sistema exigeixi |
| Protocol de xarxa | El que digui el protocol. HTTP exigeix CRLF (mòdul 9) |
A BiblioTech farem servir System.lineSeparator() per als informes i "\n" fix per als fitxers de dades. La constant va, com el charset, a FormatFitxers.
- Permisos, fitxers de només lectura i
IOException
IOExceptionEscriure pot fallar per causes que no depenen del teu codi:
| Causa | Excepció | Missatge típic |
|---|---|---|
| No existeix el directori pare | FileNotFoundException |
No such file or directory |
| Sense permís d'escriptura | FileNotFoundException |
Permission denied |
| Fitxer marcat de només lectura | FileNotFoundException |
Access is denied (Windows) |
| El camí és un directori | FileNotFoundException |
Is a directory |
| Disc ple | IOException |
No space left on device |
| Unitat de xarxa desconnectada | IOException |
Varia |
| Quota d'usuari superada | IOException |
Disk quota exceeded |
Observa un detall important: les fallades d'obertura arriben com a FileNotFoundException —igual que en lectura, amb el nom igual d'enganyós— i les fallades durant l'escriptura arriben com a IOException genèrica. I el més traïdor de tots és el disc ple, perquè no passa en obrir sinó a mig fitxer: quan salta, ja tens mig fitxer escrit.
Aquest és precisament l'argument definitiu a favor de l'apartat següent.
Gestió per capes, amb el criteri de 06-07:
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class EscripturaAmbGestio {
private static final Logger LOG = Logger.getLogger(EscripturaAmbGestio.class.getName());
public void exportar(String cami, String contingut) throws CatalegNoAccessibleException {
try (FileWriter w = new FileWriter(cami, StandardCharsets.UTF_8)) {
w.write(contingut);
} catch (FileNotFoundException e) {
// Problema d'UBICACIO o PERMISOS: l'usuari pot corregir-ho
LOG.log(Level.WARNING, "No es pot escriure a "
+ new java.io.File(cami).getAbsolutePath(), e);
throw new CatalegNoAccessibleException(cami, e); // 06-04, amb la causa
} catch (IOException e) {
// Problema DURANT l'escriptura: disc ple, xarxa caiguda...
LOG.log(Level.SEVERE, "Fallada d'E/S escrivint " + cami, e);
throw new CatalegNoAccessibleException(cami, e);
}
}
}Fixa't que es reutilitza CatalegNoAccessibleException, l'excepció comprovada que vas declarar a 06-04 precisament per a això: una fallada d'infraestructura de la qual l'aplicació té una alternativa raonable, i que per tant mereix que el compilador obligui a decidir què fer. La causa original viatja a dins, segons la regla d'encadenament de 06-03.
- Escriptura atòmica: temporal i reanomenat
Aquest és el patró professional de la lliçó. El problema és aquest:
// PERILLOS: regenera el cataleg complet sobreescrivint el fitxer bo
try (PrintWriter w = new PrintWriter(
new FileWriter("dades/cataleg.txt", StandardCharsets.UTF_8))) {
for (Material m : cataleg.llistar()) { // 10 000 materials
w.println(serialitzar(m));
}
// Si falla al material 6 000 (disc ple, excepcio a serialitzar,
// el proces mor), el fitxer queda amb 6 000 linies i el cataleg
// ORIGINAL JA NO EXISTEIX: es va truncar en obrir.
}El fitxer bo es va destruir en l'instant d'obrir el FileWriter, i el nou no va arribar a completar-se. Has perdut el catàleg. I el pitjor: el fitxer resultant sembla vàlid —té format correcte, es llegeix sense errors— només que li falten 4 000 materials. Una fallada silenciosa, que és la pitjor classe.
La solució és el patró escriure-i-reanomenar:
flowchart LR
A["cataleg.txt<br/>versio bona"] --> B{"Escriure-ho tot a<br/>cataleg.txt.tmp"}
B -->|"exit"| C["Reanomenar tmp<br/>sobre cataleg.txt"]
B -->|"fallada"| D["Esborrar el tmp"]
C --> E["cataleg.txt<br/>versio nova completa"]
D --> F["cataleg.txt<br/>versio bona INTACTA"]
style A fill:#e8f5e9
style E fill:#e8f5e9
style F fill:#e8f5e9
style D fill:#ffebee
Per què funciona: el reanomenat dins del mateix sistema de fitxers és una operació atòmica als ulls de qui llegeix. No existeix un instant en què el fitxer estigui a mitges. Un lector concurrent veu la versió antiga completa o la nova completa, mai una barreja. I si alguna cosa falla abans del reanomenat, el fitxer bo ni s'ha tocat.
La implementació:
package com.nexussoftware.bibliotech.infraestructura;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Escriptura atomica de fitxers de text.
*
* Escriu en un fitxer temporal i, NOMES si tot ha anat be, el reanomena
* sobre el definitiu. Garanteix que el fitxer desti no queda mai a mitges.
*
* Es l'equivalent al disc de la compensacio al finally de 06-05: si
* l'operacio no es completa, l'estat anterior es conserva intacte.
*/
public final class EscripturaAtomica {
private static final Logger LOG = Logger.getLogger(EscripturaAtomica.class.getName());
private static final String SUFIX_TEMPORAL = ".tmp";
private EscripturaAtomica() { }
/**
* Contracte del que s'escriura. Interficie funcional (04-06):
* permet passar la logica d'escriptura com a lambda.
*
* No es java.util.function.Consumer perque necessitem que pugui
* declarar IOException, i Consumer.accept() no la declara.
*/
@FunctionalInterface
public interface Contingut {
void escriureA(PrintWriter sortida) throws IOException;
}
/**
* Escriu de forma atomica.
*
* @param desti fitxer final
* @param contingut que cal escriure
* @throws IOException si falla l'escriptura o el reanomenat
*/
public static void escriure(File desti, Contingut contingut) throws IOException {
File temporal = new File(desti.getAbsolutePath() + SUFIX_TEMPORAL);
// 1. Assegurar que existeix el directori pare (FileWriter no el crea)
File pare = desti.getAbsoluteFile().getParentFile();
if (pare != null && !pare.exists() && !pare.mkdirs()) {
throw new IOException("No s'ha pogut crear el directori " + pare.getAbsolutePath());
}
boolean completat = false;
try {
// 2. Escriure-ho TOT al temporal
try (PrintWriter sortida = new PrintWriter(
new FileWriter(temporal, StandardCharsets.UTF_8))) {
contingut.escriureA(sortida);
// PrintWriter s'empassa les IOException: cal preguntar-li
if (sortida.checkError()) {
throw new IOException("Fallada en escriure a "
+ temporal.getAbsolutePath());
}
} // close() garantit: aqui el temporal esta tancat i complet
// 3. Reanomenar NOMES si arribem fins aqui
if (!reanomenar(temporal, desti)) {
throw new IOException("No s'ha pogut reanomenar "
+ temporal.getName() + " sobre " + desti.getName());
}
completat = true;
LOG.fine(() -> "Escriptura atomica completada: " + desti.getAbsolutePath());
} finally {
// 4. COMPENSACIO (06-05): si no s'ha completat, no deixar brossa.
// El desti original continua intacte perque no es va obrir mai.
if (!completat && temporal.exists() && !temporal.delete()) {
LOG.warning(() -> "Ha quedat un fitxer temporal sense esborrar: "
+ temporal.getAbsolutePath());
}
}
}
/**
* Reanomena el temporal sobre el desti.
*
* File.renameTo retorna un boolean mut (07-01) i a Windows falla si
* el desti existeix. D'aqui l'esborrat previ. A 07-06 aixo es resol
* amb Files.move i ATOMIC_MOVE, que es la forma correcta.
*/
private static boolean reanomenar(File temporal, File desti) {
if (desti.exists() && !desti.delete()) {
return false;
}
return temporal.renameTo(desti);
}
}Es fa servir així, amb una lambda:
EscripturaAtomica.escriure(new File("dades/cataleg.txt"), sortida -> {
sortida.println("# Cataleg de BiblioTech");
for (Material m : cataleg.llistar()) {
sortida.printf("%s;%s;%s;%b%n",
m.getTipus(), m.getReferencia(), m.getTitol(), m.estaDisponible());
}
});Tres observacions sobre aquesta implementació:
- El
finallycompensa igual que a 06-05. Si no s'ha completat, s'esborra el temporal. I el destí no necessita compensació perquè no es va obrir mai: aquesta és tota la gràcia del patró. checkError()és obligatori ambPrintWriter. Sense ell, un disc ple passaria desapercebut i reanomenaries un fitxer truncat sobre el bo, que és just el que volíem evitar.renameToés defectuós —booleanmut, comportament diferent a Windows, finestra entre eldeletei elrenameTo—. A 07-06 se substitueix perFiles.move(origen, desti, StandardCopyOption.ATOMIC_MOVE), que sí que és atòmica de veritat i llança excepcions informatives. La versió d'aquí és la que calia escriure abans de Java 7, i serveix per entendre exactament què garanteix NIO.2.
Quan fer servir escriptura atòmica i quan no:
| Situació | Atòmica? |
|---|---|
| Regenerar un fitxer complet (catàleg, exportació, configuració) | Sí, sempre |
| Afegir una línia a un registre d'auditoria | No: s'obre en mode append i es tanca |
| Fitxer temporal de treball que s'esborra després | No cal |
| Qualsevol fitxer que un altre procés pugui estar llegint | Sí, imprescindible |
- Fitxers de bloqueig i sobreescriptura accidental
Dos perills més, breus però reals.
Dos processos escrivint el mateix fitxer. Si dues instàncies de BiblioTech exporten el catàleg alhora, el resultat és impredictible: línies entremesclades, fitxer truncat, o una de les dues escriptures perduda. Ni el sistema operatiu ni Java ho impedeixen per defecte.
La solució tradicional és un fitxer de bloqueig: un fitxer la sola existència del qual significa "ocupat".
package com.nexussoftware.bibliotech.infraestructura;
import java.io.File;
import java.io.IOException;
/**
* Bloqueig entre processos basat en l'existencia d'un fitxer.
*
* AutoCloseable (06-06): el bloqueig s'allibera en sortir del try, se surti
* com se surti. Sense aquesta garantia, un bloqueig orfe deixa el sistema
* inutilitzable fins que algu esborri el fitxer a ma.
*/
public class BloqueigFitxer implements AutoCloseable {
private final File bloqueig;
private boolean adquirit = false;
public BloqueigFitxer(String camiProtegit) throws IOException {
this.bloqueig = new File(camiProtegit + ".lock");
// createNewFile() es ATOMIC: comprova i crea en una sola operacio.
// Per aixo serveix com a bloqueig i un exists() + create() NO serviria:
// entre les dues crides hi cap l'altre proces (TOCTOU, 06-07).
if (!bloqueig.createNewFile()) {
throw new IOException("El fitxer '" + camiProtegit
+ "' l'esta fent servir un altre proces"
+ " (existeix " + bloqueig.getName() + ")");
}
adquirit = true;
bloqueig.deleteOnExit(); // xarxa de seguretat davant d'una sortida brusca
}
@Override
public void close() {
if (!adquirit) {
return; // idempotent (06-06)
}
adquirit = false;
if (!bloqueig.delete()) {
System.err.println("Avis: no s'ha pogut alliberar " + bloqueig.getAbsolutePath());
}
}
}Ús:
try (BloqueigFitxer lock = new BloqueigFitxer("dades/cataleg.txt")) {
EscripturaAtomica.escriure(new File("dades/cataleg.txt"), sortida -> { /* ... */ });
} // el bloqueig s'allibera aqui, passi el que passiLimitacions honestes d'aquest mecanisme, que cal conèixer: si el procés mor de forma brusca, el .lock queda orfe i bloqueja tots els altres. Els sistemes seriosos hi desen a dins l'identificador del procés i la seva hora d'inici per poder detectar bloqueigs caducats. Java ofereix a més FileLock a través de FileChannel, que sí que és un bloqueig real del sistema operatiu. I tan bon punt entrin fils en joc, el problema canvia de naturalesa: això és el mòdul 8.
Sobreescriptura accidental. Abans de regenerar un fitxer important, desa'n una còpia:
/** Conserva la versio anterior abans de sobreescriure. */
private static void ferCopiaSeguretat(File fitxer) {
if (!fitxer.exists()) {
return;
}
File copia = new File(fitxer.getAbsolutePath() + ".bak");
if (copia.exists()) {
copia.delete();
}
if (!fitxer.renameTo(copia)) {
LOG.warning("No s'ha pogut fer copia de seguretat de " + fitxer.getName());
}
}A 07-06 això es converteix en una còpia de seguretat rotativa amb NIO.2: .bak.1, .bak.2, .bak.3, conservant les tres últimes versions.
- BiblioTech:
ExportadorCataleg escriu de veritat
ExportadorCataleg escriu de veritatHora de saldar el deute. A 06-06 vas declarar ExportadorCataleg com a Closeable i vas explicar per què el seu close() sí que ha de propagar IOException —perquè en tancar es buida la memòria intermèdia, i si això falla les dades no estan escrites—. Però la seva escriptura era un esbós. Ara s'escriu sencera, amb tot el d'aquesta lliçó.
package com.nexussoftware.bibliotech.servei;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
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.Prestec;
import com.nexussoftware.bibliotech.infraestructura.EscripturaAtomica;
import com.nexussoftware.bibliotech.infraestructura.FormatFitxers;
/**
* Exporta el cataleg i el registre de prestecs de BiblioTech a fitxers
* de text, de forma ATOMICA: el fitxer desti no queda mai a mitges.
*
* Canvi de disseny respecte a 06-06: aquesta classe ja NO es Closeable.
* No mante cap fitxer obert entre crides; cada exportacio obre, escriu i
* tanca dins d'EscripturaAtomica. Un objecte que no posseeix recursos
* vius no ha de ser tancable: seria una promesa buida.
*
* El format es el que llegeix CarregadorCataleg (07-01). Tots dos comparteixen
* les constants de FormatFitxers perque no puguin discrepar.
*/
public class ExportadorCataleg {
private static final Logger LOG = Logger.getLogger(ExportadorCataleg.class.getName());
private static final String CAPCALERA_CATALEG =
FormatFitxers.COMENTARI + " Cataleg de BiblioTech - Nexus Software";
private static final String CAMPS_CATALEG =
FormatFitxers.COMENTARI + " tipus;referencia;titol;autor;any";
private final File directori;
public ExportadorCataleg(String directoriDades) {
this.directori = new File(
Objects.requireNonNull(directoriDades, "El directori no pot ser nul"));
}
/**
* Exporta el cataleg complet.
*
* @return nombre de materials escrits
* @throws IOException si no s'ha pogut completar l'escriptura
*/
public int exportarCataleg(Cataleg cataleg) throws IOException {
Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
List<Material> materials = cataleg.llistar();
File desti = new File(directori, "cataleg.txt");
long inici = System.nanoTime();
EscripturaAtomica.escriure(desti, sortida -> {
sortida.println(CAPCALERA_CATALEG);
sortida.println(CAMPS_CATALEG);
for (Material m : materials) {
sortida.println(serialitzar(m));
}
});
double ms = (System.nanoTime() - inici) / 1_000_000.0;
// Logger, mai System.out per a diagnostic (06-07). Forma mandrosa.
LOG.info(() -> String.format("Cataleg exportat: %d materials a %s (%.1f ms)",
materials.size(), desti.getAbsolutePath(), ms));
return materials.size();
}
/**
* Serialitza un material a una linia.
*
* Els camps es sanegen: un ';' dins d'un titol trencaria el fitxer
* en rellegir-lo. Aqui se substitueix; a 07-07 es fara BE, entrecometant
* i escapant segons la convencio CSV.
*/
private String serialitzar(Material m) {
String sep = FormatFitxers.SEPARADOR_CAMPS;
if (m instanceof Llibre llibre) { // patro de 03-06
return String.join(sep,
"LLIBRE",
sanejar(llibre.getIsbn()),
sanejar(llibre.getTitol()),
sanejar(llibre.getAutor()),
String.valueOf(llibre.getAnyPublicacio()));
}
return String.join(sep,
m.getTipus().toUpperCase(),
sanejar(m.getReferencia()),
sanejar(m.getTitol()),
"-",
"0");
}
/** Elimina separadors i salts de linia que trencarien el format. */
private String sanejar(String valor) {
if (valor == null) {
return "";
}
return valor.replace(FormatFitxers.SEPARADOR_CAMPS, ",")
.replace("\n", " ")
.replace("\r", " ")
.trim();
}
/**
* Afegeix una linia al registre d'auditoria de prestecs.
*
* AQUEST fitxer SI que s'obre en mode AFEGIR: es un historic acumulatiu,
* no un fitxer que es regenera. No porta escriptura atomica perque no
* hi ha res per reemplacar; nomes s'agrega al final.
*/
public void registrarPrestec(Prestec prestec, String operacio) {
Objects.requireNonNull(prestec, "El prestec no pot ser nul");
File auditoria = new File(directori, "auditoria.txt");
// El 'true' final es AFEGIR. Sense ell, cada crida esborraria l'historic.
try (PrintWriter sortida = new PrintWriter(
new java.io.FileWriter(auditoria, FormatFitxers.CHARSET, true))) {
sortida.printf("%s;%s;%s;%s%n",
operacio,
prestec.getReferencia(),
prestec.getMaterial().getReferencia(),
prestec.getTitular().getIdentificador());
if (sortida.checkError()) {
throw new IOException("Fallada escrivint a " + auditoria.getAbsolutePath());
}
} catch (IOException e) {
// POLITICA: l'auditoria NO ha de tombar l'operacio de negoci.
// El prestec ja s'ha registrat en memoria i es valid.
// Es degrada: s'avisa al log i es continua (06-07).
LOG.log(Level.WARNING, "No s'ha pogut auditar l'operacio "
+ operacio + " de " + prestec.getReferencia(), e);
}
}
}Les cinc decisions de disseny que cal entendre d'aquesta classe:
- Ha deixat de ser
Closeable. A 06-06 mantenia unBufferedWriterobert durant tota la seva vida. Ara cada exportació obre i tanca dins d'EscripturaAtomica, així que no posseeix cap recurs viu. Un objecte que no té res per tancar no ha d'implementarCloseable: seria una promesa buida que fa que qui el fa servir escrigui untry-with-resourcesinútil. - El catàleg s'escriu de forma atòmica; l'auditoria, en mode
append. Són dues naturaleses diferents: un es regenera sencer, l'altre creix. L'escriptura atòmica és per regenerar; el modeappendés per créixer. Confondre'ls produeix, en un sentit, un fitxer destruït, i en l'altre, un fitxer que ho duplica tot cada vegada. - La fallada d'auditoria no avorta l'operació. El préstec és vàlid encara que no s'hagi pogut auditar. És la distinció recuperable/irrecuperable de 06-07 aplicada aquí: no auditar és una pèrdua d'informació, no una dada incorrecta. Si la política de l'empresa exigís el contrari —i en un sistema financer ho exigiria—, aquesta decisió s'invertiria i es documentaria.
- El
checkError()és als dos llocs. Sense ell,PrintWriterno diu res quan el disc s'omple. sanejar()és un pedaç declarat com a tal. Substituir el;per una coma perd informació: el títol torna malament. És un compromís conscient fins a 07-07, on l'escapada CSV ho resoldrà de veritat. Marcar els pedaços en un comentari és part de l'ofici.
I el desat en sortir, enganxat al shutdown hook de 06-05:
package com.nexussoftware.bibliotech.presentacio;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.CarregadorCataleg;
import com.nexussoftware.bibliotech.servei.ExportadorCataleg;
import com.nexussoftware.bibliotech.infraestructura.ConfiguracioLog;
public class BiblioTechApp {
private static final Logger LOG = Logger.getLogger(BiblioTechApp.class.getName());
private static final String DIRECTORI_DADES = "dades";
public static void main(String[] args) {
ConfiguracioLog.inicialitzar();
Cataleg cataleg = new CarregadorCataleg(DIRECTORI_DADES + "/cataleg.txt").carregar();
ExportadorCataleg exportador = new ExportadorCataleg(DIRECTORI_DADES);
// Desat en sortir. Cobreix la sortida normal i Ctrl+C; NO cobreix kill -9.
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
int n = exportador.exportarCataleg(cataleg);
LOG.info("Cataleg desat en sortir: " + n + " materials");
} catch (IOException e) {
LOG.log(Level.SEVERE, "NO S'HA POGUT DESAR EL CATALEG EN SORTIR", e);
}
}, "desat-final"));
System.out.println("BiblioTech - Nexus Software");
System.out.println("Materials al cataleg: " + cataleg.mida());
new MenuBiblioTech(cataleg, exportador).executar();
}
}Avís important sobre el hook. Desar només en sortir és fràgil:
kill -9, un tall de corrent o unOutOfMemoryErrorse'l salten. I el hook té un temps limitat abans que el sistema mati el procés. En un sistema real es desa també després de cada operació rellevant, o de forma periòdica. El hook és la xarxa de seguretat, no l'estratègia.
Estat de BiblioTech en tancar aquesta lliçó: el catàleg es carrega en arrencar (07-01) i es desa en sortir (07-02). Per primera vegada en set mòduls, l'aplicació recorda.
Errors Comuns i Consells
- Oblidar el
truedel modeappend. L'error més car del mòdul. Cada execució esborra l'històric i deixa una línia. No falla, no avisa: destrueix en silenci. Fes servir una constantAFEGIRamb nom. - Confondre
new FileWriter(cami, true)ambnew FileWriter(cami, UTF_8). S'assemblen i signifiquen coses oposades. Quan vulguis les dues, fes servir la versió de tres paràmetres amb elbooleanal final. - Creure que obrir el fitxer és inofensiu. Construir un
FileWritersenseappendtrunca el fitxer immediatament, abans d'escriure res. Si després falla, et quedes sense dades i sense fitxer nou. - No tancar l'escriptor. El fitxer queda buit. En lectura no tancar és una fuita; en escriptura és pèrdua de dades.
try-with-resourcessempre. - Confiar en el recol·lector de brossa per tancar.
finalize()està obsolet i eliminat, i mai no va ser una garantia. - Confondre
flush()amb "és al disc".flush()arriba al sistema operatiu; noméssync()arriba al plat. Per a gairebé tot,close()en té prou. - Ignorar que
PrintWriters'empassa lesIOException. Un disc ple passa desapercebut i reanomenes un fitxer truncat sobre el bo.checkError()abans de tancar, o fes servirBufferedWriter. - Escriure sobre el fitxer definitiu. Si falla a mitges, perds l'original i obtens un d'incomplet que sembla vàlid. Temporal i reanomenat.
- No crear el directori pare.
FileWriterno el crea; llançaIOExceptionamb un missatge que sembla dir una altra cosa. - No especificar el charset en escriure. Pitjor que en lectura: les dades queden mal gravades, i el problema apareix mesos després quan una altra eina les llegeix.
- Escriure caràcters no representables al charset destí. Se substitueixen per
?sense avisar.€içen US-ASCII desapareixen en silenci. - Que el lector i l'escriptor facin servir charsets diferents. Declara el charset una sola vegada en una constant compartida.
- Fer servir
\nquan el consumidor espera CRLF, o a l'inrevés.printlninewLine()fan servir el del sistema; per a fitxers que es versionen, fixa\ni documenta-ho. - Creure que
write(65)escriu "65". Escriu la lletraA. És un caràcter, no un nombre. - Dos processos escrivint el mateix fitxer. Resultat impredictible. Fitxer de bloqueig, i tot i així amb els seus límits.
- Consell: pregunta't sempre "aquest fitxer es regenera o creix?". Regenerar exigeix escriptura atòmica; créixer exigeix mode
append. Tota la lliçó cap en aquesta pregunta. - Consell: prova d'omplir el disc. Escriu en un dispositiu petit —una partició temporal d'uns quants MB— i comprova què fa el teu codi quan s'acaba. La majoria del codi no ho suporta, i la fallada a mig fitxer és l'escenari que l'escriptura atòmica existeix per cobrir.
- Consell: comprova-ho amb un editor extern. Obre el fitxer generat amb un altre programa i amb una altra codificació. Si només el llegeixes amb el teu propi codi, els errors de format i de charset són invisibles.
- Consell: no escriguis la contrasenya ni el DNI al fitxer. Tot el de 06-07 sobre què no registrar s'aplica igual al que es persisteix, i amb més motiu: el fitxer es queda.
Exercicis
Exercici 1: demostrador d'append
Escriu DemoAppend que demostri empíricament la diferència entre els dos modes:
- Un mètode
escriureLinia(String cami, String text, boolean afegir)que escrigui una línia amb el mode indicat i charset UTF-8. - Un mètode
mostrar(String cami)que imprimeixi el contingut i el nombre de línies. - Un
mainque: esborri el fitxer si existeix; escrigui tres línies senseappendmostrant l'estat després de cadascuna; i repeteixi l'experiment ambappend. - Un comentari final que expliqui el resultat en una frase.
Exercici 2: escriptura atòmica amb verificació
Amplia EscripturaAtomica amb un mètode escriureVerificat(File desti, Contingut contingut, int liniesEsperades) que, a més del que ja fa:
- Compti les línies realment escrites al temporal.
- Abans de reanomenar, verifiqui que el nombre coincideix amb
liniesEsperades; si no, llanciIOExceptioni no reanomeni. - Comprovi també que el temporal té mida més gran que zero.
- Registri al logger el resultat de la verificació.
Escriu un main que provoqui la fallada a propòsit —una lambda que llanci una excepció a mitges— i comprovi que el fitxer destí conserva el seu contingut anterior.
Exercici 3: registre d'auditoria rotatiu
Escriu AuditoriaBiblioTech que afegeixi línies a un fitxer d'auditoria amb rotació per mida:
registrar(String operacio, String referencia, String idEmpleat)que afegeixi una línia amb formatoperacio;referencia;idEmpleaten modeappend.- Abans d'escriure, si el fitxer supera
MIDA_MAXIMA(fes servir 1 KB per poder provar-ho), reanomena'l aauditoria-1.txt, desplaçant els anteriors fins a un màxim de 3, i comença'n un de nou. - Charset explícit i separador de línia fix
\n(és un fitxer de dades, no un informe). - Una fallada d'escriptura no s'ha de propagar: es registra al logger i es continua, seguint la política d'
ExportadorCataleg. - Un
mainque generi 200 operacions i mostri els fitxers resultants amb les seves mides.
Solucions
Solució 1
package com.nexussoftware.bibliotech.demo;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* Demostracio empirica del parametre append.
*/
public class DemoAppend {
/** Noms per al boolean: evita el 'true' solt i illegible. */
private static final boolean AFEGIR = true;
private static final boolean SOBREESCRIURE = false;
private static final String CAMI = "demo-append.txt";
public static void escriureLinia(String cami, String text, boolean afegir)
throws IOException {
try (FileWriter w = new FileWriter(cami, StandardCharsets.UTF_8, afegir)) {
w.write(text);
w.write('\n');
} // close() buida la memoria intermedia: sense ell, el fitxer quedaria buit
}
public static void mostrar(String cami) {
File f = new File(cami);
if (!f.exists()) {
System.out.println(" (el fitxer no existeix)");
return;
}
System.out.println(" mida: " + f.length() + " bytes");
try (Scanner sc = new Scanner(f, StandardCharsets.UTF_8)) {
int n = 0;
while (sc.hasNextLine()) {
System.out.println(" | " + sc.nextLine());
n++;
}
System.out.println(" linies: " + n);
} catch (IOException e) {
System.out.println(" error en llegir: " + e.getMessage());
}
}
public static void main(String[] args) throws IOException {
File f = new File(CAMI);
if (f.exists() && !f.delete()) {
System.err.println("No s'ha pogut esborrar el fitxer previ");
return;
}
System.out.println("=== EXPERIMENT 1: SENSE append (sobreescriure) ===");
for (int i = 1; i <= 3; i++) {
escriureLinia(CAMI, "Operacio numero " + i, SOBREESCRIURE);
System.out.println(" Despres d'escriure la linia " + i + ":");
mostrar(CAMI);
}
f.delete();
System.out.println();
System.out.println("=== EXPERIMENT 2: AMB append (afegir) ===");
for (int i = 1; i <= 3; i++) {
escriureLinia(CAMI, "Operacio numero " + i, AFEGIR);
System.out.println(" Despres d'escriure la linia " + i + ":");
mostrar(CAMI);
}
// CONCLUSIO: sense append, cada escriptura TRUNCA el fitxer a zero
// en el moment d'obrir-lo, aixi que nomes sobreviu l'ultima linia.
// Amb append, el punter se situa al final i el contingut creix.
}
}Sortida:
=== EXPERIMENT 1: SENSE append (sobreescriure) ===
Despres d'escriure la linia 1:
mida: 18 bytes
| Operacio numero 1
linies: 1
Despres d'escriure la linia 2:
mida: 18 bytes
| Operacio numero 2
linies: 1
Despres d'escriure la linia 3:
mida: 18 bytes
| Operacio numero 3
linies: 1
=== EXPERIMENT 2: AMB append (afegir) ===
Despres d'escriure la linia 1:
mida: 18 bytes
| Operacio numero 1
linies: 1
Despres d'escriure la linia 2:
mida: 36 bytes
| Operacio numero 1
| Operacio numero 2
linies: 2
Despres d'escriure la linia 3:
mida: 54 bytes
| Operacio numero 1
| Operacio numero 2
| Operacio numero 3
linies: 3L'experiment 1 és el bug de l'apartat 2 en directe. Fixa't que la mida es manté constant en 18 bytes: el fitxer no creix perquè cada obertura el buida. Res no falla, res no avisa, i l'històric no existeix.
Solució 2
package com.nexussoftware.bibliotech.infraestructura;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Escriptura atomica amb verificacio previa al reanomenat.
*
* Afegeix una comprovacio d'integritat ABANS de substituir el fitxer bo:
* si el resultat no es l'esperat, l'original es conserva intacte.
*/
public final class EscripturaAtomicaVerificada {
private static final Logger LOG =
Logger.getLogger(EscripturaAtomicaVerificada.class.getName());
private static final String SUFIX_TEMPORAL = ".tmp";
private EscripturaAtomicaVerificada() { }
@FunctionalInterface
public interface Contingut {
void escriureA(PrintWriter sortida) throws IOException;
}
/**
* Escriu de forma atomica verificant el resultat.
*
* @param liniesEsperades nombre de linies que HA de tenir el resultat
* @throws IOException si falla l'escriptura o la verificacio
*/
public static void escriureVerificat(File desti, Contingut contingut,
int liniesEsperades) throws IOException {
File temporal = new File(desti.getAbsolutePath() + SUFIX_TEMPORAL);
File pare = desti.getAbsoluteFile().getParentFile();
if (pare != null && !pare.exists() && !pare.mkdirs()) {
throw new IOException("No s'ha pogut crear el directori " + pare.getAbsolutePath());
}
boolean completat = false;
try {
// ---- FASE 1: escriure al temporal ----
try (PrintWriter sortida = new PrintWriter(
new FileWriter(temporal, StandardCharsets.UTF_8))) {
contingut.escriureA(sortida);
if (sortida.checkError()) {
throw new IOException("Fallada d'escriptura a " + temporal.getName());
}
}
// ---- FASE 2: verificar ABANS de tocar el desti ----
long mida = temporal.length();
if (mida == 0) {
throw new IOException("El fitxer temporal ha quedat buit; no se substitueix "
+ desti.getName());
}
int liniesReals = comptarLinies(temporal);
if (liniesReals != liniesEsperades) {
throw new IOException(String.format(
"Verificacio fallida a %s: s'esperaven %d linies i n'hi ha %d. "
+ "El fitxer original NO s'ha modificat.",
desti.getName(), liniesEsperades, liniesReals));
}
LOG.fine(() -> String.format("Verificacio OK: %d linies, %d bytes",
liniesReals, mida));
// ---- FASE 3: substituir. Nomes si tot l'anterior ha anat be ----
if (desti.exists() && !desti.delete()) {
throw new IOException("No s'ha pogut eliminar el desti " + desti.getName());
}
if (!temporal.renameTo(desti)) {
throw new IOException("No s'ha pogut reanomenar " + temporal.getName());
}
completat = true;
LOG.info(() -> String.format("Escriptura verificada de %s: %d linies, %d bytes",
desti.getAbsolutePath(), liniesEsperades, mida));
} finally {
// COMPENSACIO (06-05): sense brossa, i amb l'original intacte
if (!completat && temporal.exists() && !temporal.delete()) {
LOG.warning(() -> "Temporal sense esborrar: " + temporal.getAbsolutePath());
}
}
}
private static int comptarLinies(File f) throws IOException {
int n = 0;
try (Scanner sc = new Scanner(f, StandardCharsets.UTF_8)) {
while (sc.hasNextLine()) {
sc.nextLine();
n++;
}
}
return n;
}
// ------------------------- DEMOSTRACIO -------------------------
public static void main(String[] args) throws IOException {
File desti = new File("dades/cataleg-verificat.txt");
// 1. Escriptura correcta de 3 linies
escriureVerificat(desti, sortida -> {
sortida.println("LLIBRE;978-0000000001;Java Eficac;Bloch;2018");
sortida.println("LLIBRE;978-0000000002;Patrons de Disseny;Gamma;1994");
sortida.println("LLIBRE;978-0000000003;Refactoritzacio;Fowler;1999");
}, 3);
System.out.println("Despres de l'escriptura correcta: "
+ desti.length() + " bytes, "
+ comptarLinies(desti) + " linies");
// 2. Escriptura que FALLA a mitges
try {
escriureVerificat(desti, sortida -> {
sortida.println("LLIBRE;978-0000000004;Llibre nou;Autor;2024");
// Fallada simulada: disc ple, error de serialitzacio, el que sigui
throw new IOException("Fallada simulada a mitja exportacio");
}, 4);
} catch (IOException e) {
System.out.println("Fallada capturada: " + e.getMessage());
}
// 3. COMPROVACIO CLAU: el fitxer bo continua alla, sencer
System.out.println("Despres de la fallada: "
+ desti.length() + " bytes, "
+ comptarLinies(desti) + " linies");
System.out.println("Ha quedat brossa .tmp? "
+ new File(desti.getAbsolutePath() + ".tmp").exists());
// 4. Escriptura amb nombre de linies incorrecte: es rebutja
try {
escriureVerificat(desti, sortida -> sortida.println("Nomes una linia"), 5);
} catch (IOException e) {
System.out.println("Verificacio: " + e.getMessage());
}
System.out.println("Despres de la verificacio fallida: "
+ comptarLinies(desti) + " linies (intacte)");
}
}Sortida:
Despres de l'escriptura correcta: 147 bytes, 3 linies
Fallada capturada: Fallada simulada a mitja exportacio
Despres de la fallada: 147 bytes, 3 linies
Ha quedat brossa .tmp? false
Verificacio: Verificacio fallida a cataleg-verificat.txt: s'esperaven 5 linies i n'hi ha 1. El fitxer original NO s'ha modificat.
Despres de la verificacio fallida: 3 linies (intacte)Les tres línies que importen d'aquesta sortida són la tercera, la quarta i l'última: després d'una fallada a mitja escriptura i després d'una verificació fallida, el fitxer bo continua amb les seves 3 línies i no ha quedat brossa temporal. Això és exactament el que aporta el patró, i és impossible d'aconseguir escrivint directament sobre el destí.
Fixa't també que la verificació va entre l'escriptura i el reanomenat. Aquest buit és el que fa que el patró sigui tan valuós: pots comprovar el que vulguis —nombre de línies, capçalera, format, fins i tot rellegir-lo i analitzar-lo sencer— abans de comprometre't a substituir l'original.
Solució 3
package com.nexussoftware.bibliotech.infraestructura;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Registre d'auditoria de BiblioTech amb rotacio per mida.
*
* Mode AFEGIR: el fitxer creix, no es regenera. Per aixo NO fa servir
* escriptura atomica: no hi ha res per reemplacar.
*
* Separador de linia fix '\n': es un fitxer de DADES que es pot comparar
* entre maquines, no un informe per llegir a la pantalla (apartat 8).
*/
public class AuditoriaBiblioTech {
private static final Logger LOG = Logger.getLogger(AuditoriaBiblioTech.class.getName());
private static final boolean AFEGIR = true;
private static final String SALT = "\n";
private static final String SEP = ";";
/** 1 KB, petit a proposit per poder provar la rotacio. */
private static final long MIDA_MAXIMA = 1024L;
/** Fitxers historics que es conserven: auditoria-1 .. auditoria-3. */
private static final int MAX_HISTORICS = 3;
private final File directori;
private final String nomBase;
private int operacionsRegistrades = 0;
private int rotacions = 0;
private int fallades = 0;
public AuditoriaBiblioTech(String directori) {
this.directori = new File(
Objects.requireNonNull(directori, "El directori no pot ser nul"));
this.nomBase = "auditoria";
if (!this.directori.exists() && !this.directori.mkdirs()) {
LOG.warning("No s'ha pogut crear " + this.directori.getAbsolutePath());
}
}
/**
* Registra una operacio.
*
* POLITICA: una fallada d'auditoria NO es propaga MAI. L'operacio de
* negoci ja es valida; perdre'n el rastre es una degradacio acceptable
* i s'avisa al log (06-07).
*/
public void registrar(String operacio, String referencia, String idEmpleat) {
Objects.requireNonNull(operacio, "L'operacio no pot ser nulla");
File actual = new File(directori, nomBase + ".txt");
try {
// 1. Rotar ABANS d'escriure, si toca
if (actual.exists() && actual.length() >= MIDA_MAXIMA) {
rotar(actual);
}
// 2. Afegir la linia. El 'true' es el que fa que aixo sigui
// un historic i no un fitxer d'una sola linia.
try (PrintWriter sortida = new PrintWriter(
new FileWriter(actual, StandardCharsets.UTF_8, AFEGIR))) {
sortida.write(String.join(SEP, operacio, referencia, idEmpleat));
sortida.write(SALT);
if (sortida.checkError()) {
throw new IOException("Fallada escrivint a " + actual.getAbsolutePath());
}
}
operacionsRegistrades++;
} catch (IOException e) {
fallades++;
LOG.log(Level.WARNING, "No s'ha pogut auditar " + operacio
+ " de " + referencia, e);
// Sense rellancar: la degradacio es deliberada
}
}
/**
* Desplaca els historics: -2 passa a -3, -1 passa a -2, l'actual a -1.
*
* Es recorre de MAJOR a MENOR. A l'inreves, el primer reanomenat
* matxucaria el seguent abans d'haver-lo desplacat.
*/
private void rotar(File actual) throws IOException {
// El mes antic es perd
File mesAntic = historic(MAX_HISTORICS);
if (mesAntic.exists() && !mesAntic.delete()) {
throw new IOException("No s'ha pogut esborrar " + mesAntic.getName());
}
for (int i = MAX_HISTORICS - 1; i >= 1; i--) {
File origen = historic(i);
File desti = historic(i + 1);
if (origen.exists() && !origen.renameTo(desti)) {
throw new IOException("No s'ha pogut rotar " + origen.getName());
}
}
if (!actual.renameTo(historic(1))) {
throw new IOException("No s'ha pogut rotar " + actual.getName());
}
rotacions++;
LOG.fine(() -> "Auditoria rotada (rotacio numero " + rotacions + ")");
}
private File historic(int n) {
return new File(directori, nomBase + "-" + n + ".txt");
}
public int getOperacionsRegistrades() { return operacionsRegistrades; }
public int getRotacions() { return rotacions; }
public int getFallades() { return fallades; }
// ------------------------- DEMOSTRACIO -------------------------
public static void main(String[] args) {
AuditoriaBiblioTech auditoria = new AuditoriaBiblioTech("dades/auditoria");
String[] operacions = { "PRESTEC", "DEVOLUCIO", "ALTA", "BAIXA" };
String[] empleats = { "E-001", "E-002", "E-003" };
for (int i = 1; i <= 200; i++) {
auditoria.registrar(
operacions[i % operacions.length],
String.format("PR-%04d", i),
empleats[i % empleats.length]);
}
System.out.println("=== AUDITORIA ===");
System.out.printf(" Operacions registrades: %d%n",
auditoria.getOperacionsRegistrades());
System.out.printf(" Rotacions realitzades : %d%n", auditoria.getRotacions());
System.out.printf(" Fallades : %d%n", auditoria.getFallades());
System.out.println(" --- Fitxers ---");
File dir = new File("dades/auditoria");
File[] fitxers = dir.listFiles(); // pot ser null: es comprova
if (fitxers != null) {
java.util.Arrays.sort(fitxers); // 05-09
for (File f : fitxers) {
System.out.printf(" %-20s %6d bytes%n", f.getName(), f.length());
}
}
}
}Sortida:
=== AUDITORIA ===
Operacions registrades: 200
Rotacions realitzades : 4
Fallades : 0
--- Fitxers ---
auditoria-1.txt 1042 bytes
auditoria-2.txt 1040 bytes
auditoria-3.txt 1039 bytes
auditoria.txt 85 bytesEls quatre punts didàctics:
- El bucle de rotació va de major a menor. És el detall que més es falla. Si rotessis d'1 a 3, el primer
renameTomatxucariaauditoria-2.txtabans d'haver-lo desplaçat a-3, i perdries tot l'històric menys l'últim. Fes-te el dibuix mental de les tres fletxes abans d'escriure-ho. - Es rota abans d'escriure, no després. Així el fitxer no supera mai el límit de forma apreciable, i no cal preocupar-se de la mida de la línia que hi entrarà.
- La fallada no es propaga i es compta. El comptador
falladespermet detectar a l'informe que l'auditoria està degradada, encara que l'aplicació continuï funcionant. Degradar sense deixar rastre seria pitjor que fallar. listFiles()pot retornarnull. La comprovació no és paranoia: retornanullsi el directori no existeix o si falla l'accés. És elbooleanmut de l'apartat 6 de 07-01 en una altra forma, i a 07-06 ho resolFiles.list().
I observa que aquest mecanisme és exactament el que fa per dins el FileHandler de java.util.logging que vas configurar a 06-07, amb el seu bibliotech-%g.log, els seus 5 fitxers i el seu límit d'1 MB. Ara ja saps com està implementat.
Conclusió
BiblioTech ja desa.
Coneixes FileWriter i el seu comportament en l'obertura: crea el fitxer si no existeix i —això és el que cal recordar per damunt de tot— el trunca a zero bytes si existeix, en l'instant de construir-lo, abans d'escriure res. Domines el paràmetre append i saps que oblidar-lo converteix un històric de tres-centes mil línies en un fitxer d'una, sense excepció, sense avís i de forma perfectament estable. Tens les tres defenses: anomenar el boolean amb una constant, fer servir les opcions explícites de NIO.2 que veuràs a 07-06, i escriure sempre a un temporal quan regeneris un fitxer sencer.
Saps escriure amb write en les seves cinc sobrecàrregues, amb l'asimetria de write(int) que escriu un caràcter i no un nombre, i coneixes PrintWriter com a embolcall que aporta println, printf i format reutilitzant el formatatge del mòdul 1 —amb Locale.ROOT quan el fitxer l'ha de llegir un programa i el Locale del sistema quan l'ha de llegir una persona—. I coneixes el seu parany major: PrintWriter no llança IOException, se la guarda en un marcador, així que un disc ple passa desapercebut si no crides checkError().
Entens la memòria intermèdia i la cadena completa de buidatges: write arriba a la memòria intermèdia de l'aplicació, flush arriba al sistema operatiu, close fa flush i allibera, i només sync arriba al plat del disc. Saps per què un fitxer acabat d'escriure té 0 bytes, per què tail -f no mostra res durant una estona, i què passa exactament si el programa acaba sense tancar: el fitxer queda buit, perquè el recol·lector de brossa no tanca res i finalize() està eliminat. Per això el try-with-resources importa més en escriure que en llegir: allà no tancar és una fuita; aquí és pèrdua de dades.
Saps que el charset explícit és encara més crític en escriure, perquè l'error queda gravat i només apareix quan una altra eina llegeix el fitxer; i que escriure un caràcter no representable —una ç en US-ASCII— el substitueix per ? sense dir res. Coneixes System.lineSeparator(), les tres convencions de final de línia, i el criteri per triar: el del sistema per als informes, \n fix per als fitxers de dades que es comparen o es versionen. I saps que el charset i el separador s'han de declarar una sola vegada en una constant compartida pel lector i l'escriptor.
I tens el patró que separa el codi aficionat del professional: l'escriptura atòmica. Escriure en un temporal, verificar, i reanomenar només al final. Saps per què funciona —el reanomenat és atòmic per a qui llegeix, així que mai no es veu un fitxer a mitges—, saps que el finally compensa esborrant el temporal exactament com vas aprendre a 06-05, i saps que el destí no necessita compensació perquè no es va obrir mai. I saps quan aplicar-la: quan el fitxer es regenera; mai quan el fitxer creix, que és el cas del mode append. Tota la lliçó cap en aquesta pregunta: aquest fitxer es regenera o creix?
Coneixes també els fitxers de bloqueig amb createNewFile() com a operació atòmica de comprovar-i-crear, les seves limitacions honestes —bloqueigs orfes, l'alternativa de FileLock—, i les còpies de seguretat rotatives que 07-06 farà bé amb NIO.2.
BiblioTech, en tancar aquesta lliçó, carrega el seu catàleg en arrencar i el desa en sortir. ExportadorCataleg escriu de veritat, de forma atòmica, comparteix amb CarregadorCataleg les constants de FormatFitxers perquè no puguin discrepar, i ha deixat de ser Closeable perquè ja no posseeix cap recurs viu —un objecte que no té res per tancar no ha de prometre que es tanca—. El seu registre d'auditoria s'obre en mode append perquè creix, i les seves fallades no tomben l'operació de negoci, sinó que degraden amb un avís al log. I un shutdown hook garanteix el desat en la sortida normal, amb l'advertència explícita que un kill -9 se'l salta i que l'estratègia seriosa és desar també durant l'execució.
Queda una pregunta de fons que les dues lliçons han esquivat. Has fet servir FileReader, FileWriter, Scanner i PrintWriter com si fossin peces soltes, i no ho són: formen part d'un disseny amb una estructura molt deliberada. Per què PrintWriter embolcalla FileWriter en lloc de substituir-lo? Per què hi ha dues famílies diferents de classes, unes acabades en Reader/Writer i altres en InputStream/OutputStream? I com es llegeix una imatge, que no és text?
A la lliçó 07-03, Fluxos de fitxers, es respon. Veuràs el model conceptual del flux amb el seu vocabulari de font, destinació i direcció; les dues jerarquies —bytes i caràcters— i per què havien de ser dues; la distinció entre classes de node i classes de filtre, que és el patró Decorador en estat pur i explica d'una vegada aquests new BufferedInputStream(new FileInputStream(...)) que apareixen pertot arreu; els ponts InputStreamReader i OutputStreamWriter, que són exactament el punt on es decideix la codificació que portes dues lliçons especificant; la lectura i l'escriptura de dades binàries per bytes i per blocs, amb la còpia de la imatge de portada d'un llibre; i transferTo, la forma moderna de copiar un flux sencer en una línia. En acabar-la, deixaràs de fer servir aquestes classes de memòria i començaràs a compondre-les sabent exactament què fa cada capa.
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
