Portes cinc lliçons deixant comentaris del tipus "a 07-06 això es fa bé". Els has posat a la creació de directoris, al reanomenat atòmic, a la rotació de còpies de seguretat, al listFiles() que pot retornar null i a cada boolean mut que deia que alguna cosa havia fallat sense dir per què.
Aquesta és aquella lliçó. NIO.2 —New I/O 2, introduïda a Java 7 amb la JSR 203— és la substitució completa de java.io.File, i no és un retoc cosmètic: és un redisseny que corregeix errors de fons d'una API de 1996. Les seves dues peces centrals són Path, que representa un camí amb tota la seva aritmètica, i Files, una classe d'utilitat amb més de cinquanta mètodes estàtics que fan tot el que fins ara has escrit a mà.
En acabar, BiblioTech tindrà la seva capa de persistència migrada del tot: crearà el seu directori dades/ si no existeix, escriurà de forma veritablement atòmica en una línia, mantindrà còpies de seguretat rotatives i localitzarà tots els seus informes recorrent un arbre de carpetes.
Amb
try-with-resourcesa tot, com sempre. I amb un advertiment nou i molt concret:Files.lines(),Files.walk()iFiles.list()retornen fluxos que S'HAN de tancar. És l'error més freqüent d'aquesta API i s'explica a l'apartat 8.
Contingut
- Per què existeix NIO.2: els defectes de
File Path: construir i consultar camins- Aritmètica de camins:
resolve,relativize,normalize Files: existència, creació i esborrat- Copiar i moure, amb l'escriptura atòmica de veritat
- Lectura i escriptura d'alt nivell
StandardOpenOption: dir el que vols dirFiles.lines,listiwalk: fluxos que es tanquen- Recórrer arbres de directoris
- Atributs de fitxer i permisos
- Fitxers temporals i
WatchService - Interoperabilitat amb
File - BiblioTech migra la seva persistència
- Errors Comuns i Consells
- Exercicis
- Per què existeix NIO.2: els defectes de
File
Filejava.io.File és de Java 1.0 i arrossega decisions de disseny d'una època en què Java havia de funcionar en sistemes molt diferents amb un denominador comú molt petit. Els seus problemes no són d'estil: són funcionals.
Defecte 1: els boolean muts.
File f = new File("/dades/cataleg.txt");
if (!f.delete()) {
// Per que? No existia? No tinc permis? Estava obert?
// Era un directori no buit? L'API no ho diu.
}És l'antipatró que el mòdul 6 va dedicar set lliçons a eradicar: comunicar una fallada sense dir quina. NIO.2 llança excepcions específiques.
Defecte 2: null com a valor de retorn.
File[] fills = directori.listFiles();
for (File f : fills) { // NullPointerException si el directori no existeix
// ...
}listFiles() retorna null si el camí no és un directori o si falla l'accés. Una altra fallada silenciosa convertida en excepció a destemps.
Defecte 3: no coneix els enllaços simbòlics. File no distingeix un fitxer d'un enllaç que hi apunta. En sistemes Unix això és una limitació seriosa, i una font de bucles infinits en recórrer arbres.
Defecte 4: metadades pobres. Només lastModified(). No hi ha data de creació, ni de darrer accés, ni propietari, ni permisos POSIX, ni atributs estesos.
Defecte 5: renameTo no és fiable. El seu comportament depèn del sistema operatiu: a Windows falla si el destí existeix, a Unix no; pot fallar entre sistemes de fitxers diferents; i retorna un boolean mut.
Defecte 6: no és extensible. No hi ha manera que File treballi amb un sistema de fitxers que no sigui el del sistema operatiu. NIO.2 sí: pot recórrer l'interior d'un .zip com si fos un directori.
Defecte 7: no hi ha recorregut eficient d'arbres. Recórrer un directori gran amb listFiles() carrega l'array sencer en memòria.
La comparació completa:
| Aspecte | java.io.File |
Path + Files (NIO.2) |
|---|---|---|
| Errors | boolean mut |
Excepcions específiques amb missatge |
| Llistar directori | File[], pot ser null |
Stream<Path> o DirectoryStream |
| Enllaços simbòlics | No els coneix | Els distingeix i els segueix o els omet a voluntat |
| Metadades | Només lastModified |
Creació, accés, propietari, permisos, ACL |
| Copiar un fitxer | No existeix: bucle a mà | Files.copy |
| Moure atòmicament | renameTo, poc fiable |
Files.move amb ATOMIC_MOVE |
| Crear directoris | mkdirs(), boolean mut |
Files.createDirectories |
| Recórrer un arbre | Recursió a mà | Files.walk, walkFileTree |
| Llegir un fitxer sencer | Bucle a mà | Files.readString, readAllLines |
| Vigilar canvis | No existeix | WatchService |
| Sistemes de fitxers alternatius | No | Sí: ZIP, en memòria, remots |
| Aritmètica de camins | Concatenar cadenes | resolve, relativize, normalize |
Un exemple comparat que resumeix el canvi:
// java.io: copiar un fitxer
try (InputStream in = new BufferedInputStream(new FileInputStream(origen));
OutputStream out = new BufferedOutputStream(new FileOutputStream(desti))) {
byte[] bloc = new byte[8192];
int llegits;
while ((llegits = in.read(bloc)) != -1) {
out.write(bloc, 0, llegits);
}
}
// NIO.2: el mateix
Files.copy(origen, desti, StandardCopyOption.REPLACE_EXISTING);Vuit línies davant d'una, i la de NIO.2 fa servir la primitiva més eficient que ofereixi el sistema operatiu, que en molts casos ni tan sols passa les dades pel teu procés.
Regla professional: en codi nou,
PathiFiles. Sempre. ConeixFileper llegir codi antic i per a les API que encara el demanen; escriu-lo només quan no hi hagi alternativa.
Path: construir i consultar camins
Path: construir i consultar caminsPath representa un camí, no un fitxer. Com File, pot apuntar a alguna cosa que no existeix: és una seqüència de noms, no un recurs.
import java.nio.file.Path;
import java.nio.file.Paths;
public class ConstruirCamins {
public static void main(String[] args) {
// Java 11+: la forma recomanada
Path p1 = Path.of("dades", "cataleg.txt");
// Java 7-10: equivalent, encara molt comu en codi existent
Path p2 = Paths.get("dades", "cataleg.txt");
// Cami complet en una cadena
Path p3 = Path.of("dades/informes/2026/gener.txt");
// Cami absolut
Path p4 = Path.of("/home/marta/bibliotech/dades/cataleg.txt");
// Des d'un URI
Path p5 = Path.of(java.net.URI.create("file:///home/marta/cataleg.txt"));
System.out.println(p1); // dades/cataleg.txt
System.out.println(p1.equals(p2)); // true
}
}Fes servir Path.of en codi nou. Paths.get fa exactament el mateix i continua funcionant; Path.of és de Java 11 i és la forma preferida.
Fixa't en una virtut del constructor de diversos arguments: Path.of("dades", "cataleg.txt") fa servir el separador correcte del sistema automàticament. S'ha acabat el File.separator i la concatenació de cadenes de 07-01.
Els mètodes de consulta:
import java.nio.file.Path;
public class ConsultarCamins {
public static void main(String[] args) {
Path p = Path.of("/home/marta/bibliotech/dades/cataleg.txt");
System.out.println("Cami complet : " + p);
System.out.println("Nom : " + p.getFileName()); // cataleg.txt
System.out.println("Directori : " + p.getParent()); // .../dades
System.out.println("Arrel : " + p.getRoot()); // /
System.out.println("Es absolut? : " + p.isAbsolute()); // true
System.out.println("Segments : " + p.getNameCount()); // 5
// Iterar els segments: Path es Iterable<Path>
System.out.println(" Recorregut:");
for (Path segment : p) {
System.out.println(" " + segment);
}
// Acces per index
System.out.println("Segment 0 : " + p.getName(0)); // home
System.out.println("Segment 3 : " + p.getName(3)); // dades
// Subcami [inici, fi)
System.out.println("Subcami 1..3 : " + p.subpath(1, 3)); // marta/bibliotech
// Comprovacions
System.out.println("Comenca per /home? " + p.startsWith("/home")); // true
System.out.println("Acaba en .txt? "
+ p.getFileName().toString().endsWith(".txt")); // true
}
}| Mètode | Retorna | Exemple amb /home/marta/dades/cataleg.txt |
|---|---|---|
getFileName() |
L'últim segment | cataleg.txt |
getParent() |
El camí sense l'últim segment | /home/marta/dades |
getRoot() |
L'arrel, o null si és relatiu |
/ |
getNameCount() |
Nombre de segments | 4 |
getName(i) |
El segment i | getName(0) → home |
subpath(a, b) |
Els segments d'a a b−1 | subpath(0,2) → home/marta |
isAbsolute() |
Si parteix de l'arrel | true |
toAbsolutePath() |
La versió absoluta, contra user.dir |
— |
toRealPath() |
Absolut, normalitzat i amb enllaços resolts. Llança IOException |
— |
startsWith / endsWith |
Comparació per segments, no per caràcters | — |
Un detall important que es falla sovint: startsWith compara segments complets, no prefixos de text:
Path p = Path.of("/home/marta/dades");
p.startsWith("/home/mar"); // false: "mar" no es un segment complet
p.startsWith("/home/marta"); // trueÉs el comportament correcte —evita que /home/martarodriguez sembli estar dins de /home/marta—, i és una comprovació de seguretat que amb cadenes es fa malament constantment.
- Aritmètica de camins:
resolve, relativize, normalize
resolve, relativize, normalizeAquí hi ha un dels guanys més grans de Path: operacions de camins ben definides, sense concatenar cadenes.
resolve: combinar camins
Path base = Path.of("/home/marta/bibliotech");
base.resolve("dades/cataleg.txt");
// /home/marta/bibliotech/dades/cataleg.txt
base.resolve("/etc/config");
// /etc/config <-- si l'argument es ABSOLUT, substitueix la base
base.resolve("");
// /home/marta/bibliotechLa regla: si l'argument és absolut, es retorna tal qual; si és relatiu, s'afegeix. Aquest comportament és deliberat i evita crear camins sense sentit com /home/marta/etc/config.
resolveSibling: germà al mateix directori
Path cataleg = Path.of("/home/marta/bibliotech/dades/cataleg.txt");
cataleg.resolveSibling("cataleg.txt.tmp");
// /home/marta/bibliotech/dades/cataleg.txt.tmp
cataleg.resolveSibling("cataleg.bak");
// /home/marta/bibliotech/dades/cataleg.bakEquival a getParent().resolve(...) però és més clar i funciona encara que no hi hagi pare. És la forma correcta de construir el fitxer temporal de l'escriptura atòmica que vas escriure a mà a 07-02.
relativize: el camí d'A a B
Path base = Path.of("/home/marta/bibliotech");
Path informe = Path.of("/home/marta/bibliotech/dades/informes/gener.txt");
base.relativize(informe);
// dades/informes/gener.txt
informe.relativize(base);
// ../../..Útil per mostrar camins curts a l'usuari o per desar camins portables en un fitxer de configuració. La seva restricció: tots dos camins han de ser tots dos absoluts o tots dos relatius; si no, llança IllegalArgumentException.
normalize: netejar . i ..
Path lleig = Path.of("/home/marta/./bibliotech/../bibliotech/dades/../dades/cataleg.txt");
lleig.normalize();
// /home/marta/bibliotech/dades/cataleg.txtnormalize és purament sintàctic: no consulta el sistema de fitxers. Si hi ha enllaços simbòlics pel mig, el resultat pot no ser el camí real. Per a això hi ha toRealPath(), que sí que consulta el disc i llança IOException si el camí no existeix.
I un ús de normalize que va més enllà de l'estètica, i que convé conèixer:
/**
* Comprova que un cami demanat per l'usuari queda DINS del directori
* de dades.
*
* Sense aquesta comprovacio, un nom com "../../etc/passwd" permetria sortir
* del directori permes. Es la vulnerabilitat coneguda com a "path
* traversal", i es de les mes frequents en aplicacions que accepten noms
* de fitxer de l'usuari. La seguretat d'aplicacions es tracta a 12-07.
*/
public static Path resoldreSegur(Path directoriBase, String nomDemanat) {
Path base = directoriBase.toAbsolutePath().normalize();
Path candidat = base.resolve(nomDemanat).normalize();
if (!candidat.startsWith(base)) {
throw new IllegalArgumentException(
"Cami fora del directori permes: " + nomDemanat);
}
return candidat;
}Les dues peces són normalize() —que resol els .. abans de comparar— i startsWith() —que compara per segments complets—. Amb concatenació de cadenes, aquesta comprovació es fa malament gairebé sempre.
Taula resum:
| Operació | Què fa | Consulta el disc |
|---|---|---|
resolve(altre) |
Combina; si altre és absolut, el retorna |
No |
resolveSibling(nom) |
Germà al mateix directori | No |
relativize(altre) |
Camí relatiu d'aquest a altre |
No |
normalize() |
Elimina . i .. sintàcticament |
No |
toAbsolutePath() |
Absolut contra user.dir, sense normalitzar |
No |
toRealPath() |
Absolut, normalitzat i amb enllaços resolts | Sí, llança IOException |
Files: existència, creació i esborrat
Files: existència, creació i esborratFiles és una classe d'utilitat amb mètodes estàtics. Tot el que File feia amb mètodes d'instància, aquí es fa passant el Path.
Existència
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.LinkOption;
Path p = Path.of("dades/cataleg.txt");
Files.exists(p); // true si existeix i es pot comprovar
Files.notExists(p); // true si NO existeix i es pot comprovar
Files.isRegularFile(p); // fitxer normal (no directori, no enllac)
Files.isDirectory(p);
Files.isSymbolicLink(p);
Files.isReadable(p);
Files.isWritable(p);
Files.isExecutable(p);
Files.isHidden(p); // llanca IOException
// Sense seguir enllacos simbolics
Files.exists(p, LinkOption.NOFOLLOW_LINKS);I aquí hi ha el detall que més desconcerta de tota l'API:
exists()inotExists()NO són oposats. Tots dos poden retornarfalsealhora.
Com és possible: tots dos retornen false quan no es pot determinar si el fitxer existeix, típicament per falta de permisos sobre el directori que el conté. Hi ha tres estats, no dos:
| Situació | exists() |
notExists() |
|---|---|---|
| El fitxer existeix i es pot comprovar | true |
false |
| El fitxer no existeix i es pot comprovar | false |
true |
| No es pot determinar (sense permís) | false |
false |
Per això !Files.exists(p) no significa "no existeix": significa "no existeix, o no ho puc saber". Si la distinció importa —i en codi de seguretat importa—, fes servir notExists() explícitament.
I l'advertiment de 06-07, que aquí continua vigent: comprovar l'existència abans d'operar és una condició de cursa (TOCTOU). Entre la comprovació i l'operació, un altre procés pot crear o esborrar el fitxer. La comprovació prèvia és una optimització o una forma de donar un missatge millor; l'excepció continua sent obligatòria.
Creació
// Un fitxer buit. Llanca FileAlreadyExistsException si ja existeix.
Files.createFile(Path.of("dades/nou.txt"));
// Un directori. Llanca NoSuchFileException si falta el pare.
Files.createDirectory(Path.of("dades"));
// Un directori I TOTS ELS SEUS PARES. No falla si ja existeixen.
Files.createDirectories(Path.of("dades/informes/2026/gener"));createDirectories és el que faràs servir gairebé sempre, i substitueix el mkdirs() amb boolean mut que vas escriure a 07-02:
// java.io (07-02)
File pare = desti.getAbsoluteFile().getParentFile();
if (pare != null && !pare.exists() && !pare.mkdirs()) {
throw new IOException("No s'ha pogut crear el directori " + pare.getAbsolutePath());
}
// NIO.2: una linia, idempotent, i amb excepcio informativa si falla
Files.createDirectories(desti.getParent());Fixa't que és idempotent: si el directori ja existeix, no fa res i no falla. Això elimina la comprovació prèvia.
Esborrat
// Esborra. Llanca NoSuchFileException si no existeix.
Files.delete(p);
// Esborra si existeix. Retorna true si ha esborrat alguna cosa. NO llanca.
boolean esborrat = Files.deleteIfExists(p);La diferència importa:
| Mètode | Si no existeix | Quan fer-lo servir |
|---|---|---|
delete |
NoSuchFileException |
Quan la seva absència és un error que cal saber |
deleteIfExists |
Retorna false, sense excepció |
Neteja: tant és si hi era o no |
I les excepcions que llança delete, que és on es veu el guany sobre el boolean mut:
| Excepció | Significa |
|---|---|
NoSuchFileException |
No existeix |
DirectoryNotEmptyException |
És un directori amb contingut |
AccessDeniedException |
Sense permís |
IOException |
Una altra fallada, amb missatge del sistema |
Compara els dos codis:
// java.io: saps que ha fallat, no per que
if (!fitxer.delete()) {
System.err.println("No s'ha pogut esborrar");
}
// NIO.2: saps exactament que ha passat i pots actuar en consequencia
try {
Files.delete(cami);
} catch (NoSuchFileException e) {
LOG.fine(() -> "Ja no hi era: " + cami); // no es un problema
} catch (DirectoryNotEmptyException e) {
LOG.warning(() -> "El directori te contingut: " + cami);
} catch (AccessDeniedException e) {
LOG.severe(() -> "Sense permis per esborrar " + cami); // aixo si que cal mirar-ho
} catch (IOException e) {
LOG.log(Level.SEVERE, "Fallada esborrant " + cami, e);
}Això és exactament el que 06-04 demanava: l'excepció ha de transportar el que qui la llegeix necessita per decidir.
- Copiar i moure, amb l'escriptura atòmica de veritat
Files.copy
import java.nio.file.StandardCopyOption;
// Copia. Llanca FileAlreadyExistsException si el desti existeix.
Files.copy(origen, desti);
// Sobreescriu el desti
Files.copy(origen, desti, StandardCopyOption.REPLACE_EXISTING);
// Conserva data de modificacio, propietari i permisos
Files.copy(origen, desti,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
// Des d'un InputStream a un fitxer (07-03)
Files.copy(entrada, desti, StandardCopyOption.REPLACE_EXISTING);
// Des d'un fitxer a un OutputStream
Files.copy(origen, sortida);Les dues últimes sobrecàrregues són molt útils: connecten el món dels fluxos de 07-03 amb el de NIO.2 sense escriure cap bucle.
Un avís: Files.copy sobre un directori copia només el directori, buit. No és recursiu. Per copiar un arbre cal recórrer-lo, i això és l'apartat 9.
Files.move i l'ATOMIC_MOVE
Aquí arriba el moment promès a 07-02:
// Mou o reanomena
Files.move(origen, desti);
// Sobreescrivint el desti
Files.move(origen, desti, StandardCopyOption.REPLACE_EXISTING);
// ATOMIC: o hi es sencer o no hi es. Mai a mitges.
Files.move(temporal, desti,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);ATOMIC_MOVE és el que feia falta. Compara amb la implementació de 07-02:
// 07-02, amb java.io: DUES operacions, amb un buit perillos entre elles
if (desti.exists() && !desti.delete()) { // 1. esborrar
return false;
}
return temporal.renameTo(desti); // 2. reanomenar
// Entre 1 i 2, el fitxer desti NO EXISTEIX. Si el proces mor alla,
// et quedes sense res. I un altre proces que llegeixi en aquell instant
// no troba el fitxer.
// NIO.2: UNA operacio atomica. No hi ha buit.
Files.move(temporal, desti, REPLACE_EXISTING, ATOMIC_MOVE);Amb ATOMIC_MOVE, no existeix cap instant en què el destí no hi sigui. Un lector concurrent veu la versió antiga completa o la nova completa. Això és el que significa "atòmic" de veritat.
Els seus dos límits, que cal conèixer:
- Només funciona dins del mateix sistema de fitxers. Entre particions o unitats diferents llança
AtomicMoveNotSupportedException, perquè moure-hi implica copiar i esborrar. Per això el fitxer temporal s'ha de crear al mateix directori que el destí, no a/tmp. REPLACE_EXISTINGambATOMIC_MOVEestà suportat als sistemes habituals, però la combinació exacta depèn del sistema de fitxers.
I així queda l'escriptura atòmica completa, que substitueix les quaranta línies d'EscripturaAtomica de 07-02:
package com.nexussoftware.bibliotech.infraestructura;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.logging.Logger;
/**
* Escriptura atomica amb NIO.2.
*
* Substitueix la versio de 07-02 escrita amb java.io. Mateixa garantia
* —el desti no queda mai a mitges— en una fraccio del codi, i amb
* una atomicitat REAL en lloc de la sequencia esborrar-mes-reanomenar.
*/
public final class EscripturaAtomica {
private static final Logger LOG = Logger.getLogger(EscripturaAtomica.class.getName());
private EscripturaAtomica() { }
@FunctionalInterface
public interface Contingut {
void escriureA(BufferedWriter sortida) throws IOException;
}
public static void escriure(Path desti, Contingut contingut) throws IOException {
// 1. Crear el directori si falta. Idempotent.
Path directori = desti.toAbsolutePath().getParent();
Files.createDirectories(directori);
// 2. El temporal, AL MATEIX DIRECTORI: ATOMIC_MOVE ho exigeix.
// resolveSibling fa exactament aixo (apartat 3).
Path temporal = desti.resolveSibling(desti.getFileName() + ".tmp");
boolean completat = false;
try {
// 3. Escriure-ho tot al temporal. BufferedWriter, no PrintWriter:
// els seus metodes SI que llancen IOException (07-04).
try (BufferedWriter sortida = Files.newBufferedWriter(
temporal, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE)) {
contingut.escriureA(sortida);
}
// 4. Substituir de forma ATOMICA. Una sola operacio.
Files.move(temporal, desti,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
completat = true;
LOG.fine(() -> "Escriptura atomica completada: " + desti);
} catch (AtomicMoveNotSupportedException e) {
// Pot passar si el temporal i el desti son en sistemes de
// fitxers diferents. Es degrada a un move no atomic, avisant.
LOG.warning(() -> "El sistema de fitxers no suporta ATOMIC_MOVE; "
+ "se substitueix de forma no atomica: " + desti);
Files.move(temporal, desti, StandardCopyOption.REPLACE_EXISTING);
completat = true;
} finally {
// 5. Compensacio (06-05): sense brossa si alguna cosa ha fallat.
if (!completat) {
Files.deleteIfExists(temporal); // no llanca si ja no hi es
}
}
}
}Fixa't en el finally: deleteIfExists no llança si el temporal ja no existeix, així que la compensació és una sola línia sense comprovacions. És la diferència entre una API ben dissenyada i una que t'obliga a defensar-te'n.
- Lectura i escriptura d'alt nivell
Files inclou mètodes que fan en una línia el que a 07-01 i 07-02 costava un bucle.
Lectura
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
Path p = Path.of("dades/cataleg.txt");
// Java 11+: el fitxer sencer com a String
String tot = Files.readString(p, StandardCharsets.UTF_8);
// Totes les linies en una llista
List<String> linies = Files.readAllLines(p, StandardCharsets.UTF_8);
// Tots els bytes (per a binari)
byte[] bytes = Files.readAllBytes(p);
// Un BufferedReader ja construit (07-04)
try (java.io.BufferedReader lector =
Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
String linia;
while ((linia = lector.readLine()) != null) {
processar(linia);
}
}
// Un InputStream (07-03)
try (java.io.InputStream entrada = Files.newInputStream(p)) {
// ...
}L'advertiment de 07-01 i 07-04 continua dret, amb més motiu perquè ara és més fàcil equivocar-se.
readString,readAllLinesireadAllBytescarreguen el fitxer sencer en memòria. Amb un fitxer de configuració de 2 KB són perfectes; amb l'històric de préstecs de 2 GB,OutOfMemoryError. Per a això hi hanewBufferedReaderoFiles.lines().
Escriptura
// Java 11+: escriure una cadena
Files.writeString(p, "contingut", StandardCharsets.UTF_8);
// Afegint al final
Files.writeString(p, "una linia mes\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// Escriure una colleccio de linies (afegeix el salt a cadascuna)
Files.write(p, linies, StandardCharsets.UTF_8);
// Escriure bytes
Files.write(p, bytes);
// Un BufferedWriter ja construit
try (java.io.BufferedWriter sortida =
Files.newBufferedWriter(p, StandardCharsets.UTF_8)) {
sortida.write("...");
sortida.newLine();
}Comparació amb el que escrivies fa tres lliçons:
// 07-02, amb java.io
try (PrintWriter sortida = new PrintWriter(
new BufferedWriter(new FileWriter(cami, StandardCharsets.UTF_8)))) {
for (String linia : linies) {
sortida.println(linia);
}
if (sortida.checkError()) {
throw new IOException("Fallada en escriure");
}
}
// NIO.2
Files.write(Path.of(cami), linies, StandardCharsets.UTF_8);Una línia, amb el charset explícit i amb les IOException propagant-se de veritat, sense cap checkError() per oblidar.
StandardOpenOption: dir el que vols dir
StandardOpenOption: dir el que vols dirRecorda el pitjor error de 07-02: el boolean append que es confon amb un Charset i que, oblidat, esborra un fitxer sencer. NIO.2 el substitueix per opcions amb nom:
| Opció | Què fa |
|---|---|
CREATE |
Crea el fitxer si no existeix |
CREATE_NEW |
Crea el fitxer i falla si ja existeix |
APPEND |
Escriu al final, conservant el contingut |
TRUNCATE_EXISTING |
Buida el fitxer en obrir-lo |
WRITE |
Obre per escriure |
READ |
Obre per llegir |
DELETE_ON_CLOSE |
Esborra el fitxer en tancar-lo. Útil per a temporals |
SYNC |
Cada escriptura arriba al disc físic (07-02, apartat 5) |
DSYNC |
Com SYNC, només les dades, no les metadades |
Els tres casos habituals, escrits de forma que no es poden confondre:
// SOBREESCRIURE: crear si falta, buidar si existeix
Files.newBufferedWriter(p, UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
// AFEGIR: crear si falta, escriure al final
Files.newBufferedWriter(p, UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
// CREAR NOU: fallar si ja existeix. Util com a bloqueig (07-02)
Files.newBufferedWriter(p, UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE);Això és el que arregla l'error més car del mòdul. StandardOpenOption.APPEND és impossible de confondre amb un charset, i impossible d'oblidar sense que es noti, perquè sense ell has d'escriure TRUNCATE_EXISTING explícitament. I CREATE_NEW dona, per fi, un bloqueig de fitxer correcte: comprovar i crear en una sola operació atòmica, amb FileAlreadyExistsException si ja hi era.
I una nota sobre els valors per defecte: si no passes cap opció, newBufferedWriter fa servir CREATE, TRUNCATE_EXISTING i WRITE. És a dir, sobreescriu. Igual que FileWriter, però almenys aquí pots veure-ho escrit.
Files.lines, list i walk: fluxos que es tanquen
Files.lines, list i walk: fluxos que es tanquenAquests tres mètodes retornen un Stream, i aquí cal ser precís amb l'abast.
Avís d'abast.
Streamés l'API de Java 8 per a col·leccions, i s'estudia sencera a 10-04. Aquí només es fa servir el mínim per recórrer fitxers:forEachper processar cada element itry-with-resourcesper tancar-lo. No es fan servirfilter,map,collectni la resta de l'API. Quan arribis a 10-04, tot el que vegis aquí se't quedarà molt curt, i estarà bé: llavors sabràs compondre-ho.
El que sí que cal entendre ara, perquè és un error real i freqüent:
Aquests fluxos són mandrosos i mantenen el fitxer obert. Retornen elements a mesura que es demanen, llegint del disc sobre la marxa. Per això S'HAN de tancar amb
try-with-resources. No fer-ho deixa descriptors de fitxer oberts, i amb prou d'ells s'esgota el límit del sistema operatiu i l'aplicació deixa de poder obrir res.
Files.lines: les línies d'un fitxer
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class LlegirAmbLines {
public static void mostrar(Path fitxer) throws IOException {
// try-with-resources OBLIGATORI: el flux mante el fitxer obert
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
// forEach: processa cada linia. L'API completa, a 10-04.
linies.forEach(System.out::println);
}
}
}El seu avantatge sobre readAllLines: és mandrós. No carrega el fitxer en memòria; llegeix a mesura que consumeixes. Un fitxer de 10 GB es recorre amb memòria constant, igual que amb BufferedReader.
Comparació:
readAllLines |
Files.lines |
newBufferedReader |
|
|---|---|---|---|
| Càrrega en memòria | Tot el fitxer | Una línia | Una línia |
| Retorna | List<String> |
Stream<String> |
BufferedReader |
| Cal tancar-lo | No | Sí | Sí |
| Es pot recórrer diverses vegades | Sí | No: un sol ús | No |
| Fitxers grans | No | Sí | Sí |
Amb acumuladors, que és el que es pot fer sense l'API de 10-04:
public static void comptar(Path fitxer) throws IOException {
// Els comptadors han de ser efectivament finals per fer-los servir en
// una lambda (04-05). Un array d'un element es el truc classic;
// a 10-04 veuras formes millors.
int[] comptadors = new int[2]; // [0] = linies, [1] = amb dades
try (Stream<String> linies = Files.lines(fitxer, StandardCharsets.UTF_8)) {
linies.forEach(linia -> {
comptadors[0]++;
if (!linia.isBlank() && !linia.startsWith("#")) {
comptadors[1]++;
}
});
}
System.out.printf("%d linies, %d amb dades%n", comptadors[0], comptadors[1]);
}Recomanació honesta: mentre no hagis estudiat 10-04,
Files.newBufferedReaderamb el bucle canònic de 07-04 és més clar per a aquest tipus de tasques.Files.linesbrilla quan el combines ambfilter,mapicollect, i això és 10-04. Coneix-lo ara per saber que existeix i per no oblidar-te de tancar-lo.
Files.list: el contingut d'un directori
public static void llistar(Path directori) throws IOException {
try (Stream<Path> fills = Files.list(directori)) {
fills.forEach(fill -> System.out.println(" " + fill.getFileName()));
}
}És el substitut de listFiles(), i n'arregla els dos defectes: no retorna null —llança NotDirectoryException o IOException amb el seu motiu— i és mandrós, així que un directori amb un milió de fitxers no carrega un array d'un milió d'elements.
No és recursiu: només el nivell immediat.
Files.walk: l'arbre complet
public static void recorrer(Path arrel) throws IOException {
try (Stream<Path> arbre = Files.walk(arrel)) {
arbre.forEach(p -> System.out.println(" " + p));
}
}
/** Limitant la profunditat. */
public static void recorrerFinsA(Path arrel, int profunditat) throws IOException {
try (Stream<Path> arbre = Files.walk(arrel, profunditat)) {
arbre.forEach(System.out::println);
}
}Files.walk recorre en profunditat, començant per la mateixa arrel. Els seus detalls importants:
- No segueix enllaços simbòlics per defecte. Amb
FileVisitOption.FOLLOW_LINKSsí, però llavors pot entrar en un bucle infinit si hi ha enllaços circulars —llançaFileSystemLoopExceptionquan ho detecta—. - Si un directori no és accessible, llança
IOExceptionen arribar-hi, i això avorta el recorregut sencer. Per saltar-se els inaccessibles cal fer servirwalkFileTree(apartat 9). - Inclou directoris i fitxers. Cal distingir-los amb
Files.isRegularFile.
Exemple d'ús realista per a BiblioTech:
/** Localitza tots els informes d'un arbre de directoris. */
public static void localitzarInformes(Path arrel) throws IOException {
try (Stream<Path> arbre = Files.walk(arrel)) {
arbre.forEach(p -> {
if (Files.isRegularFile(p) && p.getFileName().toString().endsWith(".informe")) {
System.out.println(" " + arrel.relativize(p));
}
});
}
}A 10-04 això s'escriurà amb .filter(...) en lloc de l'if dins del forEach, i quedarà molt millor. Aquí és correcte i s'entén.
- Recórrer arbres de directoris
Files.walk està bé per al que és simple. Per a control real existeix walkFileTree amb FileVisitor, que és el patró Visitant aplicat al sistema de fitxers.
package com.nexussoftware.bibliotech.util;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.logging.Logger;
/**
* Recorregut d'arbres amb control complet, fent servir walkFileTree.
*
* Avantatge decisiu sobre Files.walk: pot DECIDIR que fer davant de cada
* element i davant de cada fallada, en lloc d'avortar el recorregut sencer
* quan un sol directori no es accessible.
*/
public class RecorregutControlat {
private static final Logger LOG = Logger.getLogger(RecorregutControlat.class.getName());
/** Calcula la mida total d'un arbre, saltant-se el que es inaccessible. */
public static Resultat calcularMida(Path arrel) throws IOException {
Resultat resultat = new Resultat();
Files.walkFileTree(arrel, new SimpleFileVisitor<Path>() {
/** Abans d'entrar en un directori. */
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
// Saltar directoris ocults i de treball sense entrar-hi
String nom = dir.getFileName().toString();
if (nom.startsWith(".") || nom.equals("tmp")) {
return FileVisitResult.SKIP_SUBTREE;
}
resultat.directoris++;
return FileVisitResult.CONTINUE;
}
/** Per cada fitxer. */
@Override
public FileVisitResult visitFile(Path fitxer, BasicFileAttributes attrs) {
resultat.fitxers++;
resultat.bytes += attrs.size();
if (attrs.size() > resultat.midaMesGran) {
resultat.midaMesGran = attrs.size();
resultat.fitxerMesGran = fitxer;
}
return FileVisitResult.CONTINUE;
}
/**
* Quan un fitxer o directori NO es pot visitar.
*
* AQUEST es el metode que fa superior walkFileTree: retornant
* CONTINUE, el recorregut SEGUEIX. Amb Files.walk, un sol directori
* sense permisos ho avorta tot.
*/
@Override
public FileVisitResult visitFileFailed(Path fitxer, IOException e) {
resultat.inaccessibles++;
LOG.fine(() -> "No accessible: " + fitxer + " (" + e.getMessage() + ")");
return FileVisitResult.CONTINUE;
}
/** En sortir d'un directori, despres de visitar-ne tot el contingut. */
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e) {
return FileVisitResult.CONTINUE;
}
});
return resultat;
}
public static class Resultat {
int directoris = 0;
int fitxers = 0;
int inaccessibles = 0;
long bytes = 0;
long midaMesGran = 0;
Path fitxerMesGran = null;
public String informe() {
return String.format(
"%d directoris, %d fitxers, %.2f MB, %d inaccessibles.%n"
+ " Mes gran: %s (%.2f MB)",
directoris, fitxers, bytes / 1024.0 / 1024.0, inaccessibles,
fitxerMesGran == null ? "(cap)" : fitxerMesGran.getFileName(),
midaMesGran / 1024.0 / 1024.0);
}
}
}Els quatre mètodes de FileVisitor i els quatre valors de retorn:
| Mètode | Quan es crida |
|---|---|
preVisitDirectory |
Abans d'entrar en un directori |
visitFile |
Per cada fitxer |
visitFileFailed |
Quan alguna cosa no es pot visitar |
postVisitDirectory |
En sortir del directori |
| Valor de retorn | Efecte |
|---|---|
CONTINUE |
Continuar normalment |
SKIP_SUBTREE |
No entrar en aquest directori (només a preVisitDirectory) |
SKIP_SIBLINGS |
Saltar els germans restants |
TERMINATE |
Aturar tot el recorregut |
SimpleFileVisitor és una implementació amb tots els mètodes retornant CONTINUE, així que només sobreescrius els que t'interessen. És el patró Adaptador, i és el que evita haver d'implementar-los tots quatre sempre.
Un ús molt pràctic: esborrar un arbre complet, que no existeix com a mètode perquè Files.delete exigeix que el directori sigui buit:
/** Esborra un directori i tot el seu contingut. FER SERVIR AMB MOLT DE COMPTE. */
public static void esborrarArbre(Path arrel) throws IOException {
if (Files.notExists(arrel)) {
return;
}
Files.walkFileTree(arrel, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path f, BasicFileAttributes attrs)
throws IOException {
Files.delete(f);
return FileVisitResult.CONTINUE;
}
/**
* El directori s'esborra DESPRES del seu contingut: per aixo va a
* postVisitDirectory i no a pre. A l'inreves, Files.delete llancaria
* DirectoryNotEmptyException.
*/
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e)
throws IOException {
if (e != null) {
throw e; // no esborrar si el recorregut ha fallat
}
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}L'ordre és la clau: el contingut primer, el directori després. És un recorregut en postordre, i és l'única forma que funcioni.
- Atributs de fitxer i permisos
File només donava lastModified(). NIO.2 dona un model complet.
Atributs bàsics
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
Path p = Path.of("dades/cataleg.txt");
// D'un en un: cada crida consulta el sistema de fitxers
long mida = Files.size(p);
FileTime modificat = Files.getLastModifiedTime(p);
// TOTS DE COP: una sola consulta. Mes eficient i CONSISTENT.
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
System.out.println("Mida : " + attrs.size());
System.out.println("Creat : " + attrs.creationTime());
System.out.println("Modificat : " + attrs.lastModifiedTime());
System.out.println("Ultim acces : " + attrs.lastAccessTime());
System.out.println("Es fitxer? : " + attrs.isRegularFile());
System.out.println("Es directori? : " + attrs.isDirectory());
System.out.println("Es enllac? : " + attrs.isSymbolicLink());Fes servir
readAttributesquan necessitis més d'un atribut. CadaFiles.size(),Files.getLastModifiedTime(),Files.isDirectory()és una consulta independent al sistema de fitxers. En recórrer un arbre de cent mil fitxers, la diferència és enorme. I a mésreadAttributesdona una foto consistent: els atributs corresponen tots al mateix instant.
FileTime i les dates
FileTime representa un instant:
FileTime moment = Files.getLastModifiedTime(p);
System.out.println(moment); // 2026-08-05T14:23:11.482Z
System.out.println(moment.toMillis()); // 1785943391482
// Comparar dos fitxers
FileTime a = Files.getLastModifiedTime(camiA);
FileTime b = Files.getLastModifiedTime(camiB);
if (a.compareTo(b) > 0) {
System.out.println("A es mes recent que B");
}
// Antiguitat en dies, amb aritmetica de millisegons
long diesDAntiguitat = (System.currentTimeMillis() - moment.toMillis())
/ (1000L * 60 * 60 * 24);
// Canviar la data de modificacio
Files.setLastModifiedTime(p, FileTime.fromMillis(System.currentTimeMillis()));Avís d'abast:
FileTimees converteix aInstanti aLocalDateTimeambtoInstant(), i allà comença l'APIjava.time, que és la lliçó 10-05. Aquí ens quedem atoString()itoMillis(), que n'hi ha prou per comparar, ordenar i calcular antiguitats. Quan arribis a 10-05 formataràs aquestes dates com cal.
Permisos POSIX
A Linux i macOS:
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
try {
Set<PosixFilePermission> permisos = Files.getPosixFilePermissions(p);
System.out.println(PosixFilePermissions.toString(permisos)); // rw-r--r--
// Establir permisos: nomes el propietari pot llegir i escriure
Files.setPosixFilePermissions(p,
PosixFilePermissions.fromString("rw-------"));
} catch (UnsupportedOperationException e) {
// A Windows el model POSIX no existeix: cal fer servir ACL
LOG.fine("Sistema sense permisos POSIX");
}Comprova sempre UnsupportedOperationException, perquè a Windows aquest model no existeix. I compte amb un ús molt important: crear un fitxer ja amb permisos restringits, en lloc de crear-lo obert i tancar-lo després —entre les dues operacions hi ha una finestra en què qualsevol el pot llegir—:
// Crear el fitxer JA amb permisos restringits, en una sola operacio
Files.createFile(p, PosixFilePermissions.asFileAttribute(
PosixFilePermissions.fromString("rw-------")));Això importa per a fitxers de configuració amb dades sensibles, i enllaça amb l'advertiment de 07-07 sobre no desar credencials al repositori.
- Fitxers temporals i
WatchService
WatchServiceFitxers i directoris temporals
// Fitxer temporal al directori temporal del sistema
Path temporal = Files.createTempFile("bibliotech-", ".tmp");
// En un directori concret: necessari si despres faras ATOMIC_MOVE
Path temporalLocal = Files.createTempFile(Path.of("dades"), "cataleg-", ".tmp");
// Directori temporal
Path dirTemporal = Files.createTempDirectory("bibliotech-import-");El nom porta un component aleatori, així que no hi ha col·lisió entre processos. I una virtut de seguretat: en sistemes POSIX es creen amb permisos rw-------, només per al propietari.
Amb esborrat automàtic en tancar:
Path temporal = Files.createTempFile("bibliotech-", ".tmp");
try (var sortida = Files.newBufferedWriter(temporal, StandardCharsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.DELETE_ON_CLOSE)) { // s'esborra en tancar
sortida.write("dades de treball");
}
// Aqui el fitxer JA NO EXISTEIXDELETE_ON_CLOSE és més fiable que el deleteOnExit() de java.io, que només actua en acabar la JVM i no s'executa amb kill -9.
Recordatori de l'apartat 5: si faràs ATOMIC_MOVE del temporal al destí, crea el temporal al mateix directori que el destí. El directori temporal del sistema sol estar en un altre sistema de fitxers.
WatchService: vigilar canvis
NIO.2 permet que l'aplicació reaccioni a canvis en un directori sense fer sondeig:
import java.nio.file.*;
/**
* Vigilancia d'un directori.
*
* NOTA: aquest bucle es BLOQUEJANT. En una aplicacio real s'executa en un
* fil a part, i aixo es el modul 8. Aqui queda com a demostracio del
* mecanisme, no com a codi que ficaries al teu main.
*/
public static void vigilar(Path directori) throws IOException, InterruptedException {
try (WatchService vigilant = FileSystems.getDefault().newWatchService()) {
directori.register(vigilant,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("Vigilant " + directori + "...");
while (true) {
WatchKey clau = vigilant.take(); // BLOQUEJA fins que passi res
for (WatchEvent<?> ev : clau.pollEvents()) {
System.out.printf(" %s: %s%n", ev.kind().name(), ev.context());
}
if (!clau.reset()) { // el directori ja no existeix
break;
}
}
}
}Usos reals: recarregar la configuració quan canvia el fitxer, processar automàticament els fitxers que apareixen en un directori d'entrada, invalidar una memòria cau.
Les seves limitacions: no és recursiu —cal registrar cada subdirectori—, a macOS la implementació fa servir sondeig i té latència, i pot generar diversos esdeveniments per una sola modificació, perquè molts editors escriuen en diverses fases.
- Interoperabilitat amb
File
FileLa conversió entre tots dos mons és trivial, i això permet migrar a poc a poc:
// De File a Path
File f = new File("dades/cataleg.txt");
Path p = f.toPath();
// De Path a File
Path p2 = Path.of("dades/cataleg.txt");
File f2 = p2.toFile();L'estratègia de migració recomanada, que és la que segueix BiblioTech:
- Fes servir
Patha tot el codi nou. - Converteix amb
toFile()només al punt exacte on una API antiga ho exigeixi. - Canvia les signatures públiques de
FileaPathquan toquis aquelles classes. - No reescriguis codi que funciona només per modernitzar-lo: migra el que toquis.
Equivalències directes:
java.io.File |
NIO.2 |
|---|---|
f.exists() |
Files.exists(p) |
f.isFile() |
Files.isRegularFile(p) |
f.isDirectory() |
Files.isDirectory(p) |
f.length() |
Files.size(p) |
f.delete() |
Files.delete(p) o Files.deleteIfExists(p) |
f.mkdirs() |
Files.createDirectories(p) |
f.renameTo(o) |
Files.move(p, o, ...) |
f.listFiles() |
Files.list(p) o Files.newDirectoryStream(p) |
f.lastModified() |
Files.getLastModifiedTime(p) |
f.getAbsolutePath() |
p.toAbsolutePath() |
f.getName() |
p.getFileName() |
f.getParent() |
p.getParent() |
f.canRead() |
Files.isReadable(p) |
- BiblioTech migra la seva persistència
Hora de recollir-ho tot. MagatzemBiblioTech substitueix la barreja de classes amb java.io de les lliçons anteriors.
package com.nexussoftware.bibliotech.infraestructura;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
/**
* Capa de persistencia de BiblioTech sobre NIO.2.
*
* Substitueix tot el codi de java.io de 07-01 a 07-05:
* - creacio de directoris -> Files.createDirectories
* - escriptura atomica -> Files.move amb ATOMIC_MOVE
* - copies rotatives -> Files.move i Files.deleteIfExists
* - localitzacio d'informes -> Files.walk
* - fallades -> excepcions especifiques, no boolean muts
*
* Estructura de directoris que gestiona:
* dades/
* cataleg.txt
* auditoria.txt
* copies/
* cataleg.bak.1 (la mes recent)
* cataleg.bak.2
* cataleg.bak.3
* informes/
* 2026/
* 01/ *.informe
* sessions/
*/
public class MagatzemBiblioTech {
private static final Logger LOG = Logger.getLogger(MagatzemBiblioTech.class.getName());
private static final java.nio.charset.Charset CHARSET = StandardCharsets.UTF_8;
private static final String CATALEG = "cataleg.txt";
private static final String AUDITORIA = "auditoria.txt";
private static final String EXT_INFORME = ".informe";
private static final int COPIES_CONSERVADES = 3;
private final Path arrel;
private final Path copies;
private final Path informes;
private final Path sessions;
/**
* Prepara l'estructura de directoris.
*
* createDirectories es IDEMPOTENT: si ja existeixen, no fa res i no
* falla. S'ha acabat el "if (!existeix) crear" amb el seu boolean mut.
*/
public MagatzemBiblioTech(String directoriArrel) throws IOException {
this.arrel = Path.of(Objects.requireNonNull(directoriArrel,
"El directori no pot ser nul")).toAbsolutePath().normalize();
this.copies = arrel.resolve("copies");
this.informes = arrel.resolve("informes");
this.sessions = arrel.resolve("sessions");
Files.createDirectories(copies);
Files.createDirectories(informes);
Files.createDirectories(sessions);
LOG.config(() -> "Magatzem de BiblioTech preparat a " + arrel);
}
// ---------------------- ESCRIPTURA ATOMICA ----------------------
@FunctionalInterface
public interface Contingut {
void escriureA(BufferedWriter sortida) throws IOException;
}
/**
* Escriu un fitxer de forma atomica, despres de fer copia de seguretat.
*
* Sequencia:
* 1. rotar les copies de seguretat existents
* 2. copiar el fitxer actual a copies/nom.bak.1
* 3. escriure el contingut nou en un temporal GERMA del desti
* 4. moure'l sobre el desti amb ATOMIC_MOVE
*/
public void escriureAmbCopia(String nom, Contingut contingut) throws IOException {
Path desti = arrel.resolve(nom);
// 1 i 2: copia de seguretat rotativa
if (Files.exists(desti)) {
rotarCopies(nom);
Files.copy(desti, copies.resolve(nom + ".bak.1"),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
}
// 3: temporal GERMA. ATOMIC_MOVE exigeix el mateix sistema de fitxers.
Path temporal = desti.resolveSibling(desti.getFileName() + ".tmp");
boolean completat = false;
try {
try (BufferedWriter sortida = Files.newBufferedWriter(temporal, CHARSET,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE)) {
contingut.escriureA(sortida);
}
// 4: substitucio atomica de veritat
Files.move(temporal, desti,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
completat = true;
LOG.info(() -> String.format("Escrit %s (%d bytes)",
desti.getFileName(), midaSegura(desti)));
} catch (AtomicMoveNotSupportedException e) {
LOG.warning(() -> "Sense suport d'ATOMIC_MOVE; substitucio no atomica");
Files.move(temporal, desti, StandardCopyOption.REPLACE_EXISTING);
completat = true;
} finally {
if (!completat) {
Files.deleteIfExists(temporal); // compensacio (06-05)
}
}
}
/**
* Rotacio de copies: .bak.2 passa a .bak.3, .bak.1 a .bak.2, i la mes
* antiga es perd.
*
* De MAJOR a MENOR, com a 07-02: a l'inreves, cada reanomenat matxucaria
* la copia seguent abans d'haver-la desplacat.
*/
private void rotarCopies(String nom) throws IOException {
Files.deleteIfExists(copies.resolve(nom + ".bak." + COPIES_CONSERVADES));
for (int i = COPIES_CONSERVADES - 1; i >= 1; i--) {
Path origen = copies.resolve(nom + ".bak." + i);
Path desti = copies.resolve(nom + ".bak." + (i + 1));
if (Files.exists(origen)) {
Files.move(origen, desti, StandardCopyOption.REPLACE_EXISTING);
}
}
}
// ---------------------- LECTURA ----------------------
/**
* Llegeix el cataleg linia a linia, amb memoria constant.
*
* Files.newBufferedReader retorna el BufferedReader de 07-04, amb el
* charset obligatori i una excepcio especifica si el fitxer falta.
*/
public List<String> llegirCataleg() throws IOException {
Path fitxer = arrel.resolve(CATALEG);
List<String> linies = new ArrayList<>();
if (Files.notExists(fitxer)) {
// DEGRADACIO ELEGANT (06-07, 07-01): la primera execucio no
// te cataleg, i aixo es normal, no una fallada.
LOG.warning(() -> "No hi ha cataleg a " + fitxer
+ "; s'arrenca amb el cataleg buit");
return linies;
}
try (BufferedReader lector = Files.newBufferedReader(fitxer, CHARSET)) {
String linia;
while ((linia = lector.readLine()) != null) { // bucle canonic (07-04)
String neta = linia.trim();
if (!neta.isEmpty() && !neta.startsWith("#")) {
linies.add(neta);
}
}
}
LOG.info(() -> "Cataleg llegit: " + linies.size() + " linies de " + fitxer);
return linies;
}
/** Afegeix una linia a l'auditoria. APPEND explicit: el fitxer CREIX. */
public void auditar(String linia) {
Path fitxer = arrel.resolve(AUDITORIA);
try (BufferedWriter sortida = Files.newBufferedWriter(fitxer, CHARSET,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND)) { // impossible de confondre
sortida.write(linia);
sortida.write('\n'); // fitxer de dades: '\n' fix
} catch (IOException e) {
// L'auditoria no tomba l'operacio de negoci (07-02)
LOG.log(Level.WARNING, "No s'ha pogut auditar: " + linia, e);
}
}
// ---------------------- INFORMES ----------------------
/** Desa un informe a informes/AAAA/MM/nom.informe. */
public Path desarInforme(String any, String mes, String nom, String contingut)
throws IOException {
Path directori = informes.resolve(any).resolve(mes);
Files.createDirectories(directori);
Path fitxer = directori.resolve(nom + EXT_INFORME);
Files.writeString(fitxer, contingut, CHARSET,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
LOG.info(() -> "Informe desat: " + arrel.relativize(fitxer));
return fitxer;
}
/**
* Localitza tots els informes de l'arbre, ordenats del mes recent al
* mes antic.
*
* Files.walk retorna un flux MANDROS que S'HA DE TANCAR: mante
* descriptors de directori oberts. D'aqui el try-with-resources.
* Es fa servir nomes forEach; l'API completa de Streams es 10-04.
*/
public List<InfoInforme> localitzarInformes() throws IOException {
List<InfoInforme> trobats = new ArrayList<>();
try (Stream<Path> arbre = Files.walk(informes)) {
arbre.forEach(p -> {
if (!Files.isRegularFile(p)
|| !p.getFileName().toString().endsWith(EXT_INFORME)) {
return;
}
try {
// readAttributes: UNA consulta per a tots els atributs,
// en lloc de tres crides independents (apartat 10)
BasicFileAttributes attrs =
Files.readAttributes(p, BasicFileAttributes.class);
trobats.add(new InfoInforme(
informes.relativize(p).toString(),
attrs.size(),
attrs.lastModifiedTime()));
} catch (IOException e) {
LOG.fine(() -> "No s'han pogut llegir els atributs de " + p);
}
});
}
// Mes recent primer (05-09)
trobats.sort(Comparator.comparing(InfoInforme::modificat).reversed());
return trobats;
}
/**
* Informacio d'un informe localitzat.
*
* FileTime es mostra amb toString() i es compara amb compareTo().
* La seva conversio a dates llegibles es java.time, que es 10-05.
*/
public record InfoInforme(String camiRelatiu, long bytes, FileTime modificat) {
public String linia() {
return String.format("%-40s %8d bytes %s",
camiRelatiu, bytes, modificat);
}
/** Antiguitat en dies, amb aritmetica de millisegons (10-05 ho fara millor). */
public long diesDAntiguitat() {
return (System.currentTimeMillis() - modificat.toMillis())
/ (1000L * 60 * 60 * 24);
}
}
// ---------------------- MANTENIMENT ----------------------
/**
* Esborra els informes amb mes de N dies.
*
* @return quants se n'han esborrat
*/
public int netejarInformesAntics(int diesMaxims) throws IOException {
int esborrats = 0;
for (InfoInforme info : localitzarInformes()) {
if (info.diesDAntiguitat() > diesMaxims) {
Path fitxer = informes.resolve(info.camiRelatiu());
if (Files.deleteIfExists(fitxer)) {
esborrats++;
LOG.fine(() -> "Informe antic esborrat: " + info.camiRelatiu());
}
}
}
final int total = esborrats;
LOG.info(() -> "Neteja: " + total + " informes amb mes de "
+ diesMaxims + " dies");
return esborrats;
}
/** Resum de l'espai ocupat, amb walkFileTree per no avortar per una fallada. */
public String informeDEspai() throws IOException {
long[] dades = new long[2]; // [0] bytes, [1] fitxers
Files.walkFileTree(arrel, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path f, BasicFileAttributes attrs) {
dades[0] += attrs.size();
dades[1]++;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path f, IOException e) {
LOG.fine(() -> "No accessible: " + f);
return FileVisitResult.CONTINUE; // no avorta el recorregut
}
});
return String.format("%s: %d fitxers, %.2f MB",
arrel.getFileName(), dades[1], dades[0] / 1024.0 / 1024.0);
}
private long midaSegura(Path p) {
try {
return Files.size(p);
} catch (IOException e) {
return -1;
}
}
public Path getArrel() { return arrel; }
public Path getSessions() { return sessions; }
}Demostració completa:
package com.nexussoftware.bibliotech.presentacio;
import java.io.IOException;
import com.nexussoftware.bibliotech.infraestructura.MagatzemBiblioTech;
public class DemoMagatzem {
public static void main(String[] args) throws IOException {
MagatzemBiblioTech magatzem = new MagatzemBiblioTech("dades");
// 1. Escriure el cataleg amb copia de seguretat i atomicitat
magatzem.escriureAmbCopia("cataleg.txt", sortida -> {
sortida.write("# Cataleg de BiblioTech - Nexus Software");
sortida.newLine();
sortida.write("LLIBRE;978-0000000001;Java Eficac;Bloch;2018");
sortida.newLine();
sortida.write("LLIBRE;978-0000000002;Patrons de Disseny;Gamma;1994");
sortida.newLine();
sortida.write("LLIBRE;978-0000000003;Refactoritzacio;Fowler;1999");
sortida.newLine();
});
// 2. Llegir
System.out.println("=== CATALEG ===");
for (String linia : magatzem.llegirCataleg()) {
System.out.println(" " + linia);
}
// 3. Auditoria (mode APPEND)
magatzem.auditar("PRESTEC;PR-0001;978-0000000001;E-001");
magatzem.auditar("DEVOLUCIO;PR-0001;978-0000000001;E-001");
// 4. Informes en un arbre de directoris
magatzem.desarInforme("2026", "01", "multes-gener", "Total: 142,50 EUR");
magatzem.desarInforme("2026", "02", "multes-febrer", "Total: 98,25 EUR");
magatzem.desarInforme("2026", "08", "multes-agost", "Total: 210,00 EUR");
System.out.println();
System.out.println("=== INFORMES LOCALITZATS ===");
for (MagatzemBiblioTech.InfoInforme info : magatzem.localitzarInformes()) {
System.out.println(" " + info.linia());
}
// 5. Espai ocupat
System.out.println();
System.out.println("=== ESPAI ===");
System.out.println(" " + magatzem.informeDEspai());
}
}Sortida:
=== CATALEG ===
LLIBRE;978-0000000001;Java Eficac;Bloch;2018
LLIBRE;978-0000000002;Patrons de Disseny;Gamma;1994
LLIBRE;978-0000000003;Refactoritzacio;Fowler;1999
=== INFORMES LOCALITZATS ===
2026/08/multes-agost.informe 17 bytes 2026-08-05T09:14:22.331Z
2026/02/multes-febrer.informe 16 bytes 2026-08-05T09:14:22.329Z
2026/01/multes-gener.informe 17 bytes 2026-08-05T09:14:22.327Z
=== ESPAI ===
dades: 9 fitxers, 0,01 MBLes vuit decisions de disseny que resumeixen la migració:
createDirectoriesal constructor. Idempotent: s'ha acabat elif (!existeix) crearamb el seubooleanmut.- L'arrel es normalitza i es fa absoluta una vegada, al constructor. Tots els camins derivats són coherents i els missatges d'error diuen on són de veritat els fitxers.
resolveen lloc de concatenar cadenes. SenseFile.separator, sense barres dobles, sense errors de plataforma.- El temporal és germà del destí amb
resolveSibling, perquèATOMIC_MOVEexigeix el mateix sistema de fitxers. Crear-lo a/tmphauria fet fallar l'atomicitat just quan importa. ATOMIC_MOVEamb degradació explícita. Si el sistema de fitxers no el suporta, s'avisa i es continua amb unmovenormal, en lloc de fallar. Degradació elegant de 06-07.- Rotació de major a menor. El mateix detall que a 07-02, i continua sent el que més es falla.
readAttributesen una sola crida dins del recorregut, en lloc de tres consultes per fitxer. Amb milers de fitxers, la diferència es nota.- Els fluxos de
Files.walkvan entry-with-resources. Mantenen descriptors oberts, i aquest és l'error més freqüent d'aquesta API.
Errors Comuns i Consells
- No tancar el flux de
Files.lines,listowalk. L'error més freqüent de NIO.2. Deixa descriptors oberts i acaba esgotant el límit del sistema.try-with-resourcessempre. - Creure que
exists()inotExists()són oposats. Tots dos sónfalsequan no es pot determinar.!exists()no significa "no existeix". - Comprovar l'existència i després operar. TOCTOU: entre les dues coses el món canvia. La comprovació és una cortesia; l'excepció és obligatòria.
- Crear el temporal a
/tmpi ferATOMIC_MOVEal destí.AtomicMoveNotSupportedExceptionsi són en sistemes de fitxers diferents. El temporal va al costat del destí:resolveSibling. - Fer servir
createDirectoryen lloc decreateDirectories. El primer falla si el pare no existeix. El segon crea tota la cadena i és idempotent. - Fer servir
deleteon voliesdeleteIfExists.NoSuchFileExceptionen una neteja que hauria de ser inofensiva. - Cridar
size(),isDirectory()igetLastModifiedTime()per separat. Tres consultes al sistema de fitxers on n'hi havia prou ambreadAttributes. En recórrer arbres grans, es nota molt. - Concatenar cadenes per compondre camins.
resolveiresolveSiblingexisteixen precisament per a això, i a més fan bé els separadors. - Fer servir
startsWithd'Stringper comprovar contenció de camins./home/martarodriguezcomença per/home/martacom a text, però no hi és a dins.Path.startsWithcompara segments. - Comprovar contenció sense
normalize()abans. Un../..al camí demanat se salta la comprovació. És path traversal, i és una vulnerabilitat real. - Esperar que
Files.copycopiï un directori amb el seu contingut. Copia el directori buit. Per a l'arbre,walkFileTree. - Esborrar un directori amb
Files.delete.DirectoryNotEmptyExceptionsi té contingut. Cal recórrer-lo en postordre. - Fer servir
Files.walkambFOLLOW_LINKSsense pensar-ho. Un enllaç circular provocaFileSystemLoopException, o un recorregut interminable. - Que un directori inaccessible avorti tot el recorregut. És el que fa
Files.walk. AmbwalkFileTreeivisitFileFailedretornantCONTINUE, el recorregut segueix. - Carregar amb
readStringoreadAllLinesun fitxer de mida desconeguda.OutOfMemoryError. Per a això hi hanewBufferedReader. - Fer servir permisos POSIX sense capturar
UnsupportedOperationException. A Windows aquell model no existeix. - Consell:
Path.ofen codi nou.Paths.getfunciona i és el mateix;Path.ofés la forma actual. - Consell:
Patha les signatures públiques,Filenomés als límits. Converteix ambtoFile()just on una API antiga ho exigeixi, i no abans. - Consell: fes servir
StandardOpenOptionencara que sigui més llarg d'escriure.APPENDés impossible de confondre amb un charset, iTRUNCATE_EXISTINGés impossible de posar sense adonar-te'n. - Consell: no migris per migrar. Codi amb
Fileque funciona no necessita reescriure's. Migra el que toquis per un altre motiu, i escriu NIO.2 el que és nou.
Exercicis
Exercici 1: explorador de camins
Escriu ExploradorCamins amb un mètode analitzar(String cami) que mostri un informe complet d'un camí fent servir només NIO.2:
- Camí tal qual, absolut, normalitzat i real (capturant l'excepció si no existeix).
- Nom, pare, arrel, nombre de segments i la llista de segments.
- Si existeix: tipus, mida, permisos llegible/escrivible, i els quatre temps de
BasicFileAttributesobtinguts en una sola crida. - Antiguitat en dies des de l'última modificació, amb aritmètica de millisegons.
- Comparació d'
exists()inotExists(), explicant a la sortida què significa cada combinació.
Prova'l amb un camí existent, un d'inexistent, un directori i un camí amb .. sense normalitzar.
Exercici 2: sincronitzador de directoris
Escriu SincronitzadorDirectoris que copiï d'un directori origen a un de destí només el que calgui:
- Copia els fitxers que no existeixen al destí.
- Copia els que existeixen però són més recents a l'origen (compara
FileTimeambcompareTo). - No toca els que estan iguals.
- Informa dels que només són al destí (possibles esborrats), sense esborrar-los.
- Crea l'estructura de directoris que falti al destí.
- Fes servir
walkFileTreeper no avortar si un fitxer és inaccessible, irelativizeper calcular el camí equivalent. - Retorna un
recordamb els recomptes i mostra un informe.
Exercici 3: gestor de còpies de seguretat rotatives
Escriu GestorCopies complet per a BiblioTech:
crearCopia(Path fitxer)que desi una còpia amb marca de temps acopies/nom.AAAAMMDD-HHMMSS.bak, fent servirSystem.currentTimeMillis()per al nom (sensejava.time, que és 10-05).- Conserva un màxim de
Ncòpies per fitxer; esborra les més antigues ordenant perFileTime. restaurar(Path fitxer, int index)que recuperi la còpia número index (0 = la més recent) de forma atòmica, fent abans una còpia de l'estat actual.llistarCopies(Path fitxer)que retorni la llista ordenada de la més recent a la més antiga, amb mida i data.espaiOcupat()que sumi la mida de totes les còpies fent servirwalkFileTree.- Un
mainque demostri el cicle complet: crear cinc còpies amb un màxim de tres, llistar, restaurar i comprovar.
Solucions
Solució 1
package com.nexussoftware.bibliotech.util;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermissions;
/**
* Informe complet d'un cami fent servir nomes NIO.2.
*
* Substitueix el DiagnosticCami de 07-01, que feia servir java.io: aqui cada
* fallada diu PER QUE, en lloc de retornar un boolean mut.
*/
public class ExploradorCamins {
public static void analitzar(String cami) {
Path p = Path.of(cami);
System.out.println("========================================");
System.out.println("ANALISI DE: " + cami);
System.out.println("========================================");
// ---- 1. Formes del cami ----
System.out.println(" Tal qual : " + p);
System.out.println(" Absoluta : " + p.toAbsolutePath());
System.out.println(" Normalitzada : " + p.toAbsolutePath().normalize());
try {
// toRealPath SI que consulta el disc i resol enllacos simbolics
System.out.println(" Real : " + p.toRealPath());
} catch (NoSuchFileException e) {
System.out.println(" Real : (no existeix, no es pot resoldre)");
} catch (IOException e) {
System.out.println(" Real : (error: " + e.getMessage() + ")");
}
// ---- 2. Estructura ----
System.out.println(" Nom : " + p.getFileName());
System.out.println(" Pare : " + p.getParent());
System.out.println(" Arrel : " + p.getRoot());
System.out.println(" Absolut? : " + p.isAbsolute());
System.out.println(" Segments : " + p.getNameCount());
StringBuilder segments = new StringBuilder();
for (Path s : p) { // Path es Iterable<Path>
segments.append('[').append(s).append(']');
}
System.out.println(" Recorregut : " + segments);
// ---- 5. exists / notExists ----
boolean existeix = Files.exists(p);
boolean noExisteix = Files.notExists(p);
System.out.println(" ----");
System.out.println(" exists() : " + existeix);
System.out.println(" notExists() : " + noExisteix);
System.out.println(" Interpretacio : " + interpretar(existeix, noExisteix));
if (!existeix) {
System.out.println();
return;
}
// ---- 3. Atributs: UNA sola consulta ----
try {
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
System.out.println(" ----");
System.out.println(" Tipus : " + tipus(attrs));
System.out.println(" Mida : " + attrs.size() + " bytes");
System.out.println(" Creat : " + attrs.creationTime());
System.out.println(" Modificat : " + attrs.lastModifiedTime());
System.out.println(" Ultim acces : " + attrs.lastAccessTime());
System.out.println(" Llegible? : " + Files.isReadable(p));
System.out.println(" Escrivible? : " + Files.isWritable(p));
// ---- 4. Antiguitat ----
long dies = (System.currentTimeMillis() - attrs.lastModifiedTime().toMillis())
/ (1000L * 60 * 60 * 24);
System.out.println(" Antiguitat : " + dies + " dies");
// Permisos POSIX si el sistema els te
try {
System.out.println(" Permisos POSIX : " + PosixFilePermissions.toString(
Files.getPosixFilePermissions(p)));
} catch (UnsupportedOperationException e) {
System.out.println(" Permisos POSIX : (sistema sense suport, p.ex. Windows)");
}
} catch (IOException e) {
System.out.println(" ERROR llegint atributs: " + e.getMessage());
}
System.out.println();
}
private static String tipus(BasicFileAttributes a) {
if (a.isDirectory()) { return "directori"; }
if (a.isRegularFile()) { return "fitxer"; }
if (a.isSymbolicLink()) { return "enllac simbolic"; }
return "altre";
}
/** Els TRES estats possibles, no dos. */
private static String interpretar(boolean existeix, boolean noExisteix) {
if (existeix && !noExisteix) {
return "EXISTEIX, i s'ha pogut comprovar";
}
if (!existeix && noExisteix) {
return "NO EXISTEIX, i s'ha pogut comprovar";
}
if (!existeix && !noExisteix) {
return "NO ES POT DETERMINAR (probablement sense permis sobre "
+ "el directori que el conte). Per aixo !exists() NO "
+ "significa 'no existeix'.";
}
return "estat impossible";
}
public static void main(String[] args) {
analitzar("dades/cataleg.txt");
analitzar("dades");
analitzar("dades/no-existeix.txt");
analitzar("dades/../dades/./informes/../cataleg.txt"); // sense normalitzar
}
}Sortida (fragment de l'última crida):
========================================
ANALISI DE: dades/../dades/./informes/../cataleg.txt
========================================
Tal qual : dades/../dades/./informes/../cataleg.txt
Absoluta : /home/marta/bibliotech/dades/../dades/./informes/../cataleg.txt
Normalitzada : /home/marta/bibliotech/dades/cataleg.txt
Real : /home/marta/bibliotech/dades/cataleg.txt
Nom : cataleg.txt
Segments : 7
Recorregut : [dades][..][dades][.][informes][..][cataleg.txt]
----
exists() : true
notExists() : false
Interpretacio : EXISTEIX, i s'ha pogut comprovarEls dos punts clau: primer, la diferència entre toAbsolutePath() —que només anteposa el directori de treball, sense netejar res— i normalize()/toRealPath(), que sí que resolen els ... Aquesta distinció és exactament la que fa possible la vulnerabilitat de path traversal de l'apartat 3. I segon, la interpretació dels tres estats d'exists/notExists, que és el parany més subtil d'aquesta API.
Solució 2
package com.nexussoftware.bibliotech.util;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
/**
* Sincronitza un directori desti amb un origen, copiant nomes el necessari.
*
* Es el nucli de qualsevol eina de copia incremental: no copiar el
* que ja esta igual estalvia la major part de la feina.
*/
public class SincronitzadorDirectoris {
private static final Logger LOG =
Logger.getLogger(SincronitzadorDirectoris.class.getName());
public record Resultat(int copiats, int actualitzats, int senseCanvis,
int nomesAlDesti, int errors,
List<String> detallNomesAlDesti) {
public String informe() {
StringBuilder sb = new StringBuilder();
sb.append("=== SINCRONITZACIO ===\n");
sb.append(String.format(" Copiats (nous) : %d%n", copiats));
sb.append(String.format(" Actualitzats : %d%n", actualitzats));
sb.append(String.format(" Sense canvis : %d%n", senseCanvis));
sb.append(String.format(" Nomes al desti : %d%n", nomesAlDesti));
sb.append(String.format(" Errors : %d%n", errors));
if (!detallNomesAlDesti.isEmpty()) {
sb.append(" --- Nomes al desti (candidats a esborrar) ---\n");
int n = Math.min(10, detallNomesAlDesti.size());
for (int i = 0; i < n; i++) {
sb.append(" ").append(detallNomesAlDesti.get(i)).append('\n');
}
if (detallNomesAlDesti.size() > n) {
sb.append(String.format(" ... i %d mes%n",
detallNomesAlDesti.size() - n));
}
}
return sb.toString();
}
}
/** Sincronitza origen cap a desti. NO esborra MAI res del desti. */
public Resultat sincronitzar(Path origen, Path desti) throws IOException {
Path arrelOrigen = origen.toAbsolutePath().normalize();
Path arrelDesti = desti.toAbsolutePath().normalize();
if (!Files.isDirectory(arrelOrigen)) {
throw new NotDirectoryException(arrelOrigen.toString());
}
Files.createDirectories(arrelDesti);
int[] comptadors = new int[4]; // copiats, actualitzats, iguals, errors
Set<Path> vistosAOrigen = new HashSet<>();
// ---- FASE 1: recorrer l'origen i sincronitzar ----
Files.walkFileTree(arrelOrigen, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
// relativize dona el cami equivalent al desti (apartat 3)
Path equivalent = arrelDesti.resolve(arrelOrigen.relativize(dir));
Files.createDirectories(equivalent); // idempotent
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path fitxer, BasicFileAttributes attrsOrigen) {
Path relatiu = arrelOrigen.relativize(fitxer);
Path equivalent = arrelDesti.resolve(relatiu);
vistosAOrigen.add(relatiu);
try {
if (Files.notExists(equivalent)) {
// NOU: copiar conservant els atributs
Files.copy(fitxer, equivalent,
StandardCopyOption.COPY_ATTRIBUTES);
comptadors[0]++;
LOG.fine(() -> "Nou: " + relatiu);
return FileVisitResult.CONTINUE;
}
BasicFileAttributes attrsDesti =
Files.readAttributes(equivalent, BasicFileAttributes.class);
FileTime tOrigen = attrsOrigen.lastModifiedTime();
FileTime tDesti = attrsDesti.lastModifiedTime();
// compareTo > 0 significa que l'origen es MES RECENT.
// Es compara tambe la mida: un fitxer amb la mateixa
// data pero mida diferent tambe cal copiar-lo.
if (tOrigen.compareTo(tDesti) > 0
|| attrsOrigen.size() != attrsDesti.size()) {
Files.copy(fitxer, equivalent,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
comptadors[1]++;
LOG.fine(() -> "Actualitzat: " + relatiu);
} else {
comptadors[2]++;
}
} catch (IOException e) {
comptadors[3]++;
LOG.warning(() -> "Error sincronitzant " + relatiu
+ ": " + e.getMessage());
}
return FileVisitResult.CONTINUE;
}
/**
* CLAU: un fitxer inaccessible NO avorta la sincronitzacio.
* Amb Files.walk aixo no seria possible.
*/
@Override
public FileVisitResult visitFileFailed(Path fitxer, IOException e) {
comptadors[3]++;
LOG.warning(() -> "No accessible a l'origen: " + fitxer);
return FileVisitResult.CONTINUE;
}
});
// ---- FASE 2: detectar el que nomes es al desti ----
List<String> nomesDesti = new ArrayList<>();
Files.walkFileTree(arrelDesti, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path fitxer, BasicFileAttributes attrs) {
Path relatiu = arrelDesti.relativize(fitxer);
if (!vistosAOrigen.contains(relatiu)) {
nomesDesti.add(relatiu.toString());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path f, IOException e) {
return FileVisitResult.CONTINUE;
}
});
nomesDesti.sort(null);
return new Resultat(comptadors[0], comptadors[1], comptadors[2],
nomesDesti.size(), comptadors[3], nomesDesti);
}
public static void main(String[] args) throws IOException {
Resultat r = new SincronitzadorDirectoris()
.sincronitzar(Path.of("dades"), Path.of("copia-seguretat/dades"));
System.out.println(r.informe());
}
}Sortida d'una segona execució, després d'haver modificat un fitxer:
=== SINCRONITZACIO ===
Copiats (nous) : 0
Actualitzats : 1
Sense canvis : 11
Nomes al desti : 2
Errors : 0
--- Nomes al desti (candidats a esborrar) ---
cataleg-antic.txt
informes/2025/12/multes-desembre.informeEls quatre punts didàctics:
relativize+resolveés la parella que fa això possible.arrelOrigen.relativize(fitxer)dona el camí relatiu;arrelDesti.resolve(relatiu)dona l'equivalent. Amb concatenació de cadenes això es fa malament tan bon punt hi ha subdirectoris.visitFileFailedretornantCONTINUEés la raó de fer servirwalkFileTree. Un sol fitxer bloquejat avortaria unFiles.walksencer, i una eina de còpia que es rendeix davant del primer problema no serveix.- Es compara data i mida. Comparar només la data deixa passar fitxers modificats amb la mateixa marca de temps, cosa que passa més del que sembla amb algunes eines. Comparar només la mida deixa passar canvis que no l'alteren.
- No esborra res. Informa del que sobra i deixa la decisió a qui executa. Una eina que esborra automàticament en un directori de còpia de seguretat és una forma eficient de perdre dades.
Solució 3
package com.nexussoftware.bibliotech.infraestructura;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;
import java.util.stream.Stream;
/**
* Copies de seguretat rotatives amb marca de temps, sobre NIO.2.
*
* Cada copia s'anomena fitxer.AAAAMMDD-HHMMSS.bak. Es conserven les N mes
* recents; les altres s'esborren.
*
* NOTA: el nom amb data es compon amb SimpleDateFormat perque java.time
* es la llico 10-05. Amb LocalDateTime i DateTimeFormatter aixo queda
* bastant millor, i alla ho veuras.
*/
public class GestorCopies {
private static final Logger LOG = Logger.getLogger(GestorCopies.class.getName());
private static final String SUFIX = ".bak";
private static final int MAX_COPIES_DEFECTE = 3;
private final Path directoriCopies;
private final int maxCopies;
public GestorCopies(Path directoriCopies) throws IOException {
this(directoriCopies, MAX_COPIES_DEFECTE);
}
public GestorCopies(Path directoriCopies, int maxCopies) throws IOException {
this.directoriCopies = Objects.requireNonNull(directoriCopies)
.toAbsolutePath().normalize();
if (maxCopies < 1) {
throw new IllegalArgumentException(
"Cal conservar com a minim una copia, i se n'han demanat: " + maxCopies);
}
this.maxCopies = maxCopies;
Files.createDirectories(this.directoriCopies);
}
/** Informacio d'una copia existent. */
public record InfoCopia(Path cami, String nom, long bytes, FileTime creada) {
public String linia() {
return String.format("%-45s %8d bytes %s", nom, bytes, creada);
}
}
// ---------------------- CREAR ----------------------
/**
* Crea una copia amb marca de temps i aplica la rotacio.
*
* @return el cami de la copia creada
*/
public Path crearCopia(Path fitxer) throws IOException {
Objects.requireNonNull(fitxer, "El fitxer no pot ser nul");
if (Files.notExists(fitxer)) {
throw new NoSuchFileException(fitxer.toString(),
null, "No es pot copiar un fitxer que no existeix");
}
String nomBase = fitxer.getFileName().toString();
String marca = new java.text.SimpleDateFormat("yyyyMMdd-HHmmss")
.format(new Date(System.currentTimeMillis()));
Path copia = directoriCopies.resolve(nomBase + "." + marca + SUFIX);
// Si al mateix segon es demana una altra copia, s'afegeix un sufix
int intent = 1;
while (Files.exists(copia)) {
copia = directoriCopies.resolve(
nomBase + "." + marca + "-" + intent + SUFIX);
intent++;
}
// COPY_ATTRIBUTES conserva la data original del fitxer copiat
Files.copy(fitxer, copia, StandardCopyOption.COPY_ATTRIBUTES);
final Path creada = copia;
LOG.info(() -> "Copia creada: " + creada.getFileName());
aplicarRotacio(nomBase);
return copia;
}
/** Esborra les copies que sobren, comencant per les mes antigues. */
private void aplicarRotacio(String nomBase) throws IOException {
List<InfoCopia> copies = llistarCopies(nomBase);
for (int i = maxCopies; i < copies.size(); i++) {
InfoCopia sobrant = copies.get(i); // ja venen de nova a vella
if (Files.deleteIfExists(sobrant.cami())) {
LOG.fine(() -> "Copia antiga esborrada: " + sobrant.nom());
}
}
}
// ---------------------- LLISTAR ----------------------
/** Copies d'un fitxer, de la MES RECENT a la mes antiga. */
public List<InfoCopia> llistarCopies(String nomBase) throws IOException {
List<InfoCopia> trobades = new ArrayList<>();
String prefix = nomBase + ".";
// Files.list retorna un flux MANDROS: try-with-resources obligatori
try (Stream<Path> fills = Files.list(directoriCopies)) {
fills.forEach(p -> {
String nom = p.getFileName().toString();
if (!nom.startsWith(prefix) || !nom.endsWith(SUFIX)) {
return;
}
try {
BasicFileAttributes attrs =
Files.readAttributes(p, BasicFileAttributes.class);
trobades.add(new InfoCopia(p, nom, attrs.size(),
attrs.lastModifiedTime()));
} catch (IOException e) {
LOG.fine(() -> "No s'ha pogut llegir " + p);
}
});
}
// Mes recent primer (05-09)
trobades.sort(Comparator.comparing(InfoCopia::creada).reversed());
return trobades;
}
public List<InfoCopia> llistarCopies(Path fitxer) throws IOException {
return llistarCopies(fitxer.getFileName().toString());
}
// ---------------------- RESTAURAR ----------------------
/**
* Restaura la copia numero 'index' (0 = la mes recent).
*
* ABANS de restaurar, fa una copia de l'estat ACTUAL: si la
* restauracio resulta ser un error, es pot desfer. Restaurar sense
* aquesta xarxa es una forma habitual de perdre la feina del dia.
*
* La substitucio es ATOMICA: el fitxer no queda mai a mitges.
*/
public void restaurar(Path fitxer, int index) throws IOException {
List<InfoCopia> copies = llistarCopies(fitxer);
if (copies.isEmpty()) {
throw new NoSuchFileException(fitxer.toString(),
null, "No hi ha cap copia de seguretat d'aquest fitxer");
}
if (index < 0 || index >= copies.size()) {
throw new IllegalArgumentException(String.format(
"Index %d fora de rang: hi ha %d copies (0..%d)",
index, copies.size(), copies.size() - 1));
}
InfoCopia triada = copies.get(index);
// 1. Xarxa de seguretat: copiar l'estat actual abans de trepitjar-lo
if (Files.exists(fitxer)) {
crearCopia(fitxer);
}
// 2. Temporal GERMA del desti: ATOMIC_MOVE ho exigeix
Path temporal = fitxer.resolveSibling(fitxer.getFileName() + ".restaurant");
boolean completat = false;
try {
Files.copy(triada.cami(), temporal,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
Files.move(temporal, fitxer,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
completat = true;
LOG.info(() -> "Restaurat " + fitxer.getFileName()
+ " des de " + triada.nom());
} catch (AtomicMoveNotSupportedException e) {
LOG.warning(() -> "Sense ATOMIC_MOVE; restauracio no atomica");
Files.move(temporal, fitxer, StandardCopyOption.REPLACE_EXISTING);
completat = true;
} finally {
if (!completat) {
Files.deleteIfExists(temporal); // compensacio (06-05)
}
}
}
// ---------------------- ESPAI ----------------------
/** Espai total ocupat per les copies. */
public long espaiOcupat() throws IOException {
long[] total = new long[1];
Files.walkFileTree(directoriCopies, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path f, BasicFileAttributes attrs) {
if (f.getFileName().toString().endsWith(SUFIX)) {
total[0] += attrs.size();
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path f, IOException e) {
return FileVisitResult.CONTINUE; // no avortar per una fallada
}
});
return total[0];
}
// ---------------------- DEMOSTRACIO ----------------------
public static void main(String[] args) throws IOException, InterruptedException {
Path fitxer = Path.of("dades/cataleg.txt");
Files.createDirectories(fitxer.getParent());
GestorCopies gestor = new GestorCopies(Path.of("dades/copies"), 3);
// 1. Cinc versions diferents, amb maxim de 3 copies
System.out.println("=== CREANT 5 COPIES (maxim 3) ===");
for (int v = 1; v <= 5; v++) {
Files.writeString(fitxer,
"# Cataleg versio " + v + "\n"
+ "LLIBRE;978-000000000" + v + ";Llibre " + v + ";Autor;2020\n",
java.nio.charset.StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
Path copia = gestor.crearCopia(fitxer);
System.out.println(" v" + v + " -> " + copia.getFileName());
Thread.sleep(1100); // perque la marca de temps canvii
}
// 2. Llistar: nomes han de quedar les 3 ultimes
System.out.println();
System.out.println("=== COPIES CONSERVADES ===");
List<InfoCopia> copies = gestor.llistarCopies(fitxer);
for (int i = 0; i < copies.size(); i++) {
System.out.printf(" [%d] %s%n", i, copies.get(i).linia());
}
// 3. Contingut actual
System.out.println();
System.out.println("=== CONTINGUT ACTUAL ===");
System.out.print(Files.readString(fitxer));
// 4. Restaurar la copia [1] (la penultima, es a dir, la versio 4)
System.out.println();
System.out.println("=== RESTAURANT LA COPIA [1] ===");
gestor.restaurar(fitxer, 1);
System.out.print(Files.readString(fitxer));
// 5. Espai
System.out.println();
System.out.printf("=== ESPAI OCUPAT: %.2f KB en %d copies ===%n",
gestor.espaiOcupat() / 1024.0,
gestor.llistarCopies(fitxer).size());
}
}Sortida:
=== CREANT 5 COPIES (maxim 3) ===
v1 -> cataleg.txt.20260805-091422.bak
v2 -> cataleg.txt.20260805-091423.bak
v3 -> cataleg.txt.20260805-091424.bak
v4 -> cataleg.txt.20260805-091425.bak
v5 -> cataleg.txt.20260805-091426.bak
=== COPIES CONSERVADES ===
[0] cataleg.txt.20260805-091426.bak 61 bytes 2026-08-05T09:14:26.118Z
[1] cataleg.txt.20260805-091425.bak 61 bytes 2026-08-05T09:14:25.012Z
[2] cataleg.txt.20260805-091424.bak 61 bytes 2026-08-05T09:14:23.907Z
=== CONTINGUT ACTUAL ===
# Cataleg versio 5
LLIBRE;978-0000000005;Llibre 5;Autor;2020
=== RESTAURANT LA COPIA [1] ===
# Cataleg versio 4
LLIBRE;978-0000000004;Llibre 4;Autor;2020
=== ESPAI OCUPAT: 0,18 KB en 3 copies ===Els cinc punts didàctics:
- La rotació es fa per data, no per nom. Ordenar per
FileTimeambComparator.comparing(...).reversed()és més robust que confiar que el nom ordeni bé —que aquí funcionaria, perquè el formatAAAAMMDD-HHMMSSés ordenable alfabèticament, però és una coincidència afortunada que no convé donar per suposada—. - Restaurar fa primer una còpia de l'estat actual. És la diferència entre una eina de còpies i un parany: si restaures la versió equivocada, pots desfer-ho. Fixa't que després de la restauració hi ha 3 còpies, no 4: la rotació s'ha aplicat també a la còpia de seguretat prèvia.
- La restauració és atòmica amb el temporal germà i
ATOMIC_MOVE, amb degradació explícita si el sistema de fitxers no ho suporta. Files.listva entry-with-resources. SillistarCopieses crida milers de vegades sense tancar el flux, s'esgoten els descriptors del procés.- El bucle de col·lisió de noms. Dues còpies al mateix segon tindrien el mateix nom; el sufix
-1,-2ho evita. És un detall petit que es descobreix en producció quan algú prem el botó dues vegades.
Conclusió
Tots aquells comentaris de "a 07-06 això es fa bé" estan resolts.
Saps per què existeix NIO.2 i quins set defectes de java.io.File corregeix: els boolean muts que diuen que alguna cosa ha fallat sense dir per què, el listFiles() que retorna null, la ceguesa davant dels enllaços simbòlics, les metadades pobres, el renameTo que es comporta diferent a cada sistema, la impossibilitat d'estendre-la i l'absència de recorregut eficient d'arbres. I saps que la regla professional no admet matisos: Path i Files en codi nou, File només per llegir codi antic i per a les API que encara l'exigeixen, amb toPath()/toFile() com a pont als límits.
Domines Path: Path.of com a forma actual, la consulta de nom, pare, arrel i segments, la iteració —perquè Path és Iterable<Path>— i el startsWith que compara segments complets, no prefixos de text. I domines la seva aritmètica: resolve per combinar, amb la regla que un argument absolut substitueix la base; resolveSibling per al germà al mateix directori, que és exactament el que necessita un fitxer temporal; relativize per al camí d'A a B; i normalize per eliminar . i .. de forma purament sintàctica, davant de toRealPath(), que sí que consulta el disc. I coneixes l'ús de normalize + startsWith que impedeix el path traversal, una vulnerabilitat real que amb concatenació de cadenes es preveu malament gairebé sempre.
Coneixes Files de dalt a baix. Saps que exists() i notExists() no són oposats, perquè hi ha un tercer estat —"no es pot determinar"— i que per tant !exists() no significa "no existeix". Saps que createDirectories és idempotent i crea tota la cadena de pares, que delete i deleteIfExists es distingeixen per si l'absència del fitxer és un error, i que cada fallada arriba com una excepció específica —NoSuchFileException, DirectoryNotEmptyException, AccessDeniedException— que diu exactament què ha passat. Això és el que 06-04 demanava a una excepció.
I tens la peça que portaves quatre lliçons aproximant: Files.move amb ATOMIC_MOVE, que substitueix la seqüència esborrar-més-reanomenar per una sola operació sense buit intermedi. Amb els seus dos límits ben entesos: només dins del mateix sistema de fitxers —d'aquí que el temporal hagi de ser germà del destí— i amb AtomicMoveNotSupportedException com a degradació prevista. L'EscripturaAtomica de 07-02, amb les seves quaranta línies de comptabilitat, cap ara en quinze i garanteix més.
Saps llegir i escriure en una línia amb readString, writeString, readAllLines i write, amb l'advertiment de sempre —carreguen el fitxer sencer en memòria— i amb newBufferedReader/newBufferedWriter com a forma correcta per al que creix. I tens StandardOpenOption, que arregla l'error més car del mòdul: APPEND és impossible de confondre amb un charset, i TRUNCATE_EXISTING és impossible d'escriure sense voler. CREATE_NEW, a més, dona per fi un bloqueig de fitxer correcte: comprovar i crear en una operació atòmica.
Coneixes Files.lines, list i walk, amb l'únic que importa ara: retornen fluxos mandrosos que mantenen el fitxer obert i S'HAN de tancar amb try-with-resources, i es fan servir aquí amb forEach perquè l'API completa de Streams és 10-04. Saps que Files.list arregla els dos defectes de listFiles() i que Files.walk avorta el recorregut sencer quan troba alguna cosa inaccessible —motiu pel qual existeix walkFileTree amb SimpleFileVisitor, els seus quatre mètodes i els seus quatre valors de retorn, i amb el visitFileFailed retornant CONTINUE que fa que un recorregut segueixi endavant—. I saps esborrar un arbre en postordre, que és l'única forma que funcioni.
Coneixes els atributs amb readAttributes —una consulta per a tots, en lloc d'una per atribut, que en recórrer arbres grans canvia el rendiment—, FileTime amb el seu toString(), el seu toMillis() i el seu compareTo per ordenar i calcular antiguitats, remetent a 10-05 per a les dates de veritat; i els permisos POSIX amb la seva UnsupportedOperationException a Windows i el seu ús important: crear un fitxer ja amb permisos restringits, sense la finestra d'exposició de crear-lo i protegir-lo després. I coneixes els fitxers temporals amb DELETE_ON_CLOSE i el WatchService amb les seves limitacions.
BiblioTech té la seva capa de persistència migrada. MagatzemBiblioTech prepara la seva estructura de directoris amb createDirectories idempotent, normalitza la seva arrel una sola vegada, compon tots els seus camins amb resolve en lloc de concatenar, escriu de forma veritablement atòmica amb el temporal germà i ATOMIC_MOVE, manté còpies de seguretat rotatives de major a menor, afegeix a l'auditoria amb APPEND explícit, desa informes en un arbre informes/AAAA/MM/, els localitza amb Files.walk tancat com cal i llegint els atributs d'una sola vegada, neteja els antics per antiguitat i calcula l'espai amb walkFileTree sense avortar per un fitxer inaccessible. Ni un boolean mut, ni un null de retorn, ni una comprovació d'existència que substitueixi una excepció.
I tanmateix, queda el que dona sentit a tot el mòdul. El catàleg de BiblioTech continua desant-se amb un split(";") ingenu i un sanejar() que substitueix els punts i comes dels títols per comes i perd la dada. El vas marcar com un pedaç a 07-02 i continua sent-ho. I les quatre constants de negoci —DIES_PRESTEC, TARIFA_DIARIA, MULTA_MAXIMA, LLINDAR_LLEU— porten set mòduls escrites a foc al codi: canviar la tarifa de les multes exigeix recompilar i tornar a desplegar l'aplicació.
A la lliçó 07-07, Formats d'intercanvi: CSV i Properties, es tanca el mòdul amb les dues coses. Veuràs per què un format de text és superior a un de binari per intercanviar i què es paga a canvi; el CSV de veritat, amb tot el que el split ingenu fa malament —comes dins de camps entrecometats, cometes escapades, salts de línia dins d'un camp, camps buits davant de nuls, el BOM— i una implementació correcta amb el seu escapar/desescapar i la seva taula de casos límit, més el conflicte del separador decimal en català que ja va treure el nas a 07-01; i Properties, amb el seu format clau-valor, el seu load/store, el seu problema històric de codificació, i la jerarquia de configuració completa: valors per defecte al codi, fitxer bibliotech.properties, propietats del sistema amb -D i variables d'entorn, amb la seva taula de precedència. I llavors sí, les quatre constants deixaran de ser constants: BiblioTech es configurarà sense recompilar, i recordarà entre execucions tot el que fa.
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
