Java NIO Files Class
High-level file system operations in Java with java.nio.file.Files — read, write, copy, move, walk.
Java NIO Files Class
Path (the previous chapter) was the noun. Files is the verb — a static utility class whose every method takes a Path and does something to the file at that path. It's the home of the one-liners that have been quietly making the rest of this part shorter: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. This chapter walks the full catalogue.
Files is large — about 80 methods — and grouped by purpose: read, write, create, inspect, modify, walk. You don't have to memorise it; you have to know it's the first place to look when you want to do anything to a file.
Read
The whole-file readers are one line each:
String text = Files.readString(path); // UTF-8 by default (Java 11+)
String utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);For files small enough to fit in memory, readString and readAllBytes are the right tools. They open the file, read it all, close it, and hand you the content. No streams, no buffers, no closing logic.
For files too large to load whole, use the streaming forms:
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) process(line);
}
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(...).forEach(...); // closes the file when the stream closes
}
try (InputStream in = Files.newInputStream(path)) {
// raw bytes for binary formats
}Files.lines is BufferedReader.lines with the open-close plumbing wrapped in. The try-with-resources around the Stream does the closing — without it, the file handle leaks.
Write
Same shape on the write side:
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>All three are one-call atomics: open, write, close. By default they create or truncate — if the file existed, its previous contents are gone. For append:
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);For the streaming form (write incrementally):
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Open options
Every read/write method that opens a file accepts an optional varargs of StandardOpenOption:
| Option | Meaning |
|---|---|
READ | Open for reading |
WRITE | Open for writing |
CREATE | Create if absent; do nothing if present |
CREATE_NEW | Create if absent; fail if present |
APPEND | Writes go to the end of the file |
TRUNCATE_EXISTING | Clear contents on open |
DELETE_ON_CLOSE | Delete when the channel closes (temp files) |
SYNC / DSYNC | Block writes until the OS confirms the data is on disk |
The default open mode for newBufferedWriter and writeString is CREATE, TRUNCATE_EXISTING, WRITE. The default for newBufferedReader and readString is READ. Explicit options override the defaults — passing any option turns off the implicit set, so you typically need to repeat the implicit ones when you customise:
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentCreate
Files.createFile(path); // empty file; fails if it exists
Files.createDirectory(path); // single dir; fails if parent absent
Files.createDirectories(path); // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target); // hard link
Path tmpFile = Files.createTempFile("prefix-", ".txt"); // in the default temp dir
Path tmpDir = Files.createTempDirectory("prefix-");createDirectories is the right tool for "I want this directory to exist." It's idempotent: if the directory is already there, it returns without error; if any ancestor is missing, it creates the whole chain. createDirectory (no -ies) does only one level and fails if the parent doesn't exist — almost always wrong unless you specifically need that check.
For temp files, the createTempFile and createTempDirectory overloads pick the system temp directory automatically and return the Path they created. Pair them with .toFile().deleteOnExit() for cleanup, or do explicit Files.delete in a finally.
Inspect
The predicates and accessors:
boolean ok = Files.exists(path);
boolean nope = Files.notExists(path); // NOT the negation of exists
boolean file = Files.isRegularFile(path);
boolean dir = Files.isDirectory(path);
boolean link = Files.isSymbolicLink(path);
boolean read = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec = Files.isExecutable(path);
long size = Files.size(path); // throws IOException
FileTime mtime = Files.getLastModifiedTime(path);
String mimeType = Files.probeContentType(path); // best-effort, can return null
UserPrincipal owner = Files.getOwner(path);exists and notExists aren't negations: both can return false when access to the file can't be determined (permission denied, dangling symlink). Use the right one for what you want — !exists(p) and notExists(p) differ in edge cases.
Copy, move, delete
Files.copy(source, target); // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // copy mtime/owner too
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // rename within a filesystem; rename-or-fail
Files.delete(path); // throws if absent
boolean deleted = Files.deleteIfExists(path); // idempotentFiles.move with ATOMIC_MOVE is the right tool for "write to a temp file, then atomically replace the live file." On the same filesystem it maps to rename(2); the live file flips from old to new at one instant, with no in-between state. This is how you build crash-safe writes:
Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);If the JVM dies after writeString but before move, the live file is untouched.
List and walk
try (Stream<Path> entries = Files.list(directory)) {
entries.forEach(System.out::println); // direct children only
}
try (Stream<Path> tree = Files.walk(directory)) {
tree.filter(Files::isRegularFile).forEach(...); // recursive
}
try (Stream<Path> tree = Files.walk(directory, 2)) { // depth-limited
...
}
try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
...
}Always try-with-resources around these — the underlying DirectoryStream is open until the Stream closes. Skip the close and the JVM holds a directory handle until garbage collection notices, which on a long-running process is "never." The next chapter, Java Walk File Tree, goes deeper on the walker.
Why this chapter is short
Files doesn't need much narrative. Every method does one thing, the names are descriptive, the parameters are Path and Charset and Option. The cognitive load is in the catalogue — knowing what's available — not in any single method's behaviour. Skim the Javadoc for java.nio.file.Files once; come back when you need a verb you don't remember.
A worked example: the full lifecycle
The program below creates a temp directory, writes a small text file with writeString, reads it back with readString, appends with the right open option, copies the file, moves it atomically, lists the directory at every step, and finally cleans up with deleteIfExists. It's the everyday-Java file lifecycle compressed into one main method.
What to take from the run:
Files.writeString(...)opened the file, wrote the content, and closed it — one call wherejava.iowould have wantedFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources. The truncate-on-open default is exactly what "save this content" wants. When you need to keep the existing content, the explicitStandardOpenOption.APPEND(passed alongsideWRITE) is the override.Files.lines(log).filter(...)did the same streaming-read job asBufferedReader.lines()with the open-close plumbing wrapped in. Thetry-with-resources around theStreamis the closing mechanism — skip it and the file handle leaks. Every method onFilesthat returns aStreamis closeable; treat it that way.- The copy step used both
REPLACE_EXISTING(allow overwrite) andCOPY_ATTRIBUTES(carry the mtime/owner along). WithoutCOPY_ATTRIBUTESthe backup would have a fresh mtime, which matters for "is this backup still current?" checks.Files.copydefaults to the conservative behaviour; you opt in to anything else. - The atomic-move block is the safe-write pattern: write content to
target.tmp, thenATOMIC_MOVEit onto the live name. If the JVM crashes mid-write, the live file is unchanged; if the rename succeeds, the live file flips at one instant. On the same filesystem this maps torename(2)— there's no copy step. Use this for any file where readers should never see a half-written state (configuration, save files, generated assets). Files.walk(dir)produced aStream<Path>of every entry under the directory in depth-first order. The cleanup at step 10 sorted reverse so children deleted before parents — the same trick you'd use with a real recursive delete. (The full delete-tree helper lives in the next chapter underwalkFileTree; the streaming form here is the shorter version for small trees.)
What's next
Files covered the operations that act on a single file or a single directory level. The next chapter, Java Walk File Tree, goes deeper on traversing a whole directory tree — Files.walkFileTree, FileVisitor, skipping subtrees, the visitor-pattern API that handles the cases the Stream form can't.
Practice
You want to overwrite the file `/var/data/config.json` with a new payload, but readers must never see a half-written state if the JVM crashes mid-write. Which sequence of `Files` calls implements the safe-write pattern?