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
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.
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
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
In the modern nio.file API, what does java.nio.file.Path primarily replace from the older java.io package?
Path replaces java.io.File as the representation of a filesystem location. The actual operations (read, write, check) move to the Files utility class.
Which Files method reads an entire text file into a List<String>?
Files.readAllLines returns a List<String> containing every line. Files.readString returns a single String. Files.lines returns a Stream, not a List.
If the target file does not exist, what does Files.readAllLines(path) throw?
Files.readAllLines throws NoSuchFileException (a subclass of IOException) when the file is missing. FileNotFoundException is from the older java.io API.
How should you build a path so that it uses the correct separator on both Windows and Linux?
Paths.get("dir", "file.txt") joins the parts with the OS-specific separator. Hard-coding any separator string is not portable.
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?
Checking existence before reading is a race condition — the file can be deleted between the two calls. The robust pattern is to attempt the operation and catch the specific exception.
Recap
java.nio.file.Pathreplacesjava.io.Fileas the modern way to represent a filesystem location.Paths.getbuilds portable paths using the correct separator for the current OS.Files.readAllLines,Files.writeString, andFiles.existsare 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.