skip to content
everything.io.filesAndPaths

files and paths

Path, Files, walking trees, and watching directories.

chapter
chapter 10.03
reading time
7 min read

a Path is just a name: a root and a list of name elements. making one never touches the disk. Files is the toolbox of static methods that actually read, write, copy, walk and watch. together they replaced java.io.File java 7+, which mixed both jobs up and answered errors with false.

the picture

you know how new File("data.txt") "works" even when there's no such file? that's because it was never a file. it's an address on an envelope. you can write any address you like; whether a house is there is a separate question.

Path makes that split explicit. the path is the address (pure text with structure). Files is the postman (the one who actually walks there, and comes back with an exception if nobody's home).

op =
Path p = Path.of("/home/ada/projects/../notes/./todo.txt");
/roothome0/ada1/projects2/..3/notes4/.5/todo.txt6
getRoot() → /getFileName() → todo.txtgetNameCount() → 7subpath(1, 3) → ada/projects (outlined)getParent() → /home/ada/projects/../notes/.

a path is a root plus a list of name elements, indexed from 0. the root isn't a name, which is why the count is 7. getParent() just drops the last name: it doesn't clean up .., and it doesn't look at the disk.

pick an operation. everything here is string surgery on the name: none of these paths exist. the root isn't a name element, struck segments are the ones normalize removes, faded segments are a shared prefix that relativize drops.
Main.java
import java.nio.file.*;
 
void main() {
    Path p = Path.of("does/not/exist.txt");    // fine. it's just a name
    IO.println(p.getFileName() + " " + Files.exists(p));
    IO.println(p.toAbsolutePath().startsWith(System.getProperty("user.dir")));
}
output, from jdk 25
exist.txt false
true

this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.

predict the output
java
IO.println(Path.of("a/b").resolve("/c"));

the Files toolbox

every method is static, takes Paths, throws a specific IOException subclass when something goes wrong, and defaults to UTF-8 for text.

Main.java
import java.nio.file.*;
import java.util.List;
 
void main() throws Exception {
    Path dir = Files.createTempDirectory("demo");
    Path todo = dir.resolve("todo.txt");
 
    IO.println(Files.exists(todo));
    Files.writeString(todo, "buy milk\nship it\n");
    Files.writeString(todo, "call mom\n", StandardOpenOption.APPEND);
 
    List<String> lines = Files.readAllLines(todo);
    IO.println(lines + " " + Files.size(todo) + " bytes");
 
    Path copy = Files.copy(todo, dir.resolve("todo.bak"));
    IO.println(Files.mismatch(todo, copy));          // -1 means identical
 
    try (var files = Files.list(dir)) {
        IO.println(files.map(p -> p.getFileName().toString()).sorted().toList());
    }
    Files.delete(copy); Files.delete(todo); Files.delete(dir);
}
output, from jdk 25
false
[buy milk, ship it, call mom] 26 bytes
-1
[todo.bak, todo.txt]

this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.

jobcall
small text file in / outreadString, writeString, readAllLines
big text file, line by linelines (a lazy Stream, close it) or newBufferedReader
bytesreadAllBytes, write, newInputStream, newOutputStream
copy / movecopy(a, b, REPLACE_EXISTING), move(a, b, ATOMIC_MOVE)
make directoriescreateDirectory (parent must exist), createDirectories (makes the chain)
deletedelete (throws if missing), deleteIfExists
ask questionsexists, isDirectory, isReadable, size, getLastModifiedTime
comparemismatch(a, b) java 12+: index of the first different byte, or -1
scratch spacecreateTempFile, createTempDirectory

StandardOpenOption is how you say how to open: CREATE, APPEND, TRUNCATE_EXISTING, CREATE_NEW (fail if it's already there, the safe way to claim a file name). the defaults for writeString are CREATE, TRUNCATE_EXISTING, WRITE: it replaces the file.

why not java.io.File?

File came with java 1.0 and has three problems you'll hit: most failures return false instead of telling you why, it can't do symlinks, permissions or atomic moves properly, and a few methods behave differently per OS. Path + Files arrived in java 7 (NIO.2, JSR 203) to fix all of it.

Main.java
import java.io.File;
import java.nio.file.*;
 
void main() throws Exception {
    Path dir = Files.createTempDirectory("full");
    Files.writeString(dir.resolve("a.txt"), "hi");
 
    IO.println("File.delete: " + new File(dir.toString()).delete());
    try {
        Files.delete(dir);
    } catch (Exception e) {
        IO.println("Files.delete: " + e.getClass().getSimpleName());
    }
    try {
        Files.delete(dir.resolve("nope.txt"));
    } catch (Exception e) {
        IO.println("Files.delete: " + e.getClass().getSimpleName());
    }
    Files.delete(dir.resolve("a.txt"));
    Files.delete(dir);
}
output, from jdk 25
File.delete: false
Files.delete: DirectoryNotEmptyException
Files.delete: NoSuchFileException

this one needs a real JVM (catching a runtime exception), so it doesn't run in the browser. copy it into java Main.java to try it.

false, why? you'll never know. Files names the actual problem. when old APIs hand you a File, convert at the edge: file.toPath() and path.toFile() go both ways.

walking a tree

Files.walk(root) gives you a lazy Stream<Path> of everything under root, depth-first, starting with root itself. Files.list(dir) is the one-level version. Files.find is walk plus a filter that also sees each file's attributes.

maxDepth =
try (Stream<Path> s = Files.walk(root)) { ... }
on disk
  • site/
  • build.gradle
  • docs/
  • readme.md
  • src/
  • main/
  • App.java
  • Util.java
what the stream hands you, in order
  1. nothing yet. the stream is lazy: no disk access until you pull.

press step. walk starts at the root itself, then goes depth-first.

step 1/9
step through a walk, then change maxDepth. a directory is entered the moment it's found, before its siblings: that's depth-first. the current path is the one the stream just handed out.
Main.java
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
import java.util.stream.Stream;
 
void main() throws IOException {
    Path root = Files.createTempDirectory("site");
    Files.createDirectories(root.resolve("src/main"));
    Files.createDirectories(root.resolve("docs"));
    Files.writeString(root.resolve("src/main/App.java"), "class App {}");
    Files.writeString(root.resolve("src/main/Util.java"), "class Util {}");
    Files.writeString(root.resolve("docs/readme.md"), "# hi");
    Files.writeString(root.resolve("build.gradle"), "");
 
    try (Stream<Path> all = Files.walk(root)) {
        all.map(root::relativize).map(Path::toString).sorted().forEach(p -> IO.println("[" + p + "]"));
    }
 
    try (Stream<Path> java = Files.find(root, 10, (p, attrs) -> p.toString().endsWith(".java"))) {
        IO.println("java files: " + java.count());
    }
 
    // delete the whole tree: children before parents, so walk in reverse order
    try (Stream<Path> all = Files.walk(root)) {
        all.sorted(Comparator.reverseOrder()).forEach(p -> {
            try { Files.delete(p); } catch (IOException e) { throw new RuntimeException(e); }
        });
    }
    IO.println("deleted: " + !Files.exists(root));
}
output, from jdk 25
[]
[build.gradle]
[docs]
[docs/readme.md]
[src]
[src/main]
[src/main/App.java]
[src/main/Util.java]
java files: 2
deleted: true

this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.

the empty [] at the top is root relativized against itself. and the reverse-sorted delete is the standard trick for rm -rf: a directory has to be empty before Files.delete accepts it, and reverse lexical order puts every child before its parent.

for more control (skip a subtree, handle errors per file, act on the way out of a directory), use Files.walkFileTree with a FileVisitor. it's the callback version of the same depth-first walk.

watching a directory

a WatchService tells you when entries in a directory are created, modified or deleted, so you can react to a dropped file instead of polling yourself.

hover a stage.

hover a stage. one WatchKey per registered directory. it collects events until you drain it, and stops reporting until you reset() it.
Main.java
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
import static java.nio.file.StandardWatchEventKinds.*;
 
void main() throws Exception {
    Path dir = Files.createTempDirectory("inbox");
    try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
        dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
 
        Files.createFile(dir.resolve("order-42.json"));
 
        WatchKey key = watcher.poll(10, TimeUnit.SECONDS);
        for (WatchEvent<?> e : key.pollEvents()) {
            IO.println(e.kind() + " " + e.context());
        }
        IO.println("still valid: " + key.reset());
    }
    Files.delete(dir.resolve("order-42.json"));
    Files.delete(dir);
}
output, from jdk 25
ENTRY_CREATE order-42.json
still valid: true

this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.

one level deeper:a zip file is a filesystem too+

Path isn't tied to your disk. it belongs to a FileSystem, and the JDK ships a provider that treats a zip (or jar) as one. same Files calls, different world:

Main.java
import java.nio.file.*;
import java.util.Map;
 
void main() throws Exception {
    Path zip = Files.createTempDirectory("z").resolve("notes.zip");
 
    try (FileSystem fs = FileSystems.newFileSystem(zip, Map.of("create", "true"))) {
        Path inside = fs.getPath("/2026/october.txt");
        Files.createDirectories(inside.getParent());
        Files.writeString(inside, "ship the io chapters");
    }
 
    try (FileSystem fs = FileSystems.newFileSystem(zip)) {
        Path inside = fs.getPath("/2026/october.txt");
        IO.println(Files.readString(inside));
        IO.println(inside.getFileSystem().provider().getScheme());
    }
}
output, from jdk 25
ship the io chapters
jar

this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.

that's also how the JDK reads its own class library: jrt:/java.base/java/lang/String.class is a path in the jrt filesystem, backed by the lib/modules image.

keylines
  • a Path is a name. creating one never touches the disk.
  • Files does the work, and throws an exception that says what went wrong.
  • resolve glues paths, unless the other one is absolute. normalize is textual. relativize is the inverse.
  • relative paths resolve against the working directory, not your code's location.
  • walk, list, find, lines: lazy streams holding OS handles. try-with-resources.
  • WatchService: one directory, not recursive, polls every 2 s on macOS.