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.2New 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-resources a tot, com sempre. I amb un advertiment nou i molt concret: Files.lines(), Files.walk() i Files.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

  1. Per què existeix NIO.2: els defectes de File
  2. Path: construir i consultar camins
  3. Aritmètica de camins: resolve, relativize, normalize
  4. Files: existència, creació i esborrat
  5. Copiar i moure, amb l'escriptura atòmica de veritat
  6. Lectura i escriptura d'alt nivell
  7. StandardOpenOption: dir el que vols dir
  8. Files.lines, list i walk: fluxos que es tanquen
  9. Recórrer arbres de directoris
  10. Atributs de fitxer i permisos
  11. Fitxers temporals i WatchService
  12. Interoperabilitat amb File
  13. BiblioTech migra la seva persistència
  14. Errors Comuns i Consells
  15. Exercicis

  1. Per què existeix NIO.2: els defectes de File

java.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, Path i Files. Sempre. Coneix File per llegir codi antic i per a les API que encara el demanen; escriu-lo només quan no hi hagi alternativa.

  1. Path: construir i consultar camins

Path 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.

  1. Aritmètica de camins: resolve, relativize, normalize

Aquí 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/bibliotech

La 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.bak

Equival 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.txt

normalize é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 , llança IOException

  1. Files: existència, creació i esborrat

Files é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() i notExists() NO són oposats. Tots dos poden retornar false alhora.

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.

  1. 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:

  1. 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.
  2. REPLACE_EXISTING amb ATOMIC_MOVE està 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.

  1. 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, readAllLines i readAllBytes carreguen 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 ha newBufferedReader o Files.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.

  1. StandardOpenOption: dir el que vols dir

Recorda 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.

  1. Files.lines, list i walk: fluxos que es tanquen

Aquests 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: forEach per processar cada element i try-with-resources per tancar-lo. No es fan servir filter, map, collect ni 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
Es pot recórrer diverses vegades No: un sol ús No
Fitxers grans No

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.newBufferedReader amb el bucle canònic de 07-04 és més clar per a aquest tipus de tasques. Files.lines brilla quan el combines amb filter, map i collect, 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_LINKS sí, però llavors pot entrar en un bucle infinit si hi ha enllaços circulars —llança FileSystemLoopException quan ho detecta—.
  • Si un directori no és accessible, llança IOException en arribar-hi, i això avorta el recorregut sencer. Per saltar-se els inaccessibles cal fer servir walkFileTree (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.

  1. 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.

  1. 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 readAttributes quan necessitis més d'un atribut. Cada Files.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és readAttributes dona 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: FileTime es converteix a Instant i a LocalDateTime amb toInstant(), i allà comença l'API java.time, que és la lliçó 10-05. Aquí ens quedem a toString() i toMillis(), 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.

  1. Fitxers temporals i WatchService

Fitxers 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 EXISTEIX

DELETE_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.

  1. Interoperabilitat amb File

La 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:

  1. Fes servir Path a tot el codi nou.
  2. Converteix amb toFile() només al punt exacte on una API antiga ho exigeixi.
  3. Canvia les signatures públiques de File a Path quan toquis aquelles classes.
  4. 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)

  1. 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 MB

