Skip to main content

File I/O

Programs need to persist data beyond their runtime. The simplest way is to read and write files. Java's java.nio.file package (NIO.2) provides a clean, modern API for file operations.

Path -- representing file locations

A Path represents a file or directory location:

import java.nio.file.Path;

Path file = Path.of("data.txt");
Path absolute = Path.of("/home/user/documents/report.txt");
Path nested = Path.of("data", "users", "config.json");

System.out.println(file);
System.out.println(absolute);
System.out.println(nested);
System.out.println(file.toAbsolutePath());
System.out.println(nested.getFileName());
System.out.println(nested.getParent());

Result:

data.txt
/home/user/documents/report.txt
data/users/config.json
/current/working/directory/data.txt
config.json
data/users

Path is just a representation -- the file does not need to exist yet.

Files -- the workhorse class

java.nio.file.Files provides static methods for all file operations. Most methods are one-liners.

Reading files

Read the entire file as a string

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

try {
String content = Files.readString(Path.of("example.txt"));
System.out.println(content);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Read all lines as a list

try {
var lines = Files.readAllLines(Path.of("example.txt"));
System.out.println("Lines: " + lines.size());

for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Read line by line (memory-efficient for large files)

try (var reader = Files.newBufferedReader(Path.of("large-file.txt"))) {
String line;
int count = 0;
while ((line = reader.readLine()) != null) {
count++;
if (count <= 3) {
System.out.println(line);
}
}
System.out.println("Total lines: " + count);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Files.readString and readAllLines load the entire file into memory. For files larger than a few hundred MB, use newBufferedReader or lines():

try (var stream = Files.lines(Path.of("large-file.txt"))) {
long count = stream.count();
System.out.println("Lines: " + count);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Writing files

Write a string

import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;

try {
String content = "Hello, file!\nSecond line.\nThird line.";
Files.writeString(Path.of("output.txt"), content);
System.out.println("File written");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Result:

File written

The file output.txt now contains:

Hello, file!
Second line.
Third line.

Write a list of lines

import java.util.List;

try {
List<String> lines = List.of("Line 1", "Line 2", "Line 3");
Files.write(Path.of("output.txt"), lines);
System.out.println("Lines written");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Append to a file

import java.nio.file.StandardOpenOption;

try {
Files.writeString(
Path.of("log.txt"),
"New log entry\n",
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
System.out.println("Appended");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

StandardOpenOption.CREATE creates the file if it does not exist. APPEND adds to the end instead of overwriting.

Write line by line (BufferedWriter)

For writing many lines efficiently:

try (var writer = Files.newBufferedWriter(Path.of("output.txt"))) {
for (int i = 1; i <= 5; i++) {
writer.write("Line " + i);
writer.newLine();
}
System.out.println("Written with BufferedWriter");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}

Result:

Written with BufferedWriter

Checking file properties

Path path = Path.of("example.txt");

System.out.println("Exists: " + Files.exists(path));
System.out.println("Is file: " + Files.isRegularFile(path));
System.out.println("Is directory: " + Files.isDirectory(path));
System.out.println("Is readable: " + Files.isReadable(path));
System.out.println("Is writable: " + Files.isWritable(path));

if (Files.exists(path)) {
System.out.println("Size: " + Files.size(path) + " bytes");
System.out.println("Last modified: " + Files.getLastModifiedTime(path));
}

Working with directories

Create directories

// Create a single directory
Files.createDirectory(Path.of("data"));

// Create nested directories (like mkdir -p)
Files.createDirectories(Path.of("data/users/profiles"));

List directory contents

try (var entries = Files.list(Path.of("."))) {
entries.forEach(System.out::println);
}

Walk a directory tree

try (var walk = Files.walk(Path.of("src"))) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}

This recursively finds all .java files under src/.

Delete files and directories

// Delete a file
Files.deleteIfExists(Path.of("temp.txt"));

// Delete an empty directory
Files.deleteIfExists(Path.of("empty-dir"));

Directories must be empty before deleting. To delete a directory tree, walk it in reverse order.

Copy and move

// Copy
Files.copy(Path.of("source.txt"), Path.of("copy.txt"));

// Move (rename)
Files.move(Path.of("old-name.txt"), Path.of("new-name.txt"));

// Copy with overwrite
import java.nio.file.StandardCopyOption;
Files.copy(
Path.of("source.txt"),
Path.of("dest.txt"),
StandardCopyOption.REPLACE_EXISTING
);

Simple CSV parsing

CSV (Comma-Separated Values) is a common plain-text data format. For simple cases, you can parse it manually:

Reading CSV

Given a file users.csv:

name,age,city
Ada,36,London
Bob,25,Berlin
Charlie,30,Tokyo
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

record User(String name, int age, String city) {}

static List<User> readUsers(Path file) throws IOException {
List<User> users = new ArrayList<>();
List<String> lines = Files.readAllLines(file);

// Skip header (first line)
for (int i = 1; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (line.isEmpty()) continue;

String[] parts = line.split(",");
if (parts.length != 3) {
System.err.println("Skipping invalid line: " + line);
continue;
}

String name = parts[0].trim();
int age = Integer.parseInt(parts[1].trim());
String city = parts[2].trim();
users.add(new User(name, age, city));
}

return users;
}

public static void main(String[] args) {
try {
List<User> users = readUsers(Path.of("users.csv"));
for (User user : users) {
System.out.println(user);
}
} catch (IOException e) {
System.out.println("Error reading CSV: " + e.getMessage());
}
}

Result:

User[name=Ada, age=36, city=London]
User[name=Bob, age=25, city=Berlin]
User[name=Charlie, age=30, city=Tokyo]

Writing CSV

static void writeUsers(Path file, List<User> users) throws IOException {
List<String> lines = new ArrayList<>();
lines.add("name,age,city"); // header

for (User user : users) {
lines.add(user.name() + "," + user.age() + "," + user.city());
}

Files.write(file, lines);
}

public static void main(String[] args) {
List<User> users = List.of(
new User("Diana", 28, "Paris"),
new User("Eve", 32, "Madrid")
);

try {
writeUsers(Path.of("new-users.csv"), users);
System.out.println("CSV written");

// Verify
String content = Files.readString(Path.of("new-users.csv"));
System.out.println(content);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}

Result:

CSV written
name,age,city
Diana,28,Paris
Eve,32,Madrid

Note: this simple approach does not handle commas or quotes inside values. For production CSV handling, use a library like OpenCSV or Apache Commons CSV.

Text-based data persistence

A simple approach for small applications -- store data as one record per line:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;

record Task(int id, String title, boolean done) {

// Serialize to a line
String toLine() {
return id + "|" + title + "|" + done;
}

// Deserialize from a line
static Task fromLine(String line) {
String[] parts = line.split("\\|");
return new Task(
Integer.parseInt(parts[0]),
parts[1],
Boolean.parseBoolean(parts[2])
);
}
}

static List<Task> loadTasks(Path file) throws IOException {
if (!Files.exists(file)) {
return new ArrayList<>();
}

List<Task> tasks = new ArrayList<>();
for (String line : Files.readAllLines(file)) {
if (!line.isBlank()) {
tasks.add(Task.fromLine(line));
}
}
return tasks;
}

static void saveTasks(Path file, List<Task> tasks) throws IOException {
List<String> lines = new ArrayList<>();
for (Task task : tasks) {
lines.add(task.toLine());
}
Files.write(file, lines);
}
Path dataFile = Path.of("tasks.dat");

// Create some tasks
List<Task> tasks = new ArrayList<>();
tasks.add(new Task(1, "Learn Java", false));
tasks.add(new Task(2, "Build a project", false));
tasks.add(new Task(3, "Deploy to VPS", false));

// Save
saveTasks(dataFile, tasks);
System.out.println("Saved " + tasks.size() + " tasks");

// Load
List<Task> loaded = loadTasks(dataFile);
for (Task task : loaded) {
System.out.println(task);
}

Result:

Saved 3 tasks
Task[id=1, title=Learn Java, done=false]
Task[id=2, title=Build a project, done=false]
Task[id=3, title=Deploy to VPS, done=false]

This pattern -- serialize to a simple text format, one record per line -- is the foundation of the CLI project in the next chapter.

Summary

  • Path represents file/directory locations; Files provides all operations.
  • Files.readString / readAllLines for small files; newBufferedReader / lines() for large files.
  • Files.writeString / write for writing; use StandardOpenOption.APPEND to append.
  • Try-with-resources for readers and writers -- always.
  • Files.exists, isRegularFile, isDirectory for checking properties.
  • Files.createDirectories for creating paths; Files.walk for recursive traversal.
  • Simple CSV/text formats work well for small data persistence needs.

Next up: Project: CLI Task Manager -- building a complete command-line application.