Stage 3: Core APIs, lesson 9 of 12

Files and I/O with NIO.2

Intermediate3 min read@since 11Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Modern file code uses java.nio.file (Java 7+):

  • Path.of("data", "orders.csv") builds a path that works on every operating system.
  • Files.readString(path) and Files.writeString(path, text) (Java 11) for small files.
  • Files.lines(path) streams large files lazily, one line at a time.
  • Files.newBufferedReader and Files.newBufferedWriter for line-by-line control.
  • Files.copy, Files.move, Files.delete and Files.createDirectories.
  • Files.walk(dir) visits a whole folder tree.

UTF-8 is the default charset since Java 18. Close streams with try-with-resources: Files.lines and Files.walk hold an open file handle until closed.

The older java.io.File class still works but reports errors poorly, often by returning false; prefer Path and Files.

Byte streams vs character streams

InputStream/OutputStream move raw bytes (images, zip files). Reader/Writer move characters and handle encoding (text). Always choose the encoding for text; UTF-8 is the default since Java 18.

Buffered reading and writing

Reading one byte or character at a time is slow. Buffered readers and writers read and write in large chunks. Files.newBufferedReader and Files.newBufferedWriter give you buffered UTF-8 by default.

Java
try (BufferedReader in = Files.newBufferedReader(Path.of("app.log"))) {
    String line;
    while ((line = in.readLine()) != null) {
        if (line.contains("ERROR")) System.out.println(line);
    }
}

Path and Files essentials

Files covers the everyday operations: exists, createDirectories, copy, move, delete, size, getLastModifiedTime and isDirectory. Options such as REPLACE_EXISTING control behaviour.

Java
Path src = Path.of("report.pdf");
Path dest = Path.of("archive", "2026", "report.pdf");
Files.createDirectories(dest.getParent());
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
long bytes = Files.size(dest);
Files.deleteIfExists(src);

Java serialization

A class that implements Serializable can be written to bytes with ObjectOutputStream and read back with ObjectInputStream. Mark fields to skip as transient, and declare a serialVersionUID. Deserialising untrusted data is a well-known security risk, so for storage and APIs prefer JSON (Jackson) or another explicit format.

Java
record Draft(String title, String body) implements Serializable {
    private static final long serialVersionUID = 1L;
}

try (var out = new ObjectOutputStream(Files.newOutputStream(Path.of("draft.bin")))) {
    out.writeObject(new Draft("Hello", "World"));
}

Walking folders and watching for changes

Files.walk visits a folder tree lazily and Files.find filters while walking. A WatchService tells you when files are created, changed or deleted in a folder.

Java
try (Stream<Path> files = Files.find(Path.of("src"), 10, (p, attr) -> p.toString().endsWith(".java"))) {
    System.out.println(files.count() + " Java files");
}

Reading files from the classpath

Files packaged inside your JAR (under src/main/resources) aren't normal files on disk. Read them as resources with getResourceAsStream.

Java
try (InputStream in = App.class.getResourceAsStream("/templates/welcome.txt")) {
    String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}

Example

Java
Path dir = Path.of("reports");
Files.createDirectories(dir);

Path file = dir.resolve("summary.txt");
Files.writeString(file, "Total orders: 42
");                        // Java 11+
Files.writeString(file, "Refunds: 3
", StandardOpenOption.APPEND);
String text = Files.readString(file);

Path csv = Path.of("orders.csv");
try (Stream<String> lines = Files.lines(csv)) {                       // lazy, closed after use
    double revenue = lines.skip(1)                                    // skip the header row
        .map(line -> line.split(","))
        .mapToDouble(cols -> Double.parseDouble(cols[2]))
        .sum();
    System.out.println(revenue);
}

try (Stream<Path> tree = Files.walk(Path.of("src"))) {
    long javaFiles = tree.filter(p -> p.toString().endsWith(".java")).count();
}

Common mistake

Forgetting to close Files.lines or Files.walk. Each keeps a file handle open until closed; use try-with-resources.

Under the hood

Files.lines reads one line at a time, so a 5 GB log never loads into memory. For real CSV files use a library (Apache Commons CSV, OpenCSV), because quoted fields can contain commas. In web apps that run several instances, don't save user uploads to one server's disk; use object storage such as S3.

Check yourself

Which reads a large file lazily, line by line?

How this connects

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.