Files and NIO

Path, Files, buffered streams, charsets, and reading a large file without loading it — the API you actually use, not the 1998 one.

3 min read🧰 Exceptions, I/O and Reflection

Java has two file APIs. The 1996 one — java.io.File, FileInputStream, FileReader — still works and still appears in tutorials. The 2011 one — java.nio.file — is the one to use: Path instead of File, Files for every operation, and streams that are closed by try-with-resources. Backends read configuration, write exports, and process uploads; each of those is a place to leak a handle or load a gigabyte into memory by accident.

Path and Files

java
Path dir = Path.of("/var/data/exports");
Path file = dir.resolve("orders-2024-03.csv");        // /var/data/exports/orders-2024-03.csv
file.getFileName();                                   // orders-2024-03.csv
file.getParent();                                     // /var/data/exports
Files.exists(file); Files.isDirectory(dir); Files.size(file);
Files.createDirectories(dir);                         // mkdir -p
Files.delete(file); Files.deleteIfExists(file);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);

A Path is a value — immutable, comparable, no I/O until you hand it to Files. resolve joins; relativize finds the difference; normalize removes .. segments. Never build paths from user input with string concatenation: dir.resolve(userInput).normalize().startsWith(dir) is the check that stops ../../etc/passwd.

Reading, small and large

For a file that fits comfortably in memory:

java
String text = Files.readString(file);                            // UTF-8 by default since 11
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
byte[] bytes = Files.readAllBytes(file);

For a file that might not — logs, exports, uploads — stream it:

java
try (Stream<String> lines = Files.lines(file)) {                 // lazy; holds a handle
    long errors = lines.filter(l -> l.contains("ERROR")).count();
}
 
try (BufferedReader r = Files.newBufferedReader(file)) {        // buffered, UTF-8
    String line;
    while ((line = r.readLine()) != null) process(line);
}
 
try (InputStream in = Files.newInputStream(file)) {              // bytes
    byte[] buf = new byte[8192];
    int n;
    while ((n = in.read(buf)) != -1) sink.write(buf, 0, n);
}

Files.lines is a stream backed by an open file — it must be in a try. The readAll* methods are not: they close before returning.

Writing

java
Files.writeString(file, text);                                   // create or truncate
Files.write(file, lines);
Files.writeString(file, text, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
 
try (BufferedWriter w = Files.newBufferedWriter(file)) {
    for (Row row : rows) { w.write(row.toCsv()); w.newLine(); }
}

For an export that must not be seen half-written, write to a temporary file in the same directory and Files.move(tmp, target, ATOMIC_MOVE). Readers see the old file or the new one, never a partial.

Buffering and charsets

Raw InputStream/OutputStream calls hit the OS per call. Wrap them: BufferedInputStream, BufferedReader. The Files.newBuffered* methods do it for you. Readers and writers convert bytes to characters and need a charset; the Files methods default to UTF-8, the old FileReader constructor used the platform default until Java 18 — one more reason to avoid it.

Directories

java
try (Stream<Path> entries = Files.list(dir)) { ... }                // one level
try (Stream<Path> all = Files.walk(dir)) {                          // recursive
    all.filter(p -> p.toString().endsWith(".log")).forEach(this::rotate);
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "*.csv")) { ... }

All three hold handles; all three go in try.

Temporary files and resources on the classpath

Files.createTempFile("export-", ".csv") — delete it when done; deleteOnExit is unreliable in long-running servers. Files packaged in the jar are not Paths on the file system: read them as streams via getClass().getResourceAsStream("/templates/invoice.html") or Spring's ClassPathResource. A File reference into a jar does not work, and it breaks the first time the application runs from a fat jar.

Channels and memory mapping

FileChannel and MappedByteBuffer exist for very large files and zero-copy transfers (transferTo). Most services never need them; know that they are there for the day you process multi-gigabyte files and Files.lines is the bottleneck.

Progress is saved on this device and to your account when signed in.