Java NIO Path Class
Rappresenta i percorsi del filesystem in Java moderno con java.nio.file.Path e la factory Paths.
Path è il sostituto moderno di java.io.File. Rappresenta un percorso del filesystem — una sequenza ordinata di componenti di nome, opzionalmente radicata in / o C:\ — e nient'altro. Non legge il file. Non verifica che il file esista. Non blocca nulla. Le operazioni sui byte su disco si trovano in Files (il capitolo successivo). Path è il sostantivo; Files è il verbo.
Se hai già usato java.io.File, la differenza è duplice: Path è immutabile (ogni operazione restituisce un nuovo Path), e separa nettamente "la stringa del percorso" da "ciò che si trova su disco a quel percorso." La maggior parte delle API moderne — Files, FileChannel, overload di BufferedReader.lines() — accettano Path, non File. Il nuovo codice usa Path.
Costruire un Path
Path p = Path.of("logs", "2025", "app.log"); // joins components with the platform separator
Path q = Path.of("/etc/hosts"); // absolute Unix path
Path r = Paths.get("C:", "Users", "vaz"); // older factory — same behaviour
Path s = Path.of(URI.create("file:///etc/hosts")); // from a URIPath.of(...) è la factory moderna; Paths.get(...) è quella precedente e funziona ancora. Entrambe costruiscono un oggetto path senza toccare il filesystem. Path.of("nope/nope/nope") ha successo anche se non esiste alcun file del genere.
Path.of unisce i varargs con il File.separator della piattaforma — / su Unix, \ su Windows. Questo rende portabile il percorso letterale che scrivi: Path.of("src", "main", "java") costruisce la cosa giusta su entrambi i sistemi. Nel momento in cui scrivi Path.of("src/main/java") con slash hardcoded, l'hai resa Unix-only per sbaglio.
Ispezionare un percorso
I metodi di accesso ai componenti, su Path.of("/var/log/app/today.log"):
| Metodo | Restituisce | Esempio |
|---|---|---|
getFileName() | ultimo componente come Path | today.log |
getParent() | tutto tranne l'ultimo | /var/log/app |
getRoot() | la radice, o null se relativo | / |
getNameCount() | numero di componenti di nome | 4 |
getName(int i) | i-esimo componente | getName(0) → var |
subpath(b, e) | componenti di nome [b, e) | subpath(1, 3) → log/app |
isAbsolute() | se il percorso ha una radice | true |
toString() | la stringa formattata dalla piattaforma | /var/log/app/today.log |
Questi sono puri: esaminano la lista interna dei nomi dell'oggetto path e ne restituiscono porzioni. Nessuno di essi tocca il disco.
resolve, resolveSibling e la trappola dell'argomento assoluto
resolve(other) significa "unisci this e other":
Path base = Path.of("/var/log");
base.resolve("app.log"); // /var/log/app.log
base.resolve("app/today.log"); // /var/log/app/today.logLa trappola: se other è assoluto, resolve restituisce other invariato:
base.resolve("/etc/hosts"); // /etc/hosts -- base is discarded
base.resolve(Path.of("/etc/hosts")); // same: /etc/hostsQuesto è il comportamento documentato, e colpisce ogni programmatore Java almeno una volta. Se accetti un nome di file dall'input dell'utente e lo risolvi rispetto a una directory base configurata, un attaccante che fornisce "/etc/passwd" ottiene il suo percorso assoluto — sfuggendo alla base. Valida o normalizza sempre l'input esterno prima di usare resolve.
resolveSibling(other) sostituisce l'ultimo componente:
Path p = Path.of("/var/log/today.log");
p.resolveSibling("yesterday.log"); // /var/log/yesterday.logÈ getParent().resolve(other) con un controllo null incorporato. Utile per "scrivi l'output accanto all'input."
relativize: l'inverso
Dato un base e un target, base.relativize(target) restituisce il percorso relativo da base a target:
Path base = Path.of("/var/log");
Path target = Path.of("/var/log/app/today.log");
base.relativize(target); // app/today.log
target.relativize(base); // ../..Il contratto: base.resolve(base.relativize(target)) è equivalente a target (modulo normalize). È il modo per trasformare un target assoluto in un riferimento breve e relativo all'interno di una directory base — utile per righe di log, voci di archivio e URL.
Entrambi i percorsi devono essere dello stesso tipo (entrambi assoluti o entrambi relativi) e devono provenire dallo stesso FileSystem. Mescolarli lancia IllegalArgumentException.
normalize: collassa . e ..
Gli oggetti Path permettono componenti . e .. — Path.of("/var/log/../tmp") è un Path valido. normalize() li rimuove sintatticamente:
Path.of("/var/log/../tmp").normalize(); // /var/tmp
Path.of("./a/./b/../c").normalize(); // a/c"Sintatticamente" è importante: normalize lavora a livello di stringa. Non chiede al filesystem se .. punta davvero dove le stringhe suggeriscono. Se /var/log è un link simbolico a /tmp/logs, allora su disco /var/log/.. è /tmp, non /var. normalize() non lo sa — si limita a eliminare il ...
Quando hai bisogno del percorso reale su disco (symlink risolti, .. interpretato correttamente), usa toRealPath(), che è una chiamata che tocca il filesystem:
Path real = Path.of("/var/log/../tmp").toRealPath(); // resolves symlinks, throws if the file doesn't existPer i confronti di uguaglianza tra percorsi e confronti di stringhe, normalize() è ciò che vuoi. Per "il nome canonico del file su disco in questo momento," è toRealPath().
L'uguaglianza è basata sulle stringhe
path1.equals(path2) confronta i percorsi come stringhe (componente per componente). Non normalizza, non risolve symlink, non controlla il filesystem:
Path.of("/var/log").equals(Path.of("/var/log/.")); // false -- one has a trailing '.' component
Path.of("/var/log/.").equals(Path.of("/var/log/.").normalize()); // false -- normalize() dropped the '.'
Path.of("/var/log").equals(Path.of("/var/log").normalize()); // true -- already normalized, no change
Path.of("/var/log").equals(Path.of("/var/log")); // truePer confrontare due percorsi come "puntano allo stesso file," normalizza entrambi e confronta, oppure chiama Files.isSameFile(p1, p2) (che tocca il filesystem, l'unico controllo completamente corretto). Per ordinamento e chiavi HashSet, l'uguaglianza basata sulle stringhe è ciò che Path ti fornisce; va bene per la maggior parte degli usi ma non significa "stesso file su disco."
Interoperabilità con File
Path e File si convertono in entrambe le direzioni:
File f = Path.of("/etc/hosts").toFile();
Path p = new File("/etc/hosts").toPath();Ne avrai bisogno quando una vecchia API accetta File e una nuova API accetta Path (o viceversa). Non memorizzare i percorsi come File; converti al confine dell'API e mantieni Path nel tuo codice.
Un esempio completo: join, resolve, relativize, normalize
Il programma seguente percorre ogni operazione su Path introdotta in questo capitolo usando percorsi concreti. L'output rende visibile la differenza tra resolve e resolveSibling, tra normalize e toRealPath, e tra resolve con argomento assoluto e con argomento relativo.
Cosa trarre dall'esecuzione:
Path.of(\"/var\", \"log\", \"app\", \"today.log\")ha prodotto/var/log/app/today.logsu Unix e\\var\\log\\app\\today.logsu Windows. Lasciare che la factory con varargs unisca i componenti è il modo portabile; gli slash o backslash hardcoded nella stringa di input sono il modo non portabile. Usa la factory.- La riga
resolve(\"/etc/hosts\")ha scartatobasee restituito/etc/hosts. Questo è il comportamento con argomento assoluto e la fonte più frequente di "ma ho fornito una directory base, perché sta scrivendo in/etc/hosts?" Valida sempre i nomi di file forniti dall'utente prima di usareresolve. La forma difensiva èbase.resolve(other).normalize().startsWith(base)— e anche quella ha sottili lacune quando sono coinvolti i symlink. base.relativize(target)ha restituitoapp/today.log. Concatenandolo di nuovo conbase.resolve(...)si è ottenuto il target originale — l'identità di andata e ritorno. Usalo quando scrivi un messaggio di log o una voce di archivio che necessita di una forma breve e relativa di un lungo percorso assoluto.Path.of(\"/var/log/../tmp/./a/b/../c\").normalize()ha prodotto/var/tmp/a/c. La trasformazione era puramente a livello di stringa: ogni.rimosso, ogni coppianome/..rimossa. Il filesystem non è stato consultato.toRealPathnella riga successiva ha invece consultato il filesystem — ecco perché il risultato era il nome canonico, con symlink risolti, del file effettivo su disco. (Su macOS vedrai il percorso temporaneo restituito radicato in/private/var/folders/...anziché/var/folders/...:toRealPathha seguito il vero symlink/var→/private/var, cosa chenormalizenon può mai fare.) PoichétoRealPathcontrolla ogni componente, la directory intermediasubnell'esempio deve effettivamente esistere — ecco perché il programma la crea prima.- I due controlli
equals:Path.of(\"/var/log\")ePath.of(\"/var\", \"log\")erano uguali (stessa sequenza interna di nomi, stessa stringa);Path.of(\"/var/log\")ePath.of(\"/var/log/.\")non lo erano (uno ha un componente.finale, l'altro no). La lezione:equalsè un confronto di stringhe. Per "questi due percorsi puntano allo stesso file?" usaFiles.isSameFile(a, b)dal prossimo capitolo — è l'unico controllo che interroga il filesystem.
Cosa viene dopo
Path è il sostantivo. Il prossimo capitolo, Java Files Class, copre il verbo — Files, la gigantesca classe di utilità con operazioni in una riga sul filesystem: readString, writeString, createDirectories, copy, move, delete, walk, e altro ancora.