El mòdul 6 va acabar amb una promesa i una mancança. La mancança: res no es guarda en sortir. Sis mòduls de feina —el catàleg, els préstecs, les multes, les reserves, l'historial— viuen íntegrament a la memòria del procés, i quan el procés acaba, la memòria s'allibera i tot desapareix. No hi ha res per recuperar. La promesa: que aquest mòdul ho resol.
Comencem per la meitat més senzilla de la persistència, que és llegir. Llegir és més senzill que escriure per una raó pràctica: si t'equivoques llegint, no destrueixes res. Si t'equivoques escrivint, pots perdre un fitxer sencer, i això és exactament el que li passarà a algú a la lliçó següent si no llegeix la taula del paràmetre append.
Aquesta lliçó cobreix el model mental complet de què és un fitxer, com es localitza i com se'n llegeix text amb les dues API clàssiques —FileReader i Scanner—, més el detall que provoca més incidències reals de tot el mòdul: la codificació de caràcters. En acabar, BiblioTech arrencarà llegint el seu catàleg d'un fitxer de text, i si aquest fitxer no existeix, arrencarà igualment amb el catàleg buit i deixant-ne constància al registre, aplicant la degradació elegant que ja saps dissenyar.
Tot el que s'obri en aquesta lliçó es tanca amb
try-with-resources. És la construcció de 06-06 i es dona per completament coneguda: ordre de tancament invers, excepcions suprimides,close()idempotent. No es torna a explicar; es fa servir sempre.
Contingut
- Per què un programa necessita persistència
- Què és un fitxer: bytes al disc davant d'objectes en memòria
- El recorregut d'una dada des de l'aplicació fins al disc
- Camins absoluts i relatius, i el directori de treball
- Separadors de camí i portabilitat
- La classe
File: l'API heretada que encara et trobaràs - Lectura amb
FileReader: caràcter a caràcter - Lectura amb
Scannersobre un fitxer useDelimiteri l'anàlisi còmoda- Codificació de caràcters: UTF-8 i el desastre dels accents
FileNotFoundExceptiondavant d'IOException- Degradació elegant: BiblioTech arrenca sense catàleg
- Fitxers grans i per què no llegir-ho tot en memòria
- Errors Comuns i Consells
- Exercicis
- Per què un programa necessita persistència
La memòria d'un procés és volàtil: existeix mentre el procés existeix. Quan main retorna, quan l'usuari tanca la finestra o quan el sistema operatiu mata el procés, la JVM allibera la seva memòria i tot el que hi havia deixa d'existir. No hi ha res per recuperar.
Això no és un defecte de Java: és la naturalesa de la RAM. Un programa que només treballa en memòria és un programa sense memòria a llarg termini, i això el desqualifica per a gairebé qualsevol ús real:
| Necessitat | Sense persistència | Amb persistència |
|---|---|---|
| Recordar dades entre execucions | Impossible | És el cas base |
| Carregar mil materials | Teclejant-los un a un | Un fitxer de mil línies |
| Compartir dades amb un altre programa | Impossible | Un fitxer que tots dos entenen |
| Auditar què va passar ahir | Impossible | Un fitxer de registre |
| Configurar sense recompilar | Impossible | Un fitxer de propietats |
| Recuperar-se d'una caiguda | Es perd tot | Es recupera l'últim estat desat |
Fixa't en l'última fila de la columna esquerra aplicada a BiblioTech: si l'aplicació cau amb vint préstecs actius, aquests vint préstecs no van existir mai. La biblioteca real continuarà tenint vint llibres fora del prestatge, però el sistema dirà que estan tots disponibles. El sistema i el món deixen de coincidir, que és la pitjor cosa que li pot passar a un sistema de gestió.
Persistir significa escriure l'estat en un mitjà que sobreviu al procés. En aquest mòdul aquest mitjà és el sistema de fitxers. En un sistema professional gran acostuma a ser una base de dades —ho veuràs amb Hibernate a 11-03—, però una base de dades és, per sota, fitxers amb molta enginyeria a sobre. Tot comença aquí.
- Què és un fitxer: bytes al disc davant d'objectes en memòria
Aquesta és la idea que cal interioritzar abans d'escriure una línia de codi:
Un fitxer és una seqüència de bytes amb un nom. Res més. No té tipus, no té objectes, no té camps. Només bytes numerats del 0 endavant.
Compara els dos mons:
| Objecte en memòria | Fitxer al disc | |
|---|---|---|
| Naturalesa | Estructura amb camps tipats | Seqüència plana de bytes |
| Identitat | Referència (adreça) | Camí (nom) |
| Vida | Mentre duri el procés | Fins que algú l'esborri |
| Accés | Directe, nanosegons | A través del sistema operatiu, microsegons o més |
| Referències entre dades | Punters natius | Cal inventar-se-les (identificadors, desplaçaments) |
| Tipus | El compilador els coneix | No existeixen: tot són bytes |
| Cost d'una operació | Barata | Cara: hi ha una crida al sistema pel mig |
Quan tens en memòria un Llibre amb títol "Java Eficaç", autor "Bloch" i ISBN 978-0000000001, l'objecte ocupa una zona de memòria amb tres referències a tres objectes String. Al disc no pots desar referències: una adreça de memòria no significa res demà ni en una altra màquina. Has de serialitzar: convertir aquesta estructura en una seqüència de bytes que puguis tornar a interpretar després. La forma més simple de fer-ho és escriure text:
Aquesta línia és una decisió de disseny completa. Has triat: un material per línia, camps separats per ;, ordre fix de camps, true/false per a la disponibilitat. Qualsevol programa que conegui aquesta convenció pot llegir-la. És exactament el que fa un fitxer CSV, i el formalitzaràs a 07-07.
L'alternativa és deixar que Java faci la conversió per tu, amb la serialització nativa de 07-05, que produeix bytes il·legibles però conserva l'estructura completa d'objectes. Cada opció té el seu preu, i el veuràs.
Un matís important sobre el vocabulari: es parla de fitxers de text i fitxers binaris, però aquesta distinció no existeix al disc. Tots els fitxers són binaris. Un "fitxer de text" és simplement un fitxer els bytes del qual, interpretats amb una determinada codificació, produeixen caràcters llegibles. Si obres un .jpg amb un editor de text, hi veuràs brossa: no perquè el fitxer sigui diferent, sinó perquè hi estàs aplicant la interpretació equivocada. Hi tornarem a l'apartat 10 i, a fons, a 07-03.
- El recorregut d'una dada des de l'aplicació fins al disc
Entre el teu String i el plat del disc hi ha més capes de les que sembla. Conèixer-les explica per què l'E/S és lenta, per què existeix la memòria intermèdia i per què un close() pot fallar.
flowchart TD
A["El teu codi Java<br/>String linia = lector.readLine()"] --> B["Classes java.io<br/>FileReader, Scanner"]
B --> C["Descodificacio de bytes a caracters<br/>charset UTF-8"]
C --> D["Crida al sistema operatiu<br/>read syscall"]
D --> E["Cache de disc del sistema operatiu<br/>page cache a la RAM"]
E --> F["Controlador del dispositiu"]
F --> G["Disc fisic<br/>SSD o disc dur"]
style A fill:#e3f2fd
style D fill:#fff3e0
style G fill:#f3e5f5
Els punts que importen d'aquest recorregut:
- La frontera cara és la crida al sistema (requadre taronja). Cada
readobliga a passar del codi de la teva aplicació al nucli del sistema operatiu i tornar. Aquest canvi de context costa ordres de magnitud més que executar unes quantes instruccions en memòria. Si fas una crida al sistema per cada caràcter, el cost és demolidor: és exactament el que passa ambFileReadersense memòria intermèdia, i ho mesuraràs a l'apartat 7. - La descodificació és un pas real (requadre blau clar del mig). Els bytes que arriben del disc no són caràcters. Algú ha de decidir quin byte o grup de bytes forma quin caràcter, i aquest algú és el charset. Si ningú no l'especifica, se'n fa servir un per defecte, i aquí comencen els problemes de l'apartat 10.
- El sistema operatiu ja té la seva pròpia memòria cau (la page cache). Per això llegir dues vegades el mateix fitxer petit és molt més ràpid la segona vegada: la segona no arriba al disc. Això també explica per què mesurar el rendiment d'E/S és traïdor.
- Escriure no significa "és al disc". Quan escrius, la dada passa a memòries intermèdies. Fins que no es buiden, un tall de corrent la perd. És el tema central de 07-02.
Queda't amb la regla que governa tot el mòdul: com menys crides al sistema, millor. Tot el disseny de l'E/S de Java —les memòries intermèdies, els blocs, transferTo— existeix per reduir aquest nombre.
- Camins absoluts i relatius, i el directori de treball
Per llegir un fitxer cal localitzar-lo, i aquí apareix el primer entrebanc clàssic: "a mi em funcionava i al servidor diu que no troba el fitxer".
Un camí absolut parteix de l'arrel del sistema de fitxers i és autosuficient:
/home/marta/bibliotech/dades/cataleg.txt (Linux, macOS)
C:\Users\marta\bibliotech\dades\cataleg.txt (Windows)Un camí relatiu no parteix de l'arrel, sinó del directori de treball actual del procés:
La pregunta clau és: relatiu a què? I la resposta és la que sorprèn tothom: relatiu al directori des del qual es va llançar la JVM, no al directori on hi ha el .class ni al del projecte. Aquest directori és a la propietat de sistema user.dir:
public class OnSoc {
public static void main(String[] args) {
System.out.println("Directori de treball: " + System.getProperty("user.dir"));
System.out.println("Directori de l'usuari: " + System.getProperty("user.home"));
System.out.println("Directori temporal: " + System.getProperty("java.io.tmpdir"));
java.io.File f = new java.io.File("dades/cataleg.txt");
System.out.println("Cami relatiu donat: " + f.getPath());
System.out.println("Es resol com: " + f.getAbsolutePath());
System.out.println("Existeix? " + f.exists());
}
}Executa aquest programa des de dos llocs diferents i veuràs el problema en directe:
# Des de l'arrel del projecte
$ cd /home/marta/bibliotech
$ java -cp target/classes OnSoc
Directori de treball: /home/marta/bibliotech
Es resol com: /home/marta/bibliotech/dades/cataleg.txt
Existeix? true
# Des del directori de classes
$ cd /home/marta/bibliotech/target/classes
$ java OnSoc
Directori de treball: /home/marta/bibliotech/target/classes
Es resol com: /home/marta/bibliotech/target/classes/dades/cataleg.txt
Existeix? falseEl mateix programa, el mateix fitxer al disc, dos resultats diferents. Això és el que passa quan funciona a l'IDE (que llança des de l'arrel del projecte) i falla en executar-lo amb un script (que llança des d'un altre lloc).
Com es tracta això professionalment:
| Estratègia | Quan fer-la servir |
|---|---|
Camí per paràmetre (args[0] o propietat -Dcami=...) |
Gairebé sempre. Qui executa decideix on són les dades |
Camí relatiu a user.home |
Configuració d'usuari: System.getProperty("user.home") + "/.bibliotech/" |
| Camí absolut en fitxer de configuració | Desplegaments en servidor. Ho veuràs a 07-07 |
Recurs del classpath (getResourceAsStream) |
Dades que viatgen dins del .jar i no canvien |
| Camí relatiu a seques | Només en proves i eines de línia d'ordres, i documentant-ho |
I un consell que estalvia molt de temps: quan un fitxer "no existeix", imprimeix sempre el camí absolut, mai el que t'han passat. El missatge No es troba 'dades/cataleg.txt' no et diu res; No es troba '/home/marta/bibliotech/target/classes/dades/cataleg.txt' et resol el problema en tres segons. Aquesta és una aplicació directa de la lliçó 06-04: una excepció ha de transportar les dades que qui la llegeix necessita per decidir.
- Separadors de camí i portabilitat
Windows fa servir \ com a separador de directoris; Linux, macOS i pràcticament tota la resta fan servir /. Java ofereix dues constants per no fixar-ho al codi:
import java.io.File;
public class Separadors {
public static void main(String[] args) {
System.out.println("Separador de camins: '" + File.separator + "'");
System.out.println("Separador de llistes: '" + File.pathSeparator + "'");
// Construccio portable d'un cami
String cami = "dades" + File.separator + "cataleg.txt";
System.out.println("Cami portable: " + cami);
// Alternativa molt millor: el constructor de File amb pare i fill
File f = new File("dades", "cataleg.txt");
System.out.println("Amb File(pare, fill): " + f.getPath());
}
}Distingeix les dues constants, que es confonen sovint:
| Constant | Linux/macOS | Windows | Per a què serveix |
|---|---|---|---|
File.separator |
/ |
\ |
Separar directoris dins d'un camí |
File.pathSeparator |
: |
; |
Separar camins dins d'una llista, com el CLASSPATH |
Ara bé, la bona notícia pràctica: Windows accepta / a gairebé totes les API de Java. new File("dades/cataleg.txt") funciona a Windows. Per això, a la pràctica:
- Per a literals curts al codi, escriu
/i no t'ho compliquis: és llegible i funciona a tot arreu. - Per compondre camins a partir de trossos, no concatenis cadenes: fes servir
new File(pare, fill)o, encara millor,Path.resolve()de NIO.2, que veuràs a 07-06 i és la forma moderna i correcta. - No escriguis mai
"C:\\dades\\cataleg.txt"al codi. A més de no ser portable, aquesta doble barra invertida és una font inesgotable d'errades.
- La classe
File: l'API heretada que encara et trobaràs
File: l'API heretada que encara et trobaràsjava.io.File és de Java 1.0. Representa un camí, no un fitxer obert: pots crear un File que apunti a una cosa inexistent sense que passi res.
import java.io.File;
public class InspeccionarFitxer {
public static void inspeccionar(String cami) {
File f = new File(cami);
System.out.println("=== " + cami + " ===");
System.out.println(" Cami absolut : " + f.getAbsolutePath());
System.out.println(" Nom : " + f.getName());
System.out.println(" Directori : " + f.getParent());
System.out.println(" Existeix? : " + f.exists());
if (!f.exists()) {
System.out.println(" (no hi ha res mes per inspeccionar)");
return;
}
System.out.println(" Es fitxer? : " + f.isFile());
System.out.println(" Es carpeta? : " + f.isDirectory());
System.out.println(" Llegible? : " + f.canRead());
System.out.println(" Escrivible? : " + f.canWrite());
System.out.println(" Ocult? : " + f.isHidden());
System.out.println(" Mida : " + f.length() + " bytes");
System.out.println(" Modificat : " + f.lastModified() + " (millisegons des de 1970)");
}
public static void main(String[] args) {
inspeccionar("dades/cataleg.txt");
inspeccionar("dades");
inspeccionar("no-existeix-aixo.txt");
}
}Els mètodes que més es fan servir:
| Mètode | Retorna | Nota important |
|---|---|---|
exists() |
boolean |
false també si no tens permís per saber-ho |
isFile() / isDirectory() |
boolean |
Tots dos false si no existeix |
canRead() / canWrite() |
boolean |
Depèn de l'usuari que executa la JVM |
length() |
long |
Bytes. 0 si no existeix, indistingible d'un fitxer buit |
getName() |
String |
Només el nom, sense directoris |
getPath() |
String |
El camí tal com s'ha donat |
getAbsolutePath() |
String |
Resolt contra user.dir, sense normalitzar els .. |
getCanonicalPath() |
String |
Absolut i normalitzat. Llança IOException |
lastModified() |
long |
Millisegons des de 1970. 0 si no existeix |
listFiles() |
File[] |
null si no és un directori o falla. Parany clàssic |
delete() |
boolean |
false sense dir per què |
mkdirs() |
boolean |
Crea també els directoris intermedis |
I aquí hi ha el gran defecte d'aquesta API, el que en va motivar la substitució:
File f = new File("/dades/important.txt");
boolean esborrat = f.delete();
if (!esborrat) {
// Per que? No existia? No tinc permis? Estava obert?
// Es un directori no buit? L'API no ho diu. Un false mut.
System.out.println("No s'ha pogut esborrar");
}Aquest false mut és exactament l'antipatró que el mòdul 6 va dedicar set lliçons a eradicar: comunicar una fallada sense dir quina. L'API moderna, NIO.2 (java.nio.file.Path i Files, lliçó 07-06), llança excepcions específiques —NoSuchFileException, AccessDeniedException, DirectoryNotEmptyException— en lloc de retornar boolean.
Aleshores, per què aprenem
File? Per tres motius molt pràctics. Primer, perquè hi ha milions de línies de codi escrites amb ella i les llegiràs. Segon, perquè moltes API encara demanen unFilecom a paràmetre. I tercer, perquè la conversió entre tots dos mons és trivial:file.toPath()ipath.toFile(). Fes servir NIO.2 al codi nou; enténFileper llegir l'antic.
- Lectura amb
FileReader: caràcter a caràcter
FileReader: caràcter a caràcterFileReader és la classe més elemental per llegir text d'un fitxer. El seu mètode fonamental és read(), i el seu contracte té un detall que cal entendre bé:
Retorna un int, no un char. Per què? Perquè necessita un valor extra que no sigui un caràcter vàlid per assenyalar el final de fitxer, i aquest valor és -1. Els caràcters vàlids van de 0 a 65535; -1 no col·lisiona amb cap. Si el mètode retornés char, no hi hauria manera de distingir "he llegit el caràcter 0" de "s'ha acabat".
import java.io.FileReader;
import java.io.IOException;
public class LecturaCaracterACaracter {
public static void mostrar(String cami) throws IOException {
try (FileReader lector = new FileReader(cami)) {
int codi; // int, NO char
while ((codi = lector.read()) != -1) {
char c = (char) codi; // conversio explicita
System.out.print(c);
}
}
// El try-with-resources ja l'ha tancat (06-06)
}
public static void main(String[] args) throws IOException {
mostrar("dades/cataleg.txt");
}
}Desglossem el bucle, perquè la seva forma és idiomàtica i la veuràs pertot arreu:
lector.read()llegeix el caràcter següent i avança la posició.codi = ...desa el resultat a la variable. En Java, una assignació és una expressió el valor de la qual és l'assignat; per això pot fer-se servir dins de la condició.- Els parèntesis externs són obligatoris: sense ells,
codi = lector.read() != -1intentaria assignar unbooleana uninti no compilaria. - La comparació decideix si continuar.
I ara, el problema. Aquest codi és correcte però lent, i convé entendre exactament quant:
import java.io.FileReader;
import java.io.IOException;
public class MesurarLecturaLenta {
public static long comptarCaracters(String cami) throws IOException {
long total = 0;
try (FileReader lector = new FileReader(cami)) {
while (lector.read() != -1) {
total++;
}
}
return total;
}
public static void main(String[] args) throws IOException {
long inici = System.nanoTime();
long n = comptarCaracters("dades/cataleg-gran.txt");
long ms = (System.nanoTime() - inici) / 1_000_000;
System.out.printf("%d caracters llegits en %d ms%n", n, ms);
}
}Amb un fitxer de 5 MB, en una màquina d'escriptori corrent, els ordres de magnitud són aquests (xifres il·lustratives, no un mesurament formal):
| Tècnica | Crides al sistema aproximades | Temps orientatiu |
|---|---|---|
FileReader.read() caràcter a caràcter |
~5 000 000 | ~4 500 ms |
FileReader.read(char[8192]) per blocs |
~640 | ~90 ms |
BufferedReader.readLine() (lliçó 07-04) |
~640 | ~60 ms |
Dos ordres de magnitud de diferència, i la causa és exactament la de l'apartat 3: cada read() sense memòria intermèdia es tradueix en una petició al sistema operatiu. Cinc milions de creuaments de frontera per llegir cinc megabytes.
La solució intermèdia, sense sortir encara de FileReader, és llegir per blocs amb la sobrecàrrega que accepta un array:
import java.io.FileReader;
import java.io.IOException;
public class LecturaPerBlocs {
public static String llegirTot(String cami) throws IOException {
StringBuilder contingut = new StringBuilder();
try (FileReader lector = new FileReader(cami)) {
char[] bloc = new char[8192]; // 8 KB, mida habitual
int llegits;
// read(char[]) retorna QUANTS caracters ha posat a l'array,
// o -1 si no hi havia res per llegir. Pot retornar MENYS de 8192
// encara que quedin dades: no suposis mai que omple l'array.
while ((llegits = lector.read(bloc)) != -1) {
contingut.append(bloc, 0, llegits); // nomes la part util
}
}
return contingut.toString();
}
}Dos detalls crítics d'aquest codi, i tots dos són font d'errors reals:
read(char[])retorna el nombre d'elements llegits, que pot ser menor que la mida de l'array encara que el fitxer no s'hagi acabat. No suposis mai que omple la memòria intermèdia.- Cal fer servir
append(bloc, 0, llegits), noappend(bloc). Si fas servir la versió sense límits, a l'última volta afegiràs la brossa que va quedar a l'array de la volta anterior. És una fallada que només es manifesta al final del fitxer i amb contingut aparentment aleatori.
La forma definitiva de resoldre això —embolcallar el FileReader en un BufferedReader que gestiona el bloc per tu— és el tema de 07-04. Aquí queda clar el perquè; allà en veuràs el com amb tot el detall.
- Lectura amb
Scanner sobre un fitxer
Scanner sobre un fitxerJa coneixes Scanner del mòdul 1, llegint de System.in. La mateixa classe llegeix d'un fitxer: només canvia el que li passes al constructor.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class LecturaAmbScanner {
public static void mostrarLinies(String cami) throws FileNotFoundException {
// Scanner es Closeable: try-with-resources obligatori (06-06).
// Tancar-lo tanca tambe el fitxer subjacent.
try (Scanner sc = new Scanner(new File(cami))) {
int numero = 1;
while (sc.hasNextLine()) {
String linia = sc.nextLine();
System.out.printf("%3d | %s%n", numero++, linia);
}
}
}
}La parella hasNextLine() / nextLine() és el patró canònic i mereix un avís:
Comprova sempre amb
hasNextLine()abans de cridarnextLine(). Si cridesnextLine()sense que quedin línies, llançaNoSuchElementException, que és no comprovada: el compilador no t'avisa i peta en execució. És el mateix contracte d'Iteratorque vas veure a 05-02.
Scanner no es queda en línies: sap analitzar tipus, i aquí hi ha la seva comoditat. Suposa aquest fitxer de préstecs de BiblioTech:
PR-0001 978-0000000001 E-001 12 0.25
PR-0002 978-0000000002 E-002 30 0.25
PR-0003 978-0000000003 E-003 5 0.10Es llegeix així, camp a camp, sense partir cadenes a mà:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class LlegirPrestecs {
public static void llegir(String cami) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(cami))) {
while (sc.hasNext()) {
String referencia = sc.next(); // PR-0001
String isbn = sc.next(); // 978-0000000001
String empleat = sc.next(); // E-001
int dies = sc.nextInt(); // 12
double tarifa = sc.nextDouble();// 0.25
System.out.printf("%s: %s a %s, %d dies, %.2f EUR/dia%n",
referencia, isbn, empleat, dies, tarifa);
}
}
}
}Compte amb dos comportaments de Scanner que causen sorpreses:
nextDouble()depèn de la configuració regional. Amb l'idioma català, el separador decimal és la coma, així que0.25pot fallar ambInputMismatchExceptionmentre que0,25funciona. És una fallada que apareix només en algunes màquines i desconcerta. La solució és fixar la configuració explícitament:
import java.util.Locale;
Scanner sc = new Scanner(new File(cami));
sc.useLocale(Locale.ROOT); // punt decimal, sempre, a qualsevol maquinaAquest mateix problema, aplicat als fitxers CSV que s'obren en un full de càlcul català, reapareix a 07-07.
nextInt()no consumeix el salt de línia. Si barregesnextInt()ambnextLine(), elnextLine()retornarà la cadena buida que queda fins al final de la línia actual. És el parany que ja vas veure amb la consola al mòdul 1, i aquí és idèntic.
Comparació honesta de les dues API vistes fins ara:
FileReader a pèl |
Scanner sobre fitxer |
|
|---|---|---|
| Unitat de lectura | Caràcter o bloc de caràcters | Línia, paraula, int, double, expressió regular |
| Anàlisi | A mà | Inclosa |
| Velocitat | Lenta sense memòria intermèdia | Lenta: fa servir expressions regulars per dins |
| Errors de format | No els detecta | InputMismatchException |
| Excepció del constructor | FileNotFoundException |
FileNotFoundException |
| Codificació | Segon paràmetre Charset |
useLocale per a nombres, Charset al constructor |
| Bo per a | Copiar, processar sense estructura | Fitxers petits amb format tabulat |
| Dolent per a | Qualsevol cosa amb estructura | Fitxers grans: és el més lent dels tres |
Per a un fitxer de configuració de vint línies, Scanner és còmode i la seva lentitud és irrellevant. Per al fitxer de catàleg de deu mil materials, la resposta correcta és BufferedReader, i la veuràs a 07-04.
useDelimiter i l'anàlisi còmoda
useDelimiter i l'anàlisi còmodaPer defecte, Scanner separa per espais en blanc. useDelimiter canvia aquest criteri a qualsevol expressió regular, cosa que permet llegir fitxers amb separadors propis sense partir cadenes a mà.
Amb aquest fitxer de catàleg:
978-0000000001;Java Eficac;Bloch;2018
978-0000000002;Patrons de Disseny;Gamma;1994
978-0000000003;Refactoritzacio;Fowler;1999Es llegeix així:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class LlegirAmbDelimitador {
public static void llegir(String cami) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(cami))) {
// Delimitador: un punt i coma O un salt de linia (en qualsevol
// de les seves tres formes). \\R representa qualsevol terminador de linia.
sc.useDelimiter(";|\\R");
while (sc.hasNext()) {
String isbn = sc.next();
String titol = sc.next();
String autor = sc.next();
int any = sc.nextInt();
System.out.printf("[%s] %-25s %-10s %d%n", isbn, titol, autor, any);
}
}
}
}Delimitadors útils:
| Patró | Significat |
|---|---|
";" |
Només punt i coma |
";|\\R" |
Punt i coma o qualsevol salt de línia |
"\\s*,\\s*" |
Coma amb espais opcionals al voltant |
"\\R" |
Només salts de línia: equival a llegir línia a línia |
"\\Z" |
Final d'entrada: llegeix el fitxer sencer com un sol testimoni |
Aquest últim truc té la seva gràcia per a fitxers molt petits:
try (Scanner sc = new Scanner(new File("nota.txt"))) {
sc.useDelimiter("\\Z");
String tot = sc.hasNext() ? sc.next() : "";
System.out.println(tot);
}Funciona, però és un pedaç. Des de Java 11 existeix Files.readString(Path), que fa el mateix de forma explícita i correcta, i el veuràs a 07-06. Reconeix el truc quan el llegeixis en codi aliè; no l'escriguis tu.
I un advertiment que cal donar ja, tot i que es desenvolupi a 07-07: partir un CSV per ; o per , és correcte només mentre cap camp no contingui el separador. Tan bon punt aparegui un títol com "Java: el llenguatge, la màquina i l'ecosistema" dins de cometes, aquest codi produeix camps mal alineats en silenci. La lliçó 07-07 implementa un lector CSV que sí que ho fa bé.
- Codificació de caràcters: UTF-8 i el desastre dels accents
Aquest apartat és el més important de la lliçó. La majoria de les fallades d'E/S que veuràs a la teva carrera professional no són fallades de lògica: són fallades de codificació.
El problema de fons
Un fitxer conté bytes. Un String conté caràcters. Convertir d'un a l'altre requereix una taula de correspondències, i aquesta taula és el charset o codificació.
- ASCII (1963): 128 caràcters, un byte cadascun. Sense
ç, senseà, sense€. - ISO-8859-1 (Latin-1): 256 caràcters, un byte cadascun. Té
çi vocals accentuades. No té€ni res que no sigui europeu occidental. - Windows-1252: variant de l'anterior, la de Windows a Europa occidental. Gairebé igual però no exactament, cosa que genera fallades subtils.
- UTF-8 (1993): cobreix tot Unicode amb longitud variable d'1 a 4 bytes. ASCII n'és un subconjunt exacte. És l'estàndard de facto del web i de l'intercanvi de dades.
Quant ocupa cada caràcter en UTF-8:
| Caràcter | Bytes en UTF-8 | Bytes en ISO-8859-1 |
|---|---|---|
a |
1 (0x61) |
1 (0x61) |
ç |
2 (0xC3 0xA7) |
1 (0xE7) |
€ |
3 (0xE2 0x82 0xAC) |
No representable |
| Un emoji | 4 | No representable |
Conseqüència directa que sorprèn molta gent: en UTF-8, el nombre de caràcters d'un text no és el nombre de bytes del fitxer. "Eficaç" són 6 caràcters i 7 bytes. Per això File.length() (bytes) no et diu quants caràcters hi ha.
Què passa exactament si t'equivoques
Escrius "Préstec vençut" en UTF-8 i ho llegeixes com a ISO-8859-1. Els dos bytes de la ç (0xC3 0xA7) s'interpreten com dos caràcters independents:
A l'inrevés, escrius en ISO-8859-1 i llegeixes en UTF-8. El byte 0xE7 de la ç no forma una seqüència UTF-8 vàlida, i el descodificador el substitueix pel caràcter de reemplaçament:
Aquest ç i aquest � són els dos símptomes que cal aprendre a reconèixer a l'instant. Quan els vegis, no busquis la fallada a la teva lògica: és codificació.
I hi ha una cosa pitjor que la lletjor. Fixa't en aquest cas:
// Fitxer escrit en UTF-8, llegit com a ISO-8859-1
String estat = "Préstec vençut"; // el que s'ha carregat a memoria
// La cerca de l'usuari, escrita correctament, no troba res:
if (estat.equals("Préstec vençut")) { // false
// no hi entra mai
}
System.out.println(estat.length()); // 16, no 14Les dades continuen malament després de llegir-les. Es desaran malament, es compararan malament i es mostraran malament. La corrupció es propaga.
La codificació per defecte i per què no hi has de confiar
Fins a Java 17 inclòs, quan no especifiques charset, FileReader i FileWriter fan servir la codificació per defecte de la plataforma, que depèn del sistema operatiu i de la configuració regional:
| Entorn | Codificació per defecte històrica |
|---|---|
| Linux modern | UTF-8 |
| macOS | UTF-8 |
| Windows a Catalunya | Windows-1252 |
| Windows al Japó | Shift_JIS |
| Contenidor Docker minimalista | Sovint US-ASCII |
Això significa que el mateix programa llegint el mateix fitxer produeix resultats diferents en màquines diferents. És la causa del clàssic "al meu portàtil es veu bé i al servidor surt amb interrogants".
Java 18 (JEP 400) va canviar el valor per defecte de file.encoding a UTF-8 a totes les plataformes, cosa que és una gran millora. Però:
- Si treballes amb Java 17 —la versió de referència d'aquest curs i la més usada en producció— continues tenint el comportament antic.
- Encara que el teu projecte sigui Java 21, el codi que especifica el charset és autoexplicatiu i no depèn d'una propietat que algú pugui canviar amb
-Dfile.encoding.
Regla professional, sense excepcions: especifica sempre el charset explícitament en tota operació de text. No costa res i elimina una categoria sencera de fallades.
Com fer-ho bé
Des de Java 11, FileReader accepta un Charset al constructor:
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class LecturaAmbCharset {
public static void llegir(String cami) throws IOException {
// Java 11+: charset explicit al constructor
try (FileReader lector = new FileReader(cami, StandardCharsets.UTF_8)) {
int c;
while ((c = lector.read()) != -1) {
System.out.print((char) c);
}
}
}
}En versions anteriors —i continua sent la forma més general, la que veuràs a 07-03— es fa servir el pont InputStreamReader:
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
try (InputStreamReader lector =
new InputStreamReader(new FileInputStream(cami), StandardCharsets.UTF_8)) {
// ...
}I Scanner també l'accepta:
Les constants disponibles a java.nio.charset.StandardCharsets, garantides a tota JVM:
| Constant | Nom |
|---|---|
StandardCharsets.UTF_8 |
UTF-8. La teva opció per defecte sempre |
StandardCharsets.ISO_8859_1 |
Latin-1. Només per llegir fitxers antics |
StandardCharsets.US_ASCII |
ASCII de 7 bits |
StandardCharsets.UTF_16 |
UTF-16 amb marca d'ordre de bytes |
Fes servir les constants, no les cadenes.
new FileReader(cami, StandardCharsets.UTF_8)no pot fallar; la versió que accepta el nom com a cadena llançaUnsupportedEncodingExceptionsi escrius"UTF8","utf-8 "amb un espai o qualsevol altra errada. És un error de compilació convertit en error d'execució sense cap avantatge.
FileNotFoundException davant d'IOException
FileNotFoundException davant d'IOExceptionL'E/S és el territori natural de les excepcions comprovades del mòdul 6, i per un motiu que ara resulta evident: el fitxer és del món exterior. Pot no existir, pot haver canviat de permisos, el disc es pot omplir, algú pot desconnectar la unitat de xarxa. Res d'això no és una fallada del teu programa, i tot és previsible.
La jerarquia rellevant:
flowchart TD
T["Throwable"] --> E["Exception"]
E --> IOE["IOException<br/>(comprovada)"]
IOE --> FNF["FileNotFoundException"]
IOE --> EOF["EOFException"]
IOE --> UEE["UnsupportedEncodingException"]
IOE --> NSF["NoSuchFileException<br/>(NIO.2, 07-06)"]
IOE --> ADE["AccessDeniedException<br/>(NIO.2, 07-06)"]
IOE --> MFE["MalformedInputException<br/>(charset invalid)"]
style IOE fill:#ffe0b2
style FNF fill:#e3f2fd
Què significa cadascuna a la pràctica:
| Excepció | Quan es llança | Què se sol fer |
|---|---|---|
FileNotFoundException |
El fitxer no existeix, és un directori, o no hi ha permís de lectura | Degradar: fer servir un valor per defecte, o demanar el camí correcte |
IOException |
Fallada genèrica en llegir: disc, xarxa, dispositiu | Registrar i propagar. Poques vegades recuperable |
EOFException |
Final inesperat llegint dades binàries estructurades (07-03) | Fitxer truncat o corrupte: avortar la càrrega |
MalformedInputException |
Bytes que no formen caràcters vàlids en el charset donat | Charset equivocat. Revisar l'apartat 10 |
Nota enganyosa sobre el nom: FileNotFoundException no significa només "no existeix". També es llança si el camí apunta a un directori o si el fitxer existeix però no tens permís de lectura. Per això el missatge del sistema operatiu és tan valuós i no l'has de descartar mai.
L'ordre dels catch segueix la regla de 06-02, del que és específic al que és general:
import java.io.FileNotFoundException;
import java.io.IOException;
public void carregar(String cami) {
try (Scanner sc = new Scanner(new File(cami), StandardCharsets.UTF_8)) {
// ... llegir ...
} catch (FileNotFoundException e) {
// L'especific primer. Aquest cas es pot tractar de forma diferent.
LOG.log(Level.WARNING, "No s'ha trobat el fitxer: " + cami, e);
} catch (IOException e) {
// El general despres. A l'inreves NO COMPILA: codi inabastable.
LOG.log(Level.SEVERE, "Fallada d'E/S llegint " + cami, e);
}
}
- Degradació elegant: BiblioTech arrenca sense catàleg
Ho ajuntarem tot amb la primera peça real del mòdul. CarregadorCataleg llegeix un fitxer de text i construeix materials, i —això és l'important— si el fitxer no existeix, l'aplicació arrenca igualment amb el catàleg buit.
Aquest és exactament el criteri de 06-07 per distingir el que és recuperable del que és irrecuperable: degrada quan el servei reduït continuï sent correcte. Una biblioteca sense materials és una biblioteca buida, que és un estat perfectament vàlid i en el qual es poden donar d'alta materials. No hi ha res d'incorrecte. Molt diferent seria arrencar sense conèixer la tarifa de les multes: allà no es pot degradar, perquè cobraríem imports falsos.
El format del fitxer:
# Cataleg de BiblioTech - Nexus Software
# tipus;referencia;titol;autor;any
LLIBRE;978-0000000001;Java Eficac;Joshua Bloch;2018
LLIBRE;978-0000000002;Patrons de Disseny;Erich Gamma;1994
LLIBRE;978-0000000003;Refactoritzacio;Martin Fowler;1999I el carregador:
package com.nexussoftware.bibliotech.servei;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.ReferenciaDuplicadaException;
/**
* Carrega el cataleg de BiblioTech des d'un fitxer de text.
*
* Politica d'errors (06-07):
* - Fitxer absent -> DEGRADAR: cataleg buit i avis al log.
* - Linia amb format dolent-> DESCARTAR aquesta linia i seguir amb les altres.
* - Fallada d'E/S greu -> propagar com a CatalegNoAccessibleException.
*
* Aquesta versio fa servir Scanner per claredat. A 07-04 se substitueix per
* BufferedReader, que es el correcte per a fitxers de milers de linies.
*/
public class CarregadorCataleg {
private static final Logger LOG = Logger.getLogger(CarregadorCataleg.class.getName());
private static final String COMENTARI = "#";
private static final String SEPARADOR = ";";
private static final int CAMPS_LLIBRE = 5;
private final String cami;
private int liniesLlegides = 0;
private int materialsAlta = 0;
private int liniesDescartades = 0;
public CarregadorCataleg(String cami) {
this.cami = java.util.Objects.requireNonNull(cami, "El cami no pot ser nul");
}
/**
* Carrega el cataleg. NO llanca MAI per fitxer absent: degrada.
*
* @return un Cataleg, possiblement buit, mai null
*/
public Cataleg carregar() {
Cataleg cataleg = new Cataleg();
File fitxer = new File(cami);
// Avis util: registrar SEMPRE el cami absolut, no el relatiu (apartat 4)
LOG.config(() -> "Carregant cataleg des de " + fitxer.getAbsolutePath());
try (Scanner sc = new Scanner(fitxer, StandardCharsets.UTF_8)) {
while (sc.hasNextLine()) {
String linia = sc.nextLine();
liniesLlegides++;
processarLinia(cataleg, linia, liniesLlegides);
}
LOG.info(String.format(
"Cataleg carregat: %d materials de %d linies (%d descartades)",
materialsAlta, liniesLlegides, liniesDescartades));
} catch (FileNotFoundException e) {
// DEGRADACIO ELEGANT. No es una fallada de l'aplicacio: la primera
// vegada que s'executa BiblioTech, aquest fitxer encara no existeix.
// Nivell WARNING, no SEVERE: quan tot es SEVERE ningu no mira els
// errors de veritat (06-07).
LOG.log(Level.WARNING,
"No hi ha fitxer de cataleg a " + fitxer.getAbsolutePath()
+ "; s'arrenca amb el cataleg buit", e);
}
return cataleg;
}
/** Processa una linia. Les fallades de format descarten la linia, no avorten la carrega. */
private void processarLinia(Cataleg cataleg, String linia, int numero) {
String neta = linia.trim();
// Linies buides i comentaris: s'ignoren en silenci, son normals
if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
return;
}
String[] camps = neta.split(SEPARADOR);
if (camps.length != CAMPS_LLIBRE) {
descartar(numero, "s'esperaven " + CAMPS_LLIBRE
+ " camps i n'hi ha " + camps.length);
return;
}
if (!"LLIBRE".equalsIgnoreCase(camps[0].trim())) {
descartar(numero, "tipus desconegut '" + camps[0].trim() + "'");
return;
}
try {
String isbn = camps[1].trim();
String titol = camps[2].trim();
String autor = camps[3].trim();
int any = Integer.parseInt(camps[4].trim());
cataleg.registrar(new Llibre(titol, autor, isbn, any));
materialsAlta++;
} catch (NumberFormatException e) {
// L'any no era un numero. Es tradueix a un descart amb context (06-03).
descartar(numero, "l'any '" + camps[4].trim() + "' no es un numero");
} catch (ReferenciaDuplicadaException e) {
// Excepcio propia del modul 6: l'ISBN ja hi era. Es descarta la linia.
descartar(numero, e.getMessage());
}
}
private void descartar(int numero, String motiu) {
liniesDescartades++;
LOG.warning(() -> "Linia " + numero + " descartada: " + motiu);
}
public int getLiniesLlegides() { return liniesLlegides; }
public int getMaterialsAlta() { return materialsAlta; }
public int getLiniesDescartades() { return liniesDescartades; }
}I l'arrencada de l'aplicació queda així:
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.servei.CarregadorCataleg;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.infraestructura.ConfiguracioLog;
public class BiblioTechApp {
/** Cami per defecte; es pot canviar per argument de linia d'ordres. */
private static final String CAMI_CATALEG_DEFECTE = "dades/cataleg.txt";
public static void main(String[] args) {
ConfiguracioLog.inicialitzar(); // 06-07
String cami = (args.length > 0) ? args[0] : CAMI_CATALEG_DEFECTE;
Cataleg cataleg = new CarregadorCataleg(cami).carregar();
System.out.println("BiblioTech - Nexus Software");
System.out.println("Materials al cataleg: " + cataleg.mida());
new MenuBiblioTech(cataleg).executar();
}
}Les tres decisions de disseny que convé assenyalar:
- El camí arriba per argument, amb un valor per defecte. És l'estratègia recomanada de l'apartat 4: qui executa decideix on són les dades.
carregar()no retorna mainull. Retorna unCatalegbuit si no hi ha fitxer. És la regla que 06-07 va establir: elnullno és una forma de comunicar res.- Una línia dolenta no avorta el fitxer. Es descarta, es compta i es registra. Que un catàleg de deu mil línies no es carregui perquè la línia 4 200 té un any mal escrit seria una política pèssima. Això és l'"objecte resultat" de 06-07 aplicat a la importació, i a 07-04 es convertirà en un informe complet.
- Fitxers grans i per què no llegir-ho tot en memòria
Tanquem amb un advertiment de dimensionament. És temptador escriure això:
// PERILLOS amb fitxers grans
String tot = llegirFitxerSencerComACadena("dades/historic-prestecs.txt");I per a un fitxer de configuració de 2 KB és perfectament raonable. Però per a l'històric de préstecs de tres anys, no:
| Mida del fitxer | Memòria aproximada de l'String |
Viable amb heap de 512 MB |
|---|---|---|
| 10 KB | ~20 KB | Sí, sense pensar-hi |
| 5 MB | ~10 MB | Sí |
| 200 MB | ~400 MB | Al límit, amb risc |
| 2 GB | ~4 GB | OutOfMemoryError |
Per què el factor de dos: internament un String de Java 9 endavant desa un byte per caràcter quan tot el text és Latin-1 i dos bytes per caràcter tan bon punt apareix un caràcter fora d'aquest rang. Un sol emoji o un caràcter ciríl·lic en un fitxer de 2 GB duplica la memòria de l'String complet. I a més necessites el StringBuilder intermedi mentre el construeixes, així que al pic arribes a tenir el contingut dues vegades.
Un OutOfMemoryError és un Error, no una Exception: no es captura, no es recupera i tomba el procés, amb la salvetat de la frontera de main que vas veure a 06-07.
L'alternativa correcta és processar en flux (streaming): llegir una línia, processar-la, oblidar-la, llegir la següent. La memòria usada és la d'una línia, no la del fitxer, i el consum és constant encara que el fitxer tingui 50 GB:
// Esquema de processament en flux. La versio completa, a 07-04.
try (BufferedReader lector = new BufferedReader(
new FileReader(cami, StandardCharsets.UTF_8))) {
String linia;
while ((linia = lector.readLine()) != null) {
processar(linia); // es fa servir i es descarta
}
}La regla pràctica:
Carrega el fitxer sencer en memòria només si en coneixes la mida màxima i és petita. Configuració, plantilles, fitxers de pocs KB: endavant. Dades, registres, exportacions, qualsevol cosa que creixi amb l'ús: processa en flux.
Aquesta és exactament la raó de ser de BufferedReader, que és la lliçó 07-04.
Errors Comuns i Consells
- Fer servir camins relatius i no saber respecte a què. El clàssic "funciona a l'IDE i falla en producció". Imprimeix
System.getProperty("user.dir")tan bon punt tinguis dubtes i passa els camins per paràmetre. - Informar de l'error amb el camí relatiu.
No es troba 'dades/cataleg.txt'no serveix de res. Registra sempregetAbsolutePath(). - No especificar el charset. L'error més car del mòdul a llarg termini, perquè no falla: corromp en silenci.
StandardCharsets.UTF_8, sempre, en tota operació de text. - Confondre
çamb?.çsignifica que vas escriure UTF-8 i vas llegir Latin-1.?o�significa el contrari. Reconèixer el símptoma t'estalvia mitja hora cada vegada. - Fer servir cadenes en lloc de constants de charset.
"UTF8"compila i peta en execució.StandardCharsets.UTF_8no es pot escriure malament. - Declarar
char c = lector.read(). No compila, i per un bon motiu: cal el-1per al final de fitxer, que no cap en unchar. - Oblidar els parèntesis a
while ((c = lector.read()) != -1). Sense ells no compila. Amb ells, és el bucle idiomàtic que veuràs sempre. - Fer servir
append(bloc)en lloc d'append(bloc, 0, llegits). Afegeix brossa de la volta anterior a l'última lectura. Falla només al final del fitxer, que és el que ho fa difícil de diagnosticar. - Cridar
nextLine()sense comprovarhasNextLine().NoSuchElementException, no comprovada, en execució. - Barrejar
nextInt()inextLine()sense consumir el salt de línia. El mateix parany del mòdul 1, idèntic sobre fitxers. - Confiar en
nextDouble()sense fixar elLocale. En una màquina amb configuració catalana,0.25pot fallar.sc.useLocale(Locale.ROOT). - Llegir caràcter a caràcter un fitxer gran. Correcte i cent vegades més lent. Blocs o
BufferedReader(07-04). - Carregar en un
Stringun fitxer de mida desconeguda.OutOfMemoryError, que no es captura. Processa en flux. - Interpretar
length() == 0com a "no existeix". Un fitxer buit dona el mateix. Fes servirexists(). - Ignorar que
listFiles()pot retornarnull.NullPointerExceptionalfor. NIO.2 ho resol (07-06). - Consell: escriu una única classe que sàpiga llegir cada tipus de fitxer. Si tens
new FileReader(...)repartit per vint classes, canviar el charset o el format serà un rastreig. Concentra l'E/S a la capa que li correspon, com faCarregadorCataleg. - Consell: prova sempre amb un fitxer que contingui
ç,ài€. Un fitxer de prova només amb ASCII t'ocultarà totes les fallades de codificació fins que arribi a producció. - Consell: prova també amb el fitxer absent i amb el fitxer buit. Són els dos casos que més s'obliden i els dos que més passen, sobretot el primer dia de vida del sistema.
Exercicis
Exercici 1: diagnòstic de camins
Escriu una classe DiagnosticCami amb un mètode public static void diagnosticar(String cami) que imprimeixi un informe complet i útil sobre un camí, pensat perquè algú hi pugui resoldre un problema:
- El camí tal com s'ha donat i la seva forma absoluta.
- El directori de treball actual.
- Si existeix; i si no existeix, si existeix el seu directori pare (per distingir "falta el fitxer" de "el directori no és aquest").
- Si existeix: si és fitxer o directori, si és llegible, la seva mida i el nombre de línies.
- Un diagnòstic final en una frase amb la causa més probable.
Prova'l amb un camí correcte, un d'inexistent i un directori.
Exercici 2: detector de codificació
Escriu DetectorCodificacio amb un mètode public static void comparar(String cami) que llegeixi el mateix fitxer tres vegades —com a UTF-8, com a ISO-8859-1 i com a US-ASCII— i mostri les tres primeres línies de cada lectura, per veure l'efecte sobre els accents i les ce trencades.
Després, afegeix public static boolean semblaUtf8(String cami), que retorni true si el fitxer es descodifica sense cap caràcter de reemplaçament (\uFFFD) en llegir-lo com a UTF-8. Explica en un comentari per què aquesta comprovació detecta "això no és UTF-8" però no pot detectar "això és Latin-1 llegit com a UTF-8 a l'inrevés".
Prepara un fitxer de prova amb Préstec vençut, Refactorització i Reunió de març.
Exercici 3: carregador d'empleats amb informe
Amplia la idea de CarregadorCataleg als empleats. Donat un fitxer com aquest:
Escriu CarregadorEmpleats amb:
public ResultatCarrega carregar(), que retorni un objecte resultat (idea de 06-07) amb la llista d'empleats carregats i la llista d'errors, en lloc de llançar a la primera fallada.- Validació de cada línia: exactament 3 camps, identificador amb format
E-NNN, nom no buit i nombre de préstecs entre 0 iEmpleat.MAX_PRESTECS_SIMULTANIS. - Degradació si el fitxer no existeix: resultat buit amb un avís, sense excepció.
- Charset explícit i
try-with-resources. - Un
mainde demostració que imprimeixi l'informe ambprintf.
Afegeix al fitxer de prova una línia amb dos camps, una amb un identificador X-999 i una amb 9 préstecs, per comprovar que les tres es descarten amb el seu motiu i les bones es carreguen.
Solucions
Solució 1
package com.nexussoftware.bibliotech.util;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* Informe de diagnostic d'un cami.
*
* L'objectiu NO es saber si el fitxer existeix, sino poder explicar PER QUE
* no existeix, que es el que de veritat cal per arreglar-ho (06-04).
*/
public class DiagnosticCami {
public static void diagnosticar(String cami) {
File f = new File(cami);
System.out.println("========================================");
System.out.println("DIAGNOSTIC DE: " + cami);
System.out.println("========================================");
// 1. Cami donat i cami resolt
System.out.println(" Cami donat : " + f.getPath());
System.out.println(" Cami absolut : " + f.getAbsolutePath());
System.out.println(" Es absolut? : " + f.isAbsolute());
// 2. Context d'execucio: la peca que gairebe sempre falta
System.out.println(" Directori actual : " + System.getProperty("user.dir"));
// 3. Existencia, distingint fitxer de directori pare
boolean existeix = f.exists();
System.out.println(" Existeix? : " + existeix);
File pare = f.getAbsoluteFile().getParentFile();
boolean existeixPare = (pare != null && pare.exists());
System.out.println(" Directori pare : "
+ (pare == null ? "(cap)" : pare.getAbsolutePath())
+ (existeixPare ? " [existeix]" : " [NO existeix]"));
// 4. Detalls nomes si existeix
if (existeix) {
System.out.println(" Es fitxer? : " + f.isFile());
System.out.println(" Es directori? : " + f.isDirectory());
System.out.println(" Llegible? : " + f.canRead());
System.out.println(" Escrivible? : " + f.canWrite());
System.out.println(" Mida : " + f.length() + " bytes");
if (f.isFile() && f.canRead()) {
System.out.println(" Linies : " + comptarLinies(f));
}
}
// 5. Conclusio en una frase
System.out.println(" ----");
System.out.println(" CONCLUSIO: " + concloure(f, existeix, existeixPare));
System.out.println();
}
/** Compta linies de forma segura: si falla, ho diu en lloc de propagar-ho. */
private static String comptarLinies(File f) {
try (Scanner sc = new Scanner(f, StandardCharsets.UTF_8)) {
int n = 0;
while (sc.hasNextLine()) {
sc.nextLine();
n++;
}
return String.valueOf(n);
} catch (FileNotFoundException e) {
// Pot passar entre l'exists() i l'open(): es una condicio de
// cursa TOCTOU, exactament l'advertida a 06-07.
return "(no s'ha pogut llegir: " + e.getMessage() + ")";
}
}
private static String concloure(File f, boolean existeix, boolean existeixPare) {
if (existeix && f.isFile() && f.canRead()) {
return "El fitxer existeix i es pot llegir. Tot correcte.";
}
if (existeix && f.isDirectory()) {
return "El cami apunta a un DIRECTORI, no a un fitxer. "
+ "En obrir-lo es llancaria FileNotFoundException, "
+ "malgrat el que suggereix el seu nom.";
}
if (existeix && !f.canRead()) {
return "El fitxer existeix pero l'usuari que executa la JVM "
+ "no te permis de lectura. Revisa els permisos.";
}
if (!existeix && !existeixPare) {
return "No existeix ni el fitxer ni el seu directori. El mes probable "
+ "es que el cami relatiu s'estigui resolent des d'un altre "
+ "directori de treball del que s'esperava.";
}
return "El directori existeix pero el fitxer no. Comprova el nom "
+ "exacte, incloses les majuscules: a Linux distingeixen.";
}
public static void main(String[] args) {
diagnosticar("dades/cataleg.txt"); // correcte
diagnosticar("dades"); // un directori
diagnosticar("dades/no-existeix.txt"); // falta el fitxer
diagnosticar("altre/cami/dolent.txt"); // falta el directori
}
}El que ensenya aquest exercici: la diferència entre detectar una fallada i explicar-la. Els cinc casos de concloure corresponen a cinc causes diferents que l'API resumeix totes en un exists() a false. Distingir-les és el que converteix un missatge d'error en una solució. Fixa't també en el comentari sobre TOCTOU: entre comprovar exists() i obrir el fitxer pot passar qualsevol cosa, així que la comprovació prèvia no substitueix el catch.
Solució 2
package com.nexussoftware.bibliotech.util;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Mostra l'efecte de llegir el mateix fitxer amb codificacions diferents.
*/
public class DetectorCodificacio {
/** Caràcter de reemplaçament que insereix el descodificador davant de bytes invàlids. */
private static final char REEMPLACAMENT = '\uFFFD';
public static void comparar(String cami) {
Charset[] candidats = {
StandardCharsets.UTF_8,
StandardCharsets.ISO_8859_1,
StandardCharsets.US_ASCII
};
for (Charset cs : candidats) {
System.out.println("--- Llegit com a " + cs.name() + " ---");
try {
String[] linies = primeresLinies(cami, cs, 3);
for (String l : linies) {
System.out.println(" " + l);
}
} catch (IOException e) {
System.out.println(" ERROR: " + e.getMessage());
}
System.out.println();
}
}
/** Llegeix les n primeres linies amb el charset donat. */
private static String[] primeresLinies(String cami, Charset cs, int n) throws IOException {
String[] resultat = new String[n];
int index = 0;
StringBuilder actual = new StringBuilder();
try (FileReader lector = new FileReader(cami, cs)) {
int c;
while (index < n && (c = lector.read()) != -1) {
if (c == '\n') {
resultat[index++] = actual.toString();
actual.setLength(0);
} else if (c != '\r') {
actual.append((char) c);
}
}
}
// Ultima linia sense salt final
if (index < n && actual.length() > 0) {
resultat[index++] = actual.toString();
}
for (int i = index; i < n; i++) {
resultat[i] = "(no hi ha mes linies)";
}
return resultat;
}
/**
* Es descodifica el fitxer com a UTF-8 sense caracters de reemplacament?
*
* PER QUE FUNCIONA EN UN SENTIT: UTF-8 te una gramatica estricta.
* El byte 0xE7 (la 'ç' de Latin-1) anuncia una sequencia de 3 bytes que
* en un fitxer Latin-1 no vindra ben formada, aixi que el descodificador
* insereix \uFFFD. Detectem "aixo NO es UTF-8".
*
* PER QUE NO FUNCIONA EN L'ALTRE: un fitxer UTF-8 llegit com a Latin-1
* NO produeix MAI reemplacaments, perque en Latin-1 els 256 bytes possibles son
* caracters valids. Simplement surten els caracters equivocats
* ("vençut"). Un descodificador no pot saber que allo esta malament: per a ell
* es text perfectament legal. Per aixo NO EXISTEIX deteccio fiable de
* codificacio, i per aixo cal ESPECIFICAR-LA sempre.
*/
public static boolean semblaUtf8(String cami) throws IOException {
try (FileReader lector = new FileReader(cami, StandardCharsets.UTF_8)) {
int c;
while ((c = lector.read()) != -1) {
if (c == REEMPLACAMENT) {
return false;
}
}
}
return true;
}
public static void main(String[] args) throws IOException {
String cami = "dades/prova-accents.txt";
comparar(cami);
System.out.println("Sembla UTF-8? " + semblaUtf8(cami));
}
}Amb un fitxer de prova desat en UTF-8 que contingui Préstec vençut, Refactorització i Reunió de març, la sortida és:
--- Llegit com a UTF-8 ---
Préstec vençut
Refactorització
Reunió de març
--- Llegit com a ISO-8859-1 ---
Préstec vençut
Refactorització
Reunió de març
--- Llegit com a US-ASCII ---
Pr??stec ven??ut
Refactoritzaci??
Reuni?? de mar??
Sembla UTF-8? trueLa conclusió pràctica és la del comentari: la detecció de codificació és fonamentalment impossible en el cas general. Els editors de text l'endevinen amb heurístiques i de vegades s'equivoquen. Per això cap quantitat de perspicàcia al codi no substitueix la regla simple: especifica sempre el charset, en llegir i en escriure.
Solució 3
package com.nexussoftware.bibliotech.servei;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.Empleat;
/**
* Carrega empleats des d'un fitxer de text acumulant errors en lloc
* d'avortar al primer (objecte resultat, 06-07).
*/
public class CarregadorEmpleats {
private static final Logger LOG = Logger.getLogger(CarregadorEmpleats.class.getName());
private static final String SEPARADOR = ";";
private static final String COMENTARI = "#";
private static final int NUM_CAMPS = 3;
private static final String PATRO_ID = "E-\\d{3}";
private final String cami;
public CarregadorEmpleats(String cami) {
this.cami = Objects.requireNonNull(cami, "El cami no pot ser nul");
}
/** Resultat de la carrega: el que ha anat be i el que ha anat malament, junt. */
public static class ResultatCarrega {
private final List<Empleat> empleats = new ArrayList<>();
private final List<String> errors = new ArrayList<>();
private boolean fitxerTrobat = true;
void afegir(Empleat e) { empleats.add(e); }
void error(int linia, String m) { errors.add("Linia " + linia + ": " + m); }
void marcarFitxerAbsent() { fitxerTrobat = false; }
public List<Empleat> getEmpleats() { return List.copyOf(empleats); }
public List<String> getErrors() { return List.copyOf(errors); }
public boolean hiHaErrors() { return !errors.isEmpty(); }
public boolean fitxerTrobat() { return fitxerTrobat; }
public int total() { return empleats.size() + errors.size(); }
}
public ResultatCarrega carregar() {
ResultatCarrega resultat = new ResultatCarrega();
File fitxer = new File(cami);
try (Scanner sc = new Scanner(fitxer, StandardCharsets.UTF_8)) {
int numero = 0;
while (sc.hasNextLine()) {
numero++;
processar(sc.nextLine(), numero, resultat);
}
} catch (FileNotFoundException e) {
// DEGRADACIO: resultat buit, avis, i sense excepcio cap amunt
resultat.marcarFitxerAbsent();
LOG.log(Level.WARNING, "No hi ha fitxer d'empleats a "
+ fitxer.getAbsolutePath() + "; s'arrenca sense empleats", e);
}
return resultat;
}
private void processar(String linia, int numero, ResultatCarrega r) {
String neta = linia.trim();
if (neta.isEmpty() || neta.startsWith(COMENTARI)) {
return; // normal, no es un error
}
// -1 a split conserva els camps buits del final: "E-001;;" dona 3 camps.
// Sense el -1 en donaria 1, i el missatge d'error seria enganyos.
String[] camps = neta.split(SEPARADOR, -1);
if (camps.length != NUM_CAMPS) {
r.error(numero, "s'esperaven " + NUM_CAMPS + " camps i n'hi ha " + camps.length);
return;
}
String id = camps[0].trim();
String nom = camps[1].trim();
String actius = camps[2].trim();
if (!id.matches(PATRO_ID)) {
r.error(numero, "l'identificador '" + id + "' no te el format E-NNN");
return;
}
if (nom.isEmpty()) {
r.error(numero, "el nom es buit");
return;
}
int prestecs;
try {
prestecs = Integer.parseInt(actius);
} catch (NumberFormatException e) {
r.error(numero, "'" + actius + "' no es un numero de prestecs valid");
return;
}
if (prestecs < 0 || prestecs > Empleat.MAX_PRESTECS_SIMULTANIS) {
r.error(numero, "prestecs actius fora de rang (0.."
+ Empleat.MAX_PRESTECS_SIMULTANIS + "): " + prestecs);
return;
}
Empleat empleat = new Empleat(nom, id);
for (int i = 0; i < prestecs; i++) {
empleat.registrarPrestec(); // restaura l'estat desat
}
r.afegir(empleat);
}
public static void main(String[] args) {
ResultatCarrega r = new CarregadorEmpleats("dades/empleats.txt").carregar();
System.out.println("=== CARREGA D'EMPLEATS ===");
if (!r.fitxerTrobat()) {
System.out.println(" (no hi havia fitxer: s'arrenca sense empleats)");
return;
}
System.out.printf(" Linies processades: %d%n", r.total());
System.out.printf(" Empleats carregats: %d%n", r.getEmpleats().size());
System.out.printf(" Linies amb error : %d%n", r.getErrors().size());
if (!r.getEmpleats().isEmpty()) {
System.out.println(" --- Carregats ---");
for (Empleat e : r.getEmpleats()) {
System.out.printf(" %-8s %s%n", e.getIdentificador(), e.getNom());
}
}
if (r.hiHaErrors()) {
System.out.println(" --- Descartats ---");
for (String error : r.getErrors()) {
System.out.println(" " + error);
}
}
}
}Amb aquest fitxer de prova:
# identificador;nom;prestecs_actius
E-001;Marta Ruiz;2
E-002;Diego Alonso;0
E-003;Nuria Vidal;1
E-004;Nomes dos camps
X-999;Format dolent;1
E-005;Massa;9La sortida és:
=== CARREGA D'EMPLEATS ===
Linies processades: 6
Empleats carregats: 3
Linies amb error : 3
--- Carregats ---
E-001 Marta Ruiz
E-002 Diego Alonso
E-003 Nuria Vidal
--- Descartats ---
Linia 5: s'esperaven 3 camps i n'hi ha 2
Linia 6: l'identificador 'X-999' no te el format E-NNN
Linia 7: prestecs actius fora de rang (0..3): 9Les tres decisions que val la pena assenyalar:
- L'objecte resultat en lloc d'una excepció per línia. Qui importa un fitxer vol veure tots els problemes de cop per corregir-los i tornar-ho a intentar, no descobrir-los d'un en un. És exactament el cas d'ús que 06-07 va plantejar per a l'objecte resultat.
split(SEPARADOR, -1). Sense aquest-1,splitdescarta els camps buits finals, així queE-001;;donaria 1 camp en lloc de 3 i el missatge d'error seria incorrecte. És un parany clàssic que reapareixerà a 07-07.- El fitxer absent no és un error de línia. Es marca a part, perquè són dues situacions diferents: "no hi ha dades" i "hi ha dades mal escrites". Confondre-les produeix informes inútils.
Conclusió
Has posat els fonaments de la persistència de BiblioTech.
Entens què és un fitxer de veritat: una seqüència de bytes amb un nom, sense tipus ni estructura, davant d'un objecte en memòria amb camps i referències. Saps que passar d'un a l'altre exigeix una decisió de format explícita —un material per línia, camps separats per ;— i que aquesta decisió és el germen del CSV de 07-07. I coneixes el recorregut complet de la dada fins al disc, amb la conclusió que governa tot el mòdul: la crida al sistema és l'operació cara, i tot el disseny de l'E/S existeix per reduir-ne el nombre.
Saps localitzar un fitxer: camins absoluts i relatius, el paper de user.dir com a origen dels relatius, i per què el mateix programa "no troba" el mateix fitxer segons des d'on es llanci. Tens les cinc estratègies professionals per no dependre d'això, amb la recomanació de passar el camí per paràmetre, i la regla de registrar sempre el camí absolut als missatges d'error. Coneixes File.separator davant de File.pathSeparator i per què és millor compondre camins que concatenar cadenes.
Coneixes java.io.File —exists, isFile, canRead, length, getAbsolutePath— i, sobretot, el defecte que la va condemnar: els boolean muts que diuen que alguna cosa ha fallat sense dir per què, exactament l'antipatró que el mòdul 6 va eradicar. Saps que 07-06 la substitueix per Path i Files, i que la conversió entre totes dues és un toPath().
Saps llegir amb FileReader caràcter a caràcter, amb l'int de retorn i el -1 de final de fitxer, i per què aquest bucle és dos ordres de magnitud més lent del que cal. Coneixes la lectura per blocs amb read(char[]) i els seus dos paranys: el valor de retorn que pot ser menor que l'array, i l'append(bloc, 0, llegits) que evita arrossegar brossa. I saps llegir amb Scanner sobre fitxer, amb hasNextLine/nextLine, l'anàlisi de tipus, useDelimiter amb expressions regulars, i les seves dues sorpreses: el Locale que canvia el separador decimal i el nextInt() que no consumeix el salt de línia.
I tens l'apartat que més incidències reals evita: la codificació. Saps què és UTF-8, per què ç ocupa dos bytes, què significa exactament veure ç i què significa veure �, per què la codificació per defecte de la plataforma és una bomba de rellotgeria entre màquines, què va canviar Java 18 i per què això no t'eximeix d'especificar-la. I saps que detectar la codificació és impossible en el cas general, cosa que converteix la regla en absoluta: StandardCharsets.UTF_8, explícit, sempre.
BiblioTech ja llegeix. CarregadorCataleg construeix el catàleg des d'un fitxer de text, descarta les línies mal formades comptant-les i registrant-les en lloc d'avortar la càrrega sencera, tradueix NumberFormatException i ReferenciaDuplicadaException a descarts amb context, i —el més important— degrada elegantment: si no hi ha fitxer de catàleg, arrenca amb el catàleg buit i un WARNING, perquè una biblioteca buida és un estat correcte. És la política d'errors de 06-07 aplicada a l'E/S des de la primera línia de codi.
Queda la meitat que falta, i és la que fa por. Llegir és segur; escriure destrueix. A la lliçó 07-02, Escriptura de fitxers, veuràs FileWriter i el paràmetre append que decideix entre afegir al final i esborrar el fitxer sencer, que és l'error més car de tot el mòdul; PrintWriter amb el printf que ja fas servir des del mòdul 1; la memòria intermèdia i el buidatge, i per què el que has escrit pot no ser al disc fins que tanquis; el patró d'escriptura atòmica —escriure a un temporal i reanomenar— que impedeix deixar un fitxer a mitges quan alguna cosa falla pel mig, germà directe de la compensació al finally que ja saps escriure; i el separador de línia del sistema, amb el motiu pel qual \n no sempre n'hi ha prou. En acabar-la, ExportadorCataleg deixarà de ser una classe que sap tancar-se però no escriure, i BiblioTech desarà per primera vegada el que ha fet.
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
