Java Reading Files

In the last lesson you learned about the Java File class. So far, your program’s data disappeared when it stopped. To read data someone saved, you work with files. Java gives you several ways to read files. Let’s learn the ones you will actually use.

πŸ€” Why read files?

Think about closing any program:

  • A running program keeps everything in memory (the fast, temporary workspace used while a program runs).
  • That memory is wiped clean the moment the program closes.
  • So a game that keeps your high score only in memory loses it the next time you open the game.
  • A file fixes this: data saved on the disk, where it stays even after the program stops or the computer is turned off.
  • Reading a file means opening that saved data and pulling it into your program.

Here are the everyday reasons programs read files:

  • Load settings a person saved earlier, like the chosen language or theme.
  • Read a list of names or numbers that the program needs to work through.
  • Open a report, a log, or a CSV (a plain-text table) that another tool produced.
  • Read input the user prepared in advance, instead of typing it line by line.

Picture a recipe card box on your kitchen counter:

  • The cards stay there whether or not you are cooking.
  • When you want to cook, you take a card out and read it.
  • The disk is the box, a file is one card, and reading the file is taking the card out.
  • Your program is the cook.

We will cover four tools: Scanner, BufferedReader, and the modern Files helpers (readAllLines, readString, and lines). They all read text, but each fits a different situation.

⚠️ Files can fail, so Java forces you to plan

One idea makes everything else click. Reading a file is not guaranteed to work:

  • The file might not be there.
  • The name might be spelled wrong.
  • The disk might be busy.

The outside world is unpredictable, so Java treats file errors as something you must deal with. It uses checked exceptions (errors the compiler forces you to handle before it builds your program):

  • IOException is the main one. It stands for input-output exception.
  • FileNotFoundException is a kind of IOException that fires when the file is not found.

You have two ways to satisfy the compiler:

  • Handle it: wrap the risky code in try and catch so you can react if it fails.
  • Declare it: add throws IOException to pass the responsibility up to whoever called your method.

Most code handles it, to show a friendly message instead of crashing. Every file read below is wrapped in try/catch for this reason.

πŸ“₯ Reading with Scanner

The friendliest starting point is Scanner, the same class you may have used for keyboard input. It reads from a file just as easily. You only swap the source. Here it opens a file and prints it one line at a time.

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File("data.txt"))) {
while (scanner.hasNextLine()) { // βœ… keep going while a line is left
String line = scanner.nextLine();
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.out.println("Could not find the file: " + e.getMessage());
}
}
}

Here is what each piece does:

  • new File("data.txt") points at the file. It does not read anything yet.
  • new Scanner(new File(...)) hands that file to a Scanner so it reads from the file instead of the keyboard.
  • scanner.hasNextLine() asks β€œis there another line waiting?” and returns true or false, a safe way to control the loop.
  • scanner.nextLine() pulls out the next full line as a String.
  • The try (...) part is try-with-resources, which closes the scanner when the loop ends. More on that soon.

When to use Scanner:

  • Most readable choice for small files.
  • Best when you want tokens (separate pieces) rather than whole lines.
  • scanner.nextInt() or scanner.nextDouble() pull numbers straight out of a file as it goes.
  • Trade-off: it does extra parsing work, so for very large files it is slower than the next tool.

Output (for a file with two lines)

First line of the file
Second line of the file

πŸ“– Reading with BufferedReader

The classic workhorse is BufferedReader, which reads text one line at a time:

  • It is built for large files because it does not load everything at once.
  • It uses a buffer (a small holding area in memory) to grab chunks of the file in one go.
  • So it does not touch the disk for every single character. That buffering is what makes it fast.

Here is the standard pattern: read line by line until there are no lines left.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) { // βœ… stop when readLine returns null
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
}
}

Here is what each part does:

  • new FileReader("data.txt") opens the file as a stream of characters.
  • We wrap that FileReader inside a BufferedReader to add the fast buffering.
  • reader.readLine() returns the next line, or null when the file ends.
  • while ((line = reader.readLine()) != null) reads a line, stores it in line, and checks for null in one step. On null, the loop stops.