Les vuit decisions de disseny que resumeixen la migració:

  1. createDirectories al constructor. Idempotent: s'ha acabat el if (!existeix) crear amb el seu boolean mut.
  2. 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.
  3. resolve en lloc de concatenar cadenes. Sense File.separator, sense barres dobles, sense errors de plataforma.
  4. El temporal és germà del destí amb resolveSibling, perquè ATOMIC_MOVE exigeix el mateix sistema de fitxers. Crear-lo a /tmp hauria fet fallar l'atomicitat just quan importa.
  5. ATOMIC_MOVE amb degradació explícita. Si el sistema de fitxers no el suporta, s'avisa i es continua amb un move normal, en lloc de fallar. Degradació elegant de 06-07.
  6. Rotació de major a menor. El mateix detall que a 07-02, i continua sent el que més es falla.
  7. readAttributes en una sola crida dins del recorregut, en lloc de tres consultes per fitxer. Amb milers de fitxers, la diferència es nota.
  8. Els fluxos de Files.walk van en try-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, list o walk. L'error més freqüent de NIO.2. Deixa descriptors oberts i acaba esgotant el límit del sistema. try-with-resources sempre.
  • Creure que exists() i notExists() són oposats. Tots dos són false quan 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 /tmp i fer ATOMIC_MOVE al destí. AtomicMoveNotSupportedException si són en sistemes de fitxers diferents. El temporal va al costat del destí: resolveSibling.
  • Fer servir createDirectory en lloc de createDirectories. El primer falla si el pare no existeix. El segon crea tota la cadena i és idempotent.
  • Fer servir delete on volies deleteIfExists. NoSuchFileException en una neteja que hauria de ser inofensiva.
  • Cridar size(), isDirectory() i getLastModifiedTime() per separat. Tres consultes al sistema de fitxers on n'hi havia prou amb readAttributes. En recórrer arbres grans, es nota molt.
  • Concatenar cadenes per compondre camins. resolve i resolveSibling existeixen precisament per a això, i a més fan bé els separadors.
  • Fer servir startsWith d'String per comprovar contenció de camins. /home/martarodriguez comença per /home/marta com a text, però no hi és a dins. Path.startsWith compara 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.copy copiï un directori amb el seu contingut. Copia el directori buit. Per a l'arbre, walkFileTree.
  • Esborrar un directori amb Files.delete. DirectoryNotEmptyException si té contingut. Cal recórrer-lo en postordre.
  • Fer servir Files.walk amb FOLLOW_LINKS sense pensar-ho. Un enllaç circular provoca FileSystemLoopException, o un recorregut interminable.
  • Que un directori inaccessible avorti tot el recorregut. És el que fa Files.walk. Amb walkFileTree i visitFileFailed retornant CONTINUE, el recorregut segueix.
  • Carregar amb readString o readAllLines un fitxer de mida desconeguda. OutOfMemoryError. Per a això hi ha newBufferedReader.
  • Fer servir permisos POSIX sense capturar UnsupportedOperationException. A Windows aquell model no existeix.
  • Consell: Path.of en codi nou. Paths.get funciona i és el mateix; Path.of és la forma actual.
  • Consell: Path a les signatures públiques, File només als límits. Converteix amb toFile() just on una API antiga ho exigeixi, i no abans.
  • Consell: fes servir StandardOpenOption encara que sigui més llarg d'escriure. APPEND és impossible de confondre amb un charset, i TRUNCATE_EXISTING és impossible de posar sense adonar-te'n.
  • Consell: no migris per migrar. Codi amb File que 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:

  1. Camí tal qual, absolut, normalitzat i real (capturant l'excepció si no existeix).
  2. Nom, pare, arrel, nombre de segments i la llista de segments.
  3. Si existeix: tipus, mida, permisos llegible/escrivible, i els quatre temps de BasicFileAttributes obtinguts en una sola crida.
  4. Antiguitat en dies des de l'última modificació, amb aritmètica de millisegons.
  5. Comparació d'exists() i notExists(), 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:

  1. Copia els fitxers que no existeixen al destí.
  2. Copia els que existeixen però són més recents a l'origen (compara FileTime amb compareTo).
  3. No toca els que estan iguals.
  4. Informa dels que només són al destí (possibles esborrats), sense esborrar-los.
  5. Crea l'estructura de directoris que falti al destí.
  6. Fes servir walkFileTree per no avortar si un fitxer és inaccessible, i relativize per calcular el camí equivalent.
  7. Retorna un record amb els recomptes i mostra un informe.

Exercici 3: gestor de còpies de seguretat rotatives

Escriu GestorCopies complet per a BiblioTech:

  1. crearCopia(Path fitxer) que desi una còpia amb marca de temps a copies/nom.AAAAMMDD-HHMMSS.bak, fent servir System.currentTimeMillis() per al nom (sense java.time, que és 10-05).
  2. Conserva un màxim de N còpies per fitxer; esborra les més antigues ordenant per FileTime.
  3. 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.
  4. llistarCopies(Path fitxer) que retorni la llista ordenada de la més recent a la més antiga, amb mida i data.
  5. espaiOcupat() que sumi la mida de totes les còpies fent servir walkFileTree.
  6. Un main que 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 comprovar

Els 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.informe

Els quatre punts didàctics:

  1. 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.
  2. visitFileFailed retornant CONTINUE és la raó de fer servir walkFileTree. Un sol fitxer bloquejat avortaria un Files.walk sencer, i una eina de còpia que es rendeix davant del primer problema no serveix.
  3. 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.
  4. 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:

  1. La rotació es fa per data, no per nom. Ordenar per FileTime amb Comparator.comparing(...).reversed() és més robust que confiar que el nom ordeni bé —que aquí funcionaria, perquè el format AAAAMMDD-HHMMSS és ordenable alfabèticament, però és una coincidència afortunada que no convé donar per suposada—.
  2. 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.
  3. La restauració és atòmica amb el temporal germà i ATOMIC_MOVE, amb degradació explícita si el sistema de fitxers no ho suporta.
  4. Files.list va en try-with-resources. Si llistarCopies es crida milers de vegades sense tancar el flux, s'esgoten els descriptors del procés.
  5. El bucle de col·lisió de noms. Dues còpies al mateix segon tindrien el mateix nom; el sufix -1, -2 ho 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 readAttributesuna 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats