W3docs

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:

OptionMeaning
READOpen for reading
WRITEOpen for writing
CREATECreate if absent; do nothing if present
CREATE_NEWCreate if absent; fail if present
APPENDWrites go to the end of the file
TRUNCATE_EXISTINGClear contents on open
DELETE_ON_CLOSEDelete when the channel closes (temp files)
SYNC / DSYNCBlock 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 absent

Create

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);                       // idempotent

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

java— editable, runs on the server

What to take from the run:

  • Files.writeString(...) opened the file, wrote the content, and closed it — one call where java.io would have wanted FileOutputStream + 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 explicit StandardOpenOption.APPEND (passed alongside WRITE) is the override.
  • Files.lines(log).filter(...) did the same streaming-read job as BufferedReader.lines() with the open-close plumbing wrapped in. The try-with-resources around the Stream is the closing mechanism — skip it and the file handle leaks. Every method on Files that returns a Stream is closeable; treat it that way.
  • The copy step used both REPLACE_EXISTING (allow overwrite) and COPY_ATTRIBUTES (carry the mtime/owner along). Without COPY_ATTRIBUTES the backup would have a fresh mtime, which matters for "is this backup still current?" checks. Files.copy defaults to the conservative behaviour; you opt in to anything else.
  • The atomic-move block is the safe-write pattern: write content to target.tmp, then ATOMIC_MOVE it 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 to rename(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 a Stream<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 under walkFileTree; 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

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?