A notebook in a drawer#
Using a file is like using a notebook kept in a drawer. You open the drawer and take the notebook out. You read it from the first page onwards, or you write in it. Then you close it and put it back. Skip that last step and someone who comes to read it later may find your last notes were never really written down.
Java works through streams. A stream is a flow of data in one direction, like water through a hose: an input stream flows from the file into your program, an output stream flows from your program into the file. The data comes in order, from the start; you do not jump around.
The classes live in two packages, so file programs usually start with imports from java.io (File, FileWriter, PrintWriter, FileReader, BufferedReader, IOException) and java.util (Scanner).
Writing to a file#
The friendliest way to write text is a PrintWriter. It has the same print, println and printf you already use with System.out, but the text goes into a file. Wrap it around a FileWriter, which does the actual opening:
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
public class Write {
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter("scores.txt"));
out.println("Ana,91");
out.println("Ben,78");
out.printf("%s,%d%n", "Cy", 85);
out.close();
PrintWriter more = new PrintWriter(new FileWriter("scores.txt", true));
more.println("Dee,99");
more.close();
Scanner check = new Scanner(new File("scores.txt"));
while (check.hasNextLine()) {
System.out.println(check.nextLine());
}
check.close();
}
}
Ana,91
Ben,78
Cy,85
Dee,99
Opening scores.txt created it, because it did not exist yet. If it had existed, it would have been emptied first: a plain FileWriter overwrites. To add to the end instead, pass true as a second argument, new FileWriter("scores.txt", true), which means “append”.
Notice throws IOException on main. Opening a file can fail (a full disk, a folder you may not write to), and IOException is a checked exception, so the compiler insists you either catch it or declare it. Declaring it keeps these first listings short; handling it properly comes below.
Closing a file, and why it matters#
Writers keep text in a buffer, a small holding area in memory, and send it to the disk in batches because each trip to the disk is slow. close() sends whatever is still waiting, then releases the file. Forget it, and the last batch, which for a small file is everything, may never arrive:
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
public class NoClose {
static int linesIn(String name) throws FileNotFoundException {
Scanner in = new Scanner(new File(name));
int n = 0;
while (in.hasNextLine()) {
in.nextLine();
n++;
}
in.close();
return n;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter("notes.txt"));
out.println("first line");
out.println("second line");
out.println("third line");
System.out.println("Lines in the file before close: " + linesIn("notes.txt"));
out.close();
System.out.println("Lines in the file after close: " + linesIn("notes.txt"));
}
}
Lines in the file before close: 0
Lines in the file after close: 3
The program wrote three lines, yet before close() the file holds none of them: they were still sitting in the buffer. After close() all three are there. Closing also releases the file so other programs can use it. The modern way to make sure a file is always closed, even if an exception happens halfway through, is try-with-resources: open the file in brackets right after try, and Java closes it automatically when the block ends.
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
public class Resources {
public static void main(String[] args) throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter("log.txt"))) {
for (int i = 1; i <= 3; i++) {
out.println("entry " + i);
}
}
try (Scanner in = new Scanner(new File("log.txt"))) {
while (in.hasNextLine()) {
System.out.println("read back: " + in.nextLine());
}
}
}
}
read back: entry 1
read back: entry 2
read back: entry 3
No close() call, and the file was closed anyway, which is why the second block could read all three entries back. The reading Scanner is closed the same way. This is the one form of try that may stand alone without catch or finally.
Reading with Scanner#
You already know Scanner from reading the keyboard. Point it at a File object instead of System.in and it reads the file with the same methods: nextLine, nextInt, next, and the hasNext... methods that ask whether anything is left.
The file scores.txt holds:
Ana,91
Ben,78
Cy,85
Dee,99
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ReadScanner {
public static void main(String[] args) throws FileNotFoundException {
Scanner in = new Scanner(new File("scores.txt"));
int count = 0, total = 0;
while (in.hasNextLine()) {
String line = in.nextLine();
String[] parts = line.split(",");
int score = Integer.parseInt(parts[1]);
System.out.println(parts[0] + " scored " + score);
count++;
total += score;
}
in.close();
System.out.println("Average of " + count + ": " + (double) total / count);
}
}
Ana scored 91
Ben scored 78
Cy scored 85
Dee scored 99
Average of 4: 88.25
The loop pattern is the one to memorise: while hasNextLine(), call nextLine(). Inside, split(",") breaks the line at each comma into an array of pieces, and Integer.parseInt turns the text "91" into the number 91.
When a file is just numbers or words separated by spaces, hasNextInt() and nextInt() read one token (one piece between spaces or line breaks) at a time, with no splitting needed. The same nextInt-then-nextLine leftover-newline trap from keyboard input applies to files too.
Now the trap that catches nearly everyone once:
The file data.txt holds:
42
import java.util.Scanner;
public class ScanTrap {
public static void main(String[] args) {
Scanner in = new Scanner("data.txt");
System.out.println(in.nextLine());
}
}
data.txt
new Scanner("data.txt") is not a file. It is a Scanner reading the String "data.txt", eight characters long. It happily returns those characters, and never looks at the real data.txt or the 42 inside it. To read the file, you must wrap the name in a File object: new Scanner(new File("data.txt")).
Reading with BufferedReader#
BufferedReader is the other common way to read text. Wrap it around a FileReader. It has one main method, readLine(), which returns the next line, or null when the file has run out:
The file poem.txt holds:
Roses are red
Violets are blue
Files must be closed
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadBuffered {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new FileReader("poem.txt"));
String line;
int number = 0, blank = 0;
while ((line = in.readLine()) != null) {
number++;
if (line.isEmpty()) {
blank++;
} else {
System.out.println(number + ": " + line);
}
}
in.close();
System.out.println(number + " lines, " + blank + " blank");
}
}
1: Roses are red
3: Violets are blue
4: Files must be closed
4 lines, 1 blank
The loop reads a line into line and checks it for null in one go: while ((line = in.readLine()) != null). The extra brackets make the assignment happen first. This is the standard BufferedReader loop, worth recognising on sight.
Scanner or BufferedReader: which and why#
The “buffered” in BufferedReader is the point. It pulls a large block of the file into memory at a time, 8,192 characters by default, and hands you lines from that block, so it makes few slow trips to the disk. Scanner also buffers, but it spends extra work breaking text into tokens and checking their types.
Scanner | BufferedReader | |
|---|---|---|
| Reads | lines and tokens: nextInt, nextDouble, next | whole lines only: readLine |
| Converts numbers for you | yes | no: use Integer.parseInt and friends |
| Speed on large files | slower | faster |
| Errors while reading | hides them; check ioException() | throws IOException so you notice |
| End of file | hasNextLine() is false | readLine() returns null |
Use Scanner for small files, and whenever the content is a mix of words and numbers you want converted as you read. Do not use it for very large files, where its speed hurts, or when you need to know that a read failed. Use BufferedReader for large files and line-by-line processing, accepting that you will convert the numbers yourself.
Handling file exceptions#
File operations throw checked exceptions, so the compiler will not let you ignore them. The two to know are FileNotFoundException, when the file to read does not exist (or cannot be opened), and its parent IOException, for anything else that goes wrong while reading or writing.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Missing {
public static int countLines(String name) {
int count = 0;
try (Scanner in = new Scanner(new File(name))) {
while (in.hasNextLine()) {
in.nextLine();
count++;
}
} catch (FileNotFoundException e) {
System.out.println("Could not open " + name + ", counting 0 lines");
}
return count;
}
public static void main(String[] args) {
System.out.println(countLines("missing.txt"));
System.out.println("The program is still running");
System.out.println(new File("missing.txt").exists());
}
}
Could not open missing.txt, counting 0 lines
0
The program is still running
false
The program did not crash: the catch turned a missing file into a clear message, and execution carried on. When a block can throw both file exceptions, the exceptions guide’s rule applies: catch FileNotFoundException, the child, before IOException, its parent. (The text of e.getMessage() differs between Windows and other systems, which is why this listing prints its own message.)
A file the program insists is missing, while you can see it in the folder, is nearly always a path problem. A bare name like "data.txt" is a relative path: Java looks for it in the program’s working directory, the folder the program was started from. In BlueJ and VS Code that is normally the project folder, not the src folder. new File("data.txt").getAbsolutePath() prints exactly where Java is looking, and exists() tells you whether anything is there.
Open, use, close: write with a PrintWriter around a FileWriter (add true to append), read with Scanner(new File(name)) or a BufferedReader, and close every file, ideally with try-with-resources. File exceptions are checked, so catch them or declare them.
Where these go wrong#
- Never closing a writer. The buffered text may never reach the disk, leaving an empty file.
new Scanner("data.txt"). That reads the String itself. Writenew Scanner(new File("data.txt")).- Opening an existing file with
new FileWriter(name)when you meant to add to it. It is emptied first. Usenew FileWriter(name, true). - Forgetting the checked exceptions. Without a
catchor athrowsclause the code does not compile. - Catching
IOExceptionbeforeFileNotFoundException. The child must come first or the second catch is unreachable. - Testing
readLine()for an empty String to find the end. An empty line is""; the end of the file isnull. - Putting the data file in the
srcfolder. A relative path is looked up from the working directory, usually the project folder.
Practice#
Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.
- What is the difference between
new FileWriter("log.txt")andnew FileWriter("log.txt", true)whenlog.txtalready has text in it? - A program writes three lines with a
PrintWriter, never callsclose(), and the file comes out empty. Explain why, and give two fixes. - A file
numbers.txtholds whole numbers separated by spaces and line breaks. Write a program that prints their sum, handling a missing file. - Give one situation where
Scanneris the better choice and one whereBufferedReaderis, with the reason. - Write a program that copies
in.txttoout.txtwith every line in capitals, usingBufferedReaderandPrintWriter. - Your program throws
FileNotFoundExceptionfordata.txt, but you can see the file in your project. What is the likely cause, and what one line would show you?
Solutions#
- The first empties the file and starts writing from the beginning. The second opens it in append mode, so new text is added after what is already there.
- A
PrintWriterholds text in a buffer and sends it to the disk in batches; the batch was never sent because the writer was never closed (or flushed). Fixes: callout.close()when finished, or open the writer with try-with-resources so it is closed automatically. - Read token by token with
hasNextIntandnextInt; for a file holding 4 8 15 16 23 42:The file
numbers.txtholds:4 8 15 16 23 42import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; public class QSum { public static void main(String[] args) { int sum = 0; try (Scanner in = new Scanner(new File("numbers.txt"))) { while (in.hasNextInt()) { sum += in.nextInt(); } } catch (FileNotFoundException e) { System.out.println("numbers.txt is missing"); } System.out.println("Sum: " + sum); } }Sum: 108 Scanner: a small file mixing words and numbers, because it converts tokens as it reads.BufferedReader: a very large log file processed line by line, because it reads big blocks at a time and is faster, and it reports read errors asIOExceptioninstead of hiding them.- Both files open in one try-with-resources, separated by a semicolon, so both are closed automatically:
The file
in.txtholds:one two threeimport java.io.BufferedReader; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; public class QCopy { public static void main(String[] args) { try (BufferedReader in = new BufferedReader(new FileReader("in.txt")); PrintWriter out = new PrintWriter(new FileWriter("out.txt"))) { String line; while ((line = in.readLine()) != null) { out.println(line.toUpperCase()); } } catch (IOException e) { System.out.println("Copy failed: " + e.getMessage()); } try (BufferedReader check = new BufferedReader(new FileReader("out.txt"))) { String line; while ((line = check.readLine()) != null) { System.out.println(line); } } catch (IOException e) { System.out.println("Could not read the copy"); } } }ONE TWO THREE - The relative path is being looked up from the working directory, and the file is somewhere else, such as inside
src.System.out.println(new File("data.txt").getAbsolutePath());prints the exact location Java is checking.
Test-readiness checklist#
These are the items from the course’s Exam 2 topic list that this guide covers. Tick each one only when you can do it without looking.
- ☐ I can open a file for writing and for reading, and explain what a stream is.
- ☐ I can write to a file with
PrintWriterandFileWriter, including appending. - ☐ I can read from a file with
Scannerand withBufferedReader, and explain why to useBufferedReader. - ☐ I can say when to use the
Scannerclass and when not to. - ☐ I can close a file, and explain why closing matters, including try-with-resources.
- ☐ I can handle file exceptions:
FileNotFoundExceptionandIOException.
Test yourself in the free Kestrel Exams app
40 practice questions on reading and writing files with Scanner, BufferedReader and FileWriter, with every code answer checked by compiling and running it. Free, offline, no account.
Frequently asked questions#
How do you write to a text file in Java?
Create a PrintWriter around a FileWriter, such as new PrintWriter(new FileWriter("out.txt")), then use print, println or printf, and close it when finished. Pass true as a second argument to FileWriter to append instead of overwriting. Opening a file can throw IOException, which must be caught or declared.
How do you read a text file in Java?
Either create a Scanner around a File object, new Scanner(new File("data.txt")), and loop while hasNextLine() calling nextLine(); or create a BufferedReader around a FileReader and loop while readLine() does not return null.
Why should I use BufferedReader instead of Scanner?
BufferedReader reads large blocks of a file into memory at a time and returns whole lines, which makes it faster for big files, and it reports read errors as IOException. Scanner is more convenient for small files because it splits text into tokens and converts numbers, but it is slower and hides read errors.
Why is my output file empty?
Most likely the writer was never closed. PrintWriter and FileWriter buffer text in memory and only send it to the disk when the buffer fills, when flush() is called, or when the file is closed. Call close(), or open the writer with try-with-resources.
What is try-with-resources?
A form of try that opens one or more resources, such as files, in parentheses after the try keyword. Java closes them automatically when the block ends, even if an exception is thrown. It may be used without catch or finally blocks.
Why does Java say my file is not found when it exists?
A file name without a folder is a relative path, looked up from the program's working directory, which is usually the project folder rather than the src folder. Print new File("name.txt").getAbsolutePath() to see exactly where Java is looking.
Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.