file-io 16 min read

Reading a File with Scanner

The File class is a path, Scanner is the reader, throws is the escape hatch

In a nutshell

Three things you need to read a file in Java:

  1. A File object — a path on disk. Constructing one does not open the file or read anything. It’s just an address.
  2. A Scanner wrapped around the File — the reader. The same Scanner methods you’ve been using on System.in (nextInt, nextLine, hasNextInt, hasNextLine) work identically on a file Scanner. The only thing that changed is the source.
  3. The throws FileNotFoundException declaration on main — the keyword the compiler insists on, because constructing a Scanner from a File can fail. This is what we use this quarter; try/catch is a CSCD 211 topic.

Once you have the Scanner, the EOF loop is the bread-and-butter pattern: keep reading until hasNextLine() (or hasNext(), or hasNextInt()) returns false.

Today in three sentences. A File is a path; a Scanner is what reads from it. throws FileNotFoundException on main is how we satisfy the checked-exception rule for now. The EOF loop is while (sc.hasNextLine()) { ... sc.nextLine() ... }.

After this lesson, you will be able to:

  • Construct a Scanner that reads from a file and explain the role of the File object.
  • Add throws FileNotFoundException to main and explain why the compiler insists on it.
  • Write the canonical EOF loop using hasNextLine / nextLine (and the related hasNext / next form).
  • Recognize the try/catch and try-with-resources alternatives without writing them yet.

From CSCD 110. Python hides this with with open("data.txt") as f: — one line, no exception declaration, file auto-closes. Java makes the steps visible: build the path, build the reader, declare what can go wrong, close it explicitly. More verbose, more explicit, fewer surprises later.


The File class is a path, not the contents

This is the first place students get confused. Read it carefully.

File f = new File("scores.txt");

That line does not open scores.txt. It does not read anything. It does not even check whether the file exists. All it does is build a small Java object whose job is to hold the path string "scores.txt".

Mental model: the File object is like an address written on a piece of paper. Having someone’s address does not mean you have visited their house. It means you know where to go if you decide to go.

File f = new File("scores.txt");   // address on paper
boolean exists = f.exists();        // ask: does the house at this address exist?
String fullPath = f.getAbsolutePath(); // ask: what's the full address?

Both f.exists() and f.getAbsolutePath() look up information about the path, but they still do not open the file or read its contents. To do that, you hand the File object to a Scanner, which is the actual reader.

The File constructor accepts either a relative path ("scores.txt") or an absolute one ("/Users/jessica/data/scores.txt"). A relative path is resolved against the program’s working directory — the folder the program was launched from. If you launch from one folder and the file is in another, the program will report it as missing. (This is the most common reason a file “isn’t there” when the file clearly is.)

Common pitfall: thinking new File(...) opens the file. It doesn’t. It’s just an address. The actual reading happens when you give that address to a Scanner. Several students every term try to call f.nextInt() on a File directly. That doesn’t compile because File doesn’t have a nextInt method. File is just the address; Scanner is the reader.

Check your understanding. What does this code do?

File f = new File("nonexistent.txt");
System.out.println(f.exists());
Reveal answer

Prints false. Constructing the File object never tries to open the file, so it succeeds even when no such file exists. exists() is the line that actually goes to the disk and asks. The program does not throw any exception here — it just prints false. (You’d get an exception only when you tried to read from a Scanner pointed at a missing file.)


Wrapping a Scanner around a File

The Scanner you’ve been using all quarter has more than one constructor. You’ve been using this one:

Scanner kb = new Scanner(System.in);

There’s also one that takes a File:

Scanner sc = new Scanner(new File("scores.txt"));

Same class. Same methods. Different source. Anything you’ve called on kb works on sc:

int n = sc.nextInt();
String line = sc.nextLine();
boolean more = sc.hasNextInt();

The numbers and tokens come from the file instead of the keyboard, but the API is identical. This is the entire reason file I/O isn’t a whole new topic — Scanner abstracted the source away. You learn one new constructor, and your Scanner skills transfer.

Closing the Scanner when you’re done:

sc.close();

Closing releases the file handle back to the operating system. For programs that read one file and exit, this matters less (the OS cleans up on process exit). For programs that read many files, or run for a long time, leaking handles is a real bug. Close where the work for that Scanner ends.

Common pitfall: the working-directory trap. A program that worked fine in IntelliJ (“scores.txt is right there!”) fails when launched from a different folder, because new File("scores.txt") is interpreted relative to wherever the program was started from, not relative to the source code’s folder. Either use an absolute path during testing, or print new File("scores.txt").getAbsolutePath() when debugging to see exactly which path the program is looking at.


throws FileNotFoundException: the route this course takes

The Scanner(File) constructor can fail. If the file doesn’t exist (or the program doesn’t have permission to read it), the constructor throws a FileNotFoundException. Java categorizes this as a checked exception, which means the compiler will not let you ignore it.

Concretely: if you write this and nothing else, the program does not compile.

public static void main(String[] args) {
    Scanner sc = new Scanner(new File("scores.txt"));   // compile error
    // ...
}

The compiler error reads:

error: unreported exception java.io.FileNotFoundException;
       must be caught or declared to be thrown

You have two ways to satisfy the compiler. For this course, we use the simpler one.

The course route: throws FileNotFoundException on main

Add the keyword to the method signature:

public static void main(String[] args) throws FileNotFoundException {
    Scanner sc = new Scanner(new File("scores.txt"));
    // ...
}

That’s it. The throws declaration tells the compiler: “if this exception happens, let it propagate up out of main. The program will print a stack trace and exit.” For our purposes, that’s exactly what we want — if the input file is missing, there is nothing useful the program can do anyway.

You’ll add import java.io.FileNotFoundException; at the top of the file, alongside import java.io.File; and import java.util.Scanner;. Three imports, one keyword. This pattern is the convention you’ll use on every file-reading lab and exam.

The other routes: try/catch and try-with-resources (CSCD 211 topics)

Two alternatives exist. You will see them mentioned and may even see them in code from other sources. You do not need to write them this quarter. Brief tour, for recognition only.

try/catch lets the calling method intercept the exception and print a friendlier message:

try {
    Scanner sc = new Scanner(new File("scores.txt"));
    // use sc
    sc.close();
} catch (FileNotFoundException e) {
    System.out.println("Could not open file: " + e.getMessage());
}

try-with-resources does the same plus auto-closes the Scanner at the end of the try block:

try (Scanner sc = new Scanner(new File("scores.txt"))) {
    // use sc, no need to call sc.close()
} catch (FileNotFoundException e) {
    System.out.println("Could not open file: " + e.getMessage());
}

These are real Java and they’re useful, but they introduce control-flow ideas (catch blocks, resource scoping) that belong in CSCD 211. For now, when you see them in someone else’s code, recognize the shape; when you write your own, use throws.

Common pitfall: forgetting throws FileNotFoundException on main. The compiler error is loud (unreported exception ...; must be caught or declared to be thrown), so this is easy to fix once you’ve seen it. The fix is exactly six words: add throws FileNotFoundException after the closing parenthesis of the parameter list.

Check your understanding. Which of these main signatures will let new Scanner(new File("data.txt")) compile inside the body?

A. public static void main(String[] args) B. public static void main(String[] args) throws FileNotFoundException C. public static void main(String[] args) throws Exception D. public static void main(String[] args) throws IOException

Reveal answer

B, C, and D all work. B is the most precise (declares exactly what can be thrown). C and D both work because FileNotFoundException is a subclass of IOException, which is a subclass of Exception — declaring a more general type covers all the more specific ones. A does not work; that’s the compile-error case. Use B for this course. Specific is better than general.


Reading until end of file

You usually don’t know in advance how many lines or tokens a file has. The canonical pattern is to keep asking the Scanner whether there is more input, and stop when there isn’t.

For line-by-line reading:

Scanner sc = new Scanner(new File("data.txt"));
while (sc.hasNextLine()) {
    String line = sc.nextLine();
    System.out.println(line);
}
sc.close();

For token-by-token reading (whitespace-separated):

Scanner sc = new Scanner(new File("numbers.txt"));
while (sc.hasNextInt()) {
    int n = sc.nextInt();
    System.out.println(n);
}
sc.close();

This is the same shape as a sentinel loop you wrote on keyboard input, with the end-of-file playing the role of the sentinel. When the Scanner runs out of input, hasNextLine() (or hasNextInt(), etc.) returns false, and the loop exits naturally.

There’s a related family of methods you should recognize:

  • hasNext() — is there another whitespace-delimited token?
  • hasNextLine() — is there another line?
  • hasNextInt() — is there another token and does it parse as an int?
  • hasNextDouble() — same for double.

The has* methods are predicates: they return true or false and they do not consume input. Asking them is always safe. Calling the matching next* method is what actually reads.

Common pitfall: calling nextLine() (or nextInt(), etc.) without checking first. If the file is empty, or you’ve already read past the end, the call throws NoSuchElementException. Always pair each next* call with a guard (while (hasNextLine()) or if (hasNextInt())) so you only read when there’s something to read.

Check your understanding. What does this loop print, given a file nums.txt containing exactly:

7
12
-3
Scanner sc = new Scanner(new File("nums.txt"));
int sum = 0;
while (sc.hasNextInt()) {
    sum += sc.nextInt();
}
sc.close();
System.out.println(sum);
Reveal answer

Prints 16. The loop reads 7, 12, -3 in order, accumulating sum = 0 + 7 + 12 + (-3) = 16. After the third read, hasNextInt() returns false (no more tokens), the loop exits, and sum is printed. This is the same accumulator pattern from Week 4, with the end-of-file playing the sentinel role.

Check your understanding. What’s the bug?

Scanner sc = new Scanner(new File("data.txt"));
while (true) {
    String line = sc.nextLine();
    System.out.println(line);
}
Reveal answer

The loop never checks hasNextLine(). Once the file is exhausted, the next call to sc.nextLine() throws NoSuchElementException. Fix: change the condition to while (sc.hasNextLine()). The while (true) form is fine when the loop exits another way (a break on a sentinel), but for a “read to the end of the file” pattern, while (sc.hasNextLine()) is the idiomatic shape.


Wrap up and what’s next

Recap.

  • A File object is a path, not the contents. Constructing it never opens or reads.
  • Wrap a Scanner around a File to read: new Scanner(new File(path)). Same methods you already know.
  • The Scanner(File) constructor throws FileNotFoundException. The course convention is to declare throws FileNotFoundException on main. try/catch and try-with-resources exist; you’ll write them in CSCD 211.
  • The EOF loop is the canonical pattern: while (sc.hasNextLine()), or while (sc.hasNextInt()), etc. Always pair next* reads with hasNext* guards.
  • Close the Scanner when you’re done.

What you can do now. Read a file line by line, or token by token, into local variables or an array. Add throws FileNotFoundException to main without thinking about it. Recognize try/catch in someone else’s code without writing your own.

Next up: The next* Cascade and the nextLine Trap. The four readers (next, nextLine, nextInt, nextDouble) and how to choose between them. The hasNextInt-before-hasNextDouble rule (and why the order matters). The famous leftover-newline trap that breaks programs which mix nextInt and nextLine.


  • Reges & Stepp, Building Java Programs, Chapter 6 sections 6.1 and 6.2 on Scanner, files, and the EOF loop.
  • The Scanner Javadoc lists every method. The hasNext* and next* families are the load-bearing ones for this course.