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