That null at the end is the key idea:

  • It is Java’s way of saying β€œnothing more to read.”
  • A blank line is different. It returns an empty string "", which is not null, so the loop keeps going.
  • Only the true end of the file gives you null.

The while ((line = reader.readLine()) != null) loop is one of the most common patterns in Java. Worth memorizing, you will see it everywhere.

Output (for a file with two lines)

First line of the file
Second line of the file

Always close file resources

A file you open must be closed, or it stays locked and wastes resources. The cleanest way is try-with-resources (try(...)), which closes the reader automatically even if an error happens. Avoid opening a file without it.

πŸ”’ Why try-with-resources matters

You have seen try (...) in every example. It is the safety net that keeps file code clean.

  • When you open a file, the operating system gives your program a handle (a connection to that file).
  • A handle is a limited resource. Forget to close it and the file stays locked and the handle leaks.
  • Open enough files without closing them and the program runs out of handles and fails.

The old way was to call reader.close() by hand in a finally block. It worked, but it was easy to forget. Try-with-resources fixes this:

  • Anything declared inside the parentheses of try (...) is closed automatically when the block ends, even on error.
  • Without it, you must call close(), and a missed call leaks the file.
  • With it, Java calls close() for you, even if an exception is thrown midway.
  • The only requirement is that the resource implements AutoCloseable. Scanner, BufferedReader, and FileReader all do.

πŸ†• The modern way: Files.readAllLines

Starting with Java 7, the Files helper (from java.nio.file) turned many file jobs into one-liners. The first is Files.readAllLines, which reads a whole file into a List<String>, one entry per line. Here is the same print job, far shorter.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;
public class Main {
public static void main(String[] args) {
try {
List<String> lines = Files.readAllLines(Paths.get("data.txt"));
for (String line : lines) { // βœ… loop over the list like any other list
System.out.println(line);
}
System.out.println("Total lines: " + lines.size());
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
}
}

What is happening here:

  • Paths.get("data.txt") turns the file name into a Path object, the modern way to point at a file.
  • Files.readAllLines(...) opens the file, reads every line into a list, and closes the file, all in one call.
  • The result is a normal List<String>, so you can loop over it, count it with size(), sort it, or search it.
  • No reader to close, no while loop. The method does it all internally.

That makes it the cleanest choice for files small enough to fit in memory.

Output

First line of the file
Second line of the file
Total lines: 2

πŸ“„ Reading the whole file as one String

Sometimes you want the entire file as a single block of text, not a list of lines. Files.readString (added in Java 11) does that in one call. Here is how to pull a whole file into one String.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try {
String content = Files.readString(Paths.get("data.txt"));
System.out.println(content); // βœ… the whole file, line breaks included
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
}
}

Files.readString keeps line breaks exactly as they were. Reach for it when you want to:

  • Search the full text.
  • Count characters.
  • Treat the file as one block instead of line by line.

Like the others, it can throw IOException if the file is missing, so it stays inside try/catch.

🌊 Streaming big files with Files.lines

One more modern tool solves a real problem:

  • Both readAllLines and readString load the entire file into memory at once.
  • For a small config file that is fine. For a multi-gigabyte log file, it can run your program out of memory.
  • Files.lines reads the file as a stream (a flow of items processed one at a time) instead of loading it all up front.
  • So you get the short modern code and the memory safety of line-by-line reading.

Here is Files.lines printing a file without ever holding the whole thing in memory.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.forEach(System.out::println); // βœ… process one line at a time
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
}
}

A couple of points make this work:

  • Files.lines(...) returns a Stream<String>, feeding lines one at a time rather than building a full list.
  • lines.forEach(System.out::println) runs an action on each line as it arrives.
  • It is wrapped in try-with-resources because, unlike readAllLines, this one does hold a file open while it works.

Reach for Files.lines when you want clean modern code but the file is too big to load all at once.

βš–οΈ Whole file vs streaming: the memory trade-off

The core decision is how much to load at once. Both approaches read the same text, so it comes down to file size:

  • Whole file (Files.readAllLines, Files.readString, Scanner into a list): use for small files. The data fits in memory, the code is short, and you can revisit any line.
  • Line by line / streaming (BufferedReader, Files.lines): use for large files. You hold one line at a time, so memory stays low no matter how big the file grows.
  • When in doubt about size, lean toward streaming. It is safe for both small and large files.

⚠️ Common Mistakes

A few file-reading slip-ups trip up almost everyone at first.

  • Ignoring IOException. File reading is a checked exception, so the compiler will refuse to build until you handle or declare it.
// ❌ Will not compile: the IOException is not handled
List<String> lines = Files.readAllLines(Paths.get("data.txt"));
// βœ… Wrap it so failures are handled
try {
List<String> lines = Files.readAllLines(Paths.get("data.txt"));
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
  • Forgetting to close the reader. A reader opened without try-with-resources can leak the file handle.
// ❌ No try-with-resources: if an error happens, this file may never close
BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
// βœ… try-with-resources closes it automatically, even on error
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
// read here
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
  • Using the wrong path. A file name with no folder is looked for in the program’s current working directory, not where your source code lives. If you get a not-found error even though the file exists, the path is usually the reason. Print the absolute path to see where Java is actually looking.

  • Assuming the file exists. Never trust that a file is there. Always handle the missing-file case so the program shows a clear message instead of crashing.

// ❌ Crashes hard if data.txt is missing, with an ugly stack trace
String content = Files.readString(Paths.get("data.txt"));
// βœ… Catches the problem and explains it
try {
String content = Files.readString(Paths.get("data.txt"));
} catch (IOException e) {
System.out.println("File not available right now.");
}
  • Loading a huge file all at once. readAllLines and readString pull the whole file into memory. For very large files, switch to BufferedReader or Files.lines and process line by line.

βœ… Best Practices

A few habits keep file reading safe and clean.

  • Always use try-with-resources. It closes file resources for you, even when an error is thrown, so handles never leak.
  • Pick the tool that fits the size. Files.readAllLines or Files.readString for small files, BufferedReader or Files.lines for big ones.
  • Check existence when it helps. Files.exists(Paths.get("data.txt")) lets you give a clear message before you even try to read.
  • Always handle the missing-file case. Catch IOException and print a message a person can understand, not a raw error.
  • Buffer or stream large data. Do not load gigabytes into memory at once. Process one line at a time instead.

🧩 What You’ve Learned

Nicely done. Let’s recap reading files.

  • βœ… Files let data stay around between runs and let programs read saved or shared data.
  • βœ… Scanner reads files in a friendly way and can pull out tokens like numbers, best for small files.
  • βœ… BufferedReader reads text efficiently line by line with readLine() until it returns null, ideal for large files.
  • βœ… Files.readAllLines reads a whole file into a List<String> in one call, great for small files.
  • βœ… Files.readString reads the entire file into a single String, and Files.lines streams a big file one line at a time.
  • βœ… File reading throws the checked IOException (handle or declare it) and resources should always be closed with try-with-resources.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why is BufferedReader good for large files?

    Why: BufferedReader reads efficiently one line at a time, so it handles large files well.

  2. 2

    What does Files.readAllLines return?

    Why: Files.readAllLines reads the whole file into a List of lines.

  3. 3

    What exception must you handle when reading a file?

    Why: File reading can fail, throwing the checked IOException, which you must catch or declare.

  4. 4

    What is the cleanest way to make sure a file reader is closed?

    Why: try-with-resources closes the reader automatically, even if an error occurs.

πŸš€ What’s Next?

Reading files is half the story. To save data so it lasts, you need to write files too. Next we learn how to write text to a file, both the classic and modern ways.

Java Writing Files

Share & Connect