Module 8 · Exceptions, I/O & Resources ⏱ 19 min

File I/O with java.nio.file (overview)

By the end of this lesson you will be able to:
  • Build portable paths with Paths.get and manipulate them with the Files utility class
  • Explain why file operations declare throws IOException and how to handle it
  • Avoid the hard-coded separator trap and the check-then-act race condition

Reading and writing files is where most programs touch the outside world. Before Java 7, java.io.File was the only tool, and it was limited: no clean way to read an entire text file in one call, confusing path manipulation, and methods that silently failed instead of throwing useful exceptions.

Java 7 introduced java.nio.file — a modern API built around Path, Paths, and Files. A Path is a filesystem location, not necessarily a real file yet. Paths.get builds one from strings. The Files utility class then provides one-liners for common tasks: read every line into a List<String>, write a String to disk, or check whether something exists.

The API is clearer and the exceptions are more specific, but the design decision that matters most is the separation of what you are touching from what you are doing to it. That separation also makes the API easier to learn: first you learn how to build a Path, then you learn what Files can do with it.

flowchart LR
  OLD["java.io.File"] --> NEW["java.nio.file.Path"]
  NEW --> OP["Files utility class"]
  OP --> READ["readAllLines"]
  OP --> WRITE["writeString"]
  OP --> CHECK["exists"]
  style NEW fill:#c2410c,color:#fff
  style OP fill:#c2410c,color:#fff
The modern nio.file API replaces java.io.File with Path and concentrates operations in the Files utility class.

Building a Path

Paths.get accepts a sequence of strings and joins them using the correct separator for the operating system. On Windows it uses backslash; on Linux and macOS it uses forward slash. Hard-coding a separator string like "data\\records.txt" breaks the moment the code runs on a different OS, so always build paths through Paths.get:

Path p = Paths.get("data", "records.txt");

Reading text

Files.readAllLines(p) reads every line of a text file into a List<String>. It assumes UTF-8 by default. If the file is missing, it throws NoSuchFileException — a subclass of IOException — so you always know whether the problem is absence versus a permissions failure. The method returns a mutable List, so you can sort it, filter it, or pass it directly into another method without writing a manual read loop. For very large files, Files.lines returns a Stream that you can process lazily, but readAllLines is the right starting point for files that fit comfortably in memory.

Reading a text file with java.nio.file. Real, compiling Java.
import java.nio.file.*;
import java.io.IOException;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Path p = Paths.get("config.txt");
        try {
            List<String> lines = Files.readAllLines(p);
            System.out.println("Lines: " + lines.size());
        } catch (IOException e) {
            System.out.println("Could not read file");
        }
    }
}

Writing and checking

Files.writeString(p, content) writes a String to a file, creating the file if it does not exist and overwriting it if it does. Like reading, it defaults to UTF-8. If the parent directory is missing, it throws NoSuchFileException rather than creating the directory for you — an intentional design choice that prevents accidental file sprawl.

Before you read or write, Files.exists(p) tells you whether the path points to something real. It returns false for missing files and for paths you lack permission to inspect, so a false result does not automatically mean "create it." Always handle IOException around file operations; the compiler enforces this for most Files methods because they declare throws IOException. This compile-time enforcement is a feature, not a bug. It forces you to decide what happens when the disk is full, the file is locked by another process, or the network drive disappears mid-write. Ignoring these cases produces brittle software that fails mysteriously in production.

flowchart LR
  PATH["Paths.get(#quot;data.txt#quot;)"] --> READ["Files.readAllLines"]
  READ --> LIST["List<String>"]
  READ -.-> ERR["NoSuchFileException"]
  style READ fill:#c2410c,color:#fff
  style ERR fill:#b45309,color:#fff
A read operation succeeds with a List, or fails with a specific exception that tells you why.
Writing a string to a file and checking existence. Real, compiling Java.
import java.nio.file.*;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        Path p = Paths.get("output.txt");
        try {
            Files.writeString(p, "Hello, file!");
            System.out.println("Written.");
            System.out.println("Exists: " + Files.exists(p));
        } catch (IOException e) {
            System.out.println("Write failed");
        }
    }
}
flowchart LR
  BAD["Hard-coded backslash path"] --> B["Breaks on Linux"]
  GOOD["Paths.get( multiple parts )"] --> G["Works everywhere"]
  style BAD fill:#b45309,color:#fff
  style GOOD fill:#3776ab,color:#fff
Paths.get chooses the right separator for the OS; a hard-coded string does not.
Exercise

In the modern nio.file API, what does java.nio.file.Path primarily replace from the older java.io package?

Exercise

Which Files method reads an entire text file into a List<String>?

Exercise

If the target file does not exist, what does Files.readAllLines(path) throw?

Exercise

How should you build a path so that it uses the correct separator on both Windows and Linux?

Exercise

You need to read a text file only if it exists, and print a friendly message if it does not. Which approach is most robust?

Recap

  • java.nio.file.Path replaces java.io.File as the modern way to represent a filesystem location.
  • Paths.get builds portable paths using the correct separator for the current OS.
  • Files.readAllLines, Files.writeString, and Files.exists are one-liners for common tasks.
  • File operations declare throws IOException; the compiler forces you to handle or declare it.
  • Never check existence before acting — that creates a race condition. Attempt the operation and catch the exception instead.

Next you will explore how Java saves objects to bytes and how JSON offers a more portable, human-readable alternative.

Checkpoint quiz

What is the relationship between NoSuchFileException and IOException?

What does Paths.get("a", "b", "c.txt") guarantee?

Go deeper — technical resources