Reading and writing text
On This Lesson 11 topics
Lesson Overview
You can now point at a file. This lesson puts text into it and gets text back out, using four one-line methods of the Files class: readAllLines, readString, writeString, and write. For small text files such as notes, settings, and a contact list, these are all you need.
The filing-cabinet picture continues: the path is the label on the folder, and these methods are the clerk who takes a whole page out, or files a whole page in, in one move. Two details decide whether your data survives. First, by default writing replaces what was in the file. Second, every one of these methods can throw IOException, so you must decide what your program does when the disk says no.
Why this matters for a backend: configuration files, exported reports, and simple logs are all text files. Knowing exactly what happens to an existing file (replaced, or added to) is the difference between a log that grows and a log that silently loses its history.
Previously, "Paths and the Files API" showed how to build paths and create folders. The next lesson, "Streams, readers, and buffering", covers what to do when a file is too big to load in one go.
Learning Objectives
By the end of this lesson you will be able to:
- Read a whole text file as a
List<String>withFiles.readAllLinesand as oneStringwithFiles.readString. - Write text with
Files.writeStringand a list of lines withFiles.write. - Explain that writing replaces the file by default, and append with
StandardOpenOption.APPEND. - Explain which method adds line breaks and which does not.
- Handle
IOException, including a file that does not exist yet. - Say why these methods suit small files only.
Written Explanation
Reading
List<String> lines = Files.readAllLines(path);
String text = Files.readString(path);readAllLines returns every line as an element of a List<String>, without the line breaks. readString returns the entire content as one String, line breaks included. Pick readAllLines when you will process line by line (the common case) and readString when you want the text as a whole.
Both methods open the file, read it, and close it for you, even if something goes wrong. You do not need try-with-resources for them. Both decode the bytes as UTF-8; the encoding lesson explains why that matters.
If the file does not exist, they throw an IOException (usually NoSuchFileException). A missing file is not a bug in your program. It is a normal situation, for example on the very first run, so plan for it.
Writing
Files.writeString(path, "hello\n");
Files.write(path, List.of("a", "b", "c"));writeString writes the exact characters you give it. It adds nothing, so a line break exists only if you type \n yourself. write(path, lines) takes any collection of strings and ends each element with a line separator, so you get one line per element.
By default both methods behave as if three options were given: create the file if needed, write to it, and truncate it to zero length first. In plain words: if the file already exists, its old content is gone. Run writeString twice and only the second text remains.
Appending
To add to the end of a file instead, pass options:
Files.writeString(path, line + "\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);APPEND says "add at the end". CREATE says "make the file if it is missing". You usually want both: with APPEND alone, a missing file causes a NoSuchFileException. This is the pattern for logs.
Handling IOException
Every method above declares IOException, a checked exception. The compiler forces you to either catch it or declare throws IOException. Remember the exceptions module: catch where you can do something useful. For a missing data file, that often means "start with an empty list":
try {
List<String> lines = Files.readAllLines(file);
} catch (IOException e) {
System.out.println("File problem: " + e.getMessage());
}Print a clear message that names the file. A bare e.printStackTrace() or an empty catch block hides the real problem.
Small files only
readAllLines and readString load the whole file into memory. The documentation says they are intended for small files, and that reading a very large file this way can run out of memory. A few thousand contact lines are fine. A multi-gigabyte log file is not. For those, the next lesson shows how to read one line at a time.
Examples
Example 1: write, replace, append lines, read back (simple)
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
public class Ex1 {
public static void main(String[] args) throws IOException {
Path file = Path.of("data", "note.txt");
Files.createDirectories(file.getParent());
Files.writeString(file, "first\n");
Files.writeString(file, "second\n"); // replaces
System.out.print(Files.readString(file));
Files.write(file, List.of("a", "b", "c")); // each element becomes a line
List<String> lines = Files.readAllLines(file);
System.out.println(lines + " size " + lines.size());
Files.writeString(file, "x"); // no newline added
System.out.println(Files.readAllLines(file));
}
}Output:
second
[a, b, c] size 3
[x]The first text, first, is gone: the second write replaced it. The list of three elements became three lines, and readAllLines gave them back as three elements without any line-break characters. Even x with no newline at the end counts as one line.
Example 2: a simple log with APPEND (practical)
import java.io.IOException;
import java.nio.file.*;
public class Ex2 {
static void log(Path file, String msg) throws IOException {
Files.writeString(file, msg + "\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
public static void main(String[] args) throws IOException {
Path file = Path.of("data", "app.log");
Files.createDirectories(file.getParent());
Files.deleteIfExists(file);
log(file, "started");
log(file, "working");
log(file, "stopped");
System.out.println(Files.readAllLines(file));
try {
Files.writeString(Path.of("data", "missing.log"), "x", StandardOpenOption.APPEND);
} catch (IOException e) {
System.out.println(e.getClass().getSimpleName());
}
}
}Output:
[started, working, stopped]
NoSuchFileExceptionEach call adds a line and keeps the earlier ones. The last lines prove the point about options: APPEND without CREATE does not make a missing file, so the call throws. (The deletion at the start only makes the demo repeatable.)
Example 3: loading a file that may not exist (realistic)
import java.io.IOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
public class Ex3 {
static List<String> loadLines(Path file) {
if (!Files.exists(file)) {
return new ArrayList<>();
}
try {
return Files.readAllLines(file);
} catch (IOException e) {
System.out.println("Could not read " + file + ": " + e.getMessage());
return new ArrayList<>();
}
}
public static void main(String[] args) throws IOException {
Path file = Path.of("data", "todo.txt");
Files.deleteIfExists(file);
System.out.println(loadLines(file));
Files.createDirectories(file.getParent());
Files.write(file, List.of("buy milk", "", "call Ana"));
List<String> lines = loadLines(file);
for (int i = 0; i < lines.size(); i++) {
System.out.println((i + 1) + ": " + lines.get(i));
}
}
}Output:
[]
1: buy milk
2:
3: call AnaOn a first run the file is missing, so the method returns an empty list and the program carries on. If the file exists but cannot be read, it reports which file failed and still returns a usable empty list. Notice that the blank line is kept as an empty string, which is why line 2 is empty. This "missing means empty" pattern is exactly what the module's lab needs.
Backend Insight
Small text files are the glue of many backend systems: an application reads its settings at startup, a batch job writes a report for another team, a service appends to a log. The decision each time is the same one you just practised: replace, or append?
A frequent production bug is writing a file with the default "replace" behaviour when the intent was to add. The service restarts, and last week's entries are gone. The reverse bug exists too: appending to a file that should have been rewritten, so it grows forever. State your intent in the code with explicit options, and say it in a comment when it is not obvious.
Also think about size. A function that calls readAllLines on an upload is fine in a test with ten lines and dangerous in production with a user who sends a huge file. Later modules on file uploads return to this problem.
Common Mistakes
Mistake 1: Expecting writeString to add a new line
- Incorrect:
Files.writeString(file, "one"); Files.writeString(file, "two");and expecting two lines. - Correct: the second call replaces the first, and neither adds a line break. Use
APPENDand add"\n"yourself, or build a list and callFiles.write(file, lines)once.
Mistake 2: Overwriting a file by accident
Calling Files.write(path, lines) on a file that already holds data destroys the old data. Before you write, ask: should the old content survive? If yes, append, or read it first and write the merged result.
Mistake 3: Using APPEND without CREATE
APPEND alone fails on the very first run because the file does not exist yet. Pass StandardOpenOption.CREATE too.
Mistake 4: Swallowing the exception
try { Files.readAllLines(file); } catch (IOException e) { } // wrongAn empty catch hides the reason the data is missing. Report the file name and message, or rethrow.
Mistake 5: Loading a huge file into memory
readAllLines and readString are for small files. For large ones, read line by line (next lesson).
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Files.writeString(p, "A\n"); runs, then Files.writeString(p, "B\n");. What is in the file afterwards?
Show solution A1
A1. Only B. The second write truncates the file first.
A2. Which method returns a List<String>, and which returns one String: readAllLines or readString?
Show solution A2
A2. readAllLines returns the List<String>; readString returns one String.
A3. Files.write(p, List.of("x", "y")) and Files.writeString(p, "x" + "y"). How many lines does each produce?
Show solution A3
A3. The first produces two lines (x and y). The second writes xy: one line, because writeString adds nothing.
Level B: Application
B1. Write static void appendLine(Path f, String line) throws IOException that adds a line to the end of a file and creates the file if needed.
Show solution B1
B1.
static void appendLine(Path f, String line) throws IOException {
Files.writeString(f, line + System.lineSeparator(),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}B2. Write static long countNonBlank(Path f) throws IOException that returns the number of lines that are not blank. (String.isBlank() tells you whether a string is empty or only spaces.)
Show solution B2
B2.
static long countNonBlank(Path f) throws IOException {
long n = 0;
for (String l : Files.readAllLines(f)) {
if (!l.isBlank()) n++;
}
return n;
}For a file with the lines one, an empty line, and three, it returns 2 (run and verified).
B3. The file data/config.txt may not exist. Write code that prints Using defaults when it is missing and otherwise prints each line with its line number. Do not let the program crash.
Show solution B3
B3.
Path cfg = Path.of("data", "config.txt");
if (!Files.exists(cfg)) {
System.out.println("Using defaults");
} else {
try {
List<String> lines = Files.readAllLines(cfg);
for (int i = 0; i < lines.size(); i++) {
System.out.println((i + 1) + ": " + lines.get(i));
}
} catch (IOException e) {
System.out.println("Could not read " + cfg + ": " + e.getMessage());
}
}Level C: Challenge
C1. Write static void removeLine(Path f, int number) throws IOException that deletes the line with the given 1-based number from the file. If the number is out of range, print No line N and leave the file unchanged. Hint: the documentation does not promise that the returned list can be modified, so copy it into an ArrayList first.
Show solution C1
C1.
static void removeLine(Path f, int number) throws IOException {
List<String> lines = new ArrayList<>(Files.readAllLines(f));
if (number < 1 || number > lines.size()) {
System.out.println("No line " + number);
return;
}
lines.remove(number - 1);
Files.write(f, lines);
}Starting from one, a blank line, three: removeLine(f, 2) left [one, three], and removeLine(f, 9) printed No line 9 (run and verified). The code needs java.util.* imports.
C2. A teammate's log shows only today's entries, although the program has run for a week. The code is Files.writeString(log, entry + "\n");. Explain the cause and fix it.
Show solution C2
C2. The default behaviour of writeString is to truncate the file, so each run (or each entry) replaces the previous content. Fix: Files.writeString(log, entry + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND);.
Mini Challenge
Build a tiny to-do program with a file data/todo.txt. Write three methods: List<String> load() (an empty list if the file is missing), void add(String task) (adds one line, keeping old ones), and void show() (prints numbered tasks, or Nothing to do for an empty list). Run the program twice with different add calls and check that the tasks from the first run are still there.
Check your work: the first run works without the file existing, the second run sees the old tasks, no catch block is empty, and every path is relative.
Summary & Next Lesson
Key takeaways
readAllLinesgives aList<String>;readStringgives oneString. Both close the file for you.writeStringwrites exactly what you give it;write(path, lines)ends each line with a line separator.- By default, writing replaces an existing file. Use
CREATEandAPPENDto add to the end. - All of these methods throw
IOException. A missing file is a normal case to plan for. - They load or write everything at once, so use them only for small files.
Next lesson
The next lesson, "Streams, readers, and buffering", shows how to process big files one line at a time and introduces the difference between byte streams and character streams.
Self-check
- What does
Files.writeStringdo to an existing file by default? - Which options make a write add to the end of a file and create the file if missing?
- Does
readAllLineskeep the line break characters in each element? - Why is an empty
catch (IOException e) { }a bad idea? - Why are
readAllLinesandreadStringunsuitable for huge files?
Show answers
- It replaces the content: the file is truncated first, then the new text is written.
StandardOpenOption.CREATEandStandardOpenOption.APPEND.- No. Each element is the line without its line terminator.
- It hides the failure, so you cannot tell why data is missing. Report the file and message, or rethrow.
- They load the entire file into memory, which can run out of memory for very large files.
References
- Oracle, Files (Java SE 21 API). It supports the UTF-8 default of these methods, the default write options (create, truncate, write), that they close the file themselves, that
writeends each line with the platform line separator whilewriteStringadds none, and the small-file warning. - Behaviour shown in the example outputs (such as
NoSuchFileExceptionforAPPENDon a missing file) was confirmed by compiling and running the code with JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.