file-io Lesson 1 20 min read

File Input with Scanner

Reading files, handling exceptions, and try-with-resources

Reading: Reges & Stepp: Ch. 6

After this lesson, you will be able to:

  • Create a Scanner that reads from a file instead of the keyboard
  • Handle FileNotFoundException with both throws and try-catch
  • Use hasNextLine() and hasNext() loops to read until end of file
  • Apply try-with-resources to guarantee files are closed
  • Explain the difference between checked and unchecked exceptions

Your Data Disappears

Every program you have written so far has one fundamental limitation: the moment it ends, all its data is gone. Variables live in memory, and memory is wiped clean when the program terminates. If a user spends ten minutes entering scores, those scores vanish the instant they close the terminal.

Files solve this. A file on disk survives after your program ends. Your program can read data that was created by something else entirely — a spreadsheet, a log file, a dataset — and it can write results that persist for the next run or the next person. File I/O is not optional. You will use it in CSCD 211, CSCD 300, and every course after that. The Advanced Programming Exam (APE) tests it heavily.

The good news: you already know most of the mechanics. You have been using Scanner all quarter. Reading from a file uses the exact same methods — you just point the Scanner at a file instead of the keyboard.

From CSCD 110: In Python, reading a file is open("data.txt") followed by a for line in file: loop. Java is more explicit: you create a File object to represent the path, wrap it in a Scanner to read tokens and lines, and must handle the possibility that the file does not exist. The concepts — open, read, close — are identical. Only the syntax changes. Where Python gives you open() and implicit closing with with, Java gives you new Scanner(new File(...)) and try-with-resources.


The File Class: Representing a Path

Before you can read a file, you need a way to refer to it. The File class (from java.io) represents a file path — the location of a file on disk. A File object does not open the file or read its contents. It stores the path as a string and provides methods to ask questions about it.

import java.io.File;

File myFile = new File("grades.txt");

Think of this like writing an address on an envelope. The address might be valid, or the house might not exist yet. Java does not check when you create the object.

Useful methods on File:

Method What It Returns
file.exists() true if the file exists on disk
file.getName() Just the filename (e.g., "grades.txt")
file.getAbsolutePath() Full path from the root of the file system
file.length() File size in bytes

When a file is not found, print the absolute path. This tells you exactly where Java is looking:

File inputFile = new File("grades.txt");

if (!inputFile.exists()) {
    System.out.println("Error: " + inputFile.getAbsolutePath() + " not found.");
    return;
}

Common Pitfall: The “current working directory” depends on how you run your program, not where the .java file lives. In VS Code with Gradle, it is typically the project root. On the command line, it is wherever your terminal is. This is the single most common cause of “file not found” errors. Always use getAbsolutePath() to debug path issues.


Console Scanner vs. File Scanner

You already know how to read from the keyboard:

Scanner keyboard = new Scanner(System.in);  // reads from keyboard
String name = keyboard.nextLine();

Reading from a file is nearly identical — you change what the Scanner is attached to:

Scanner fileScanner = new Scanner(new File("grades.txt"));  // reads from file
String firstLine = fileScanner.nextLine();

The same methods work either way: nextLine(), nextInt(), nextDouble(), next(), hasNextLine(), hasNextInt(). The only difference is the constructor argument: System.in for keyboard, a File object for file input.

There is one critical behavioral difference. A keyboard Scanner blocks — it waits for the user to type something. A file Scanner does not block — the data is already there. When you call hasNextLine() on a file Scanner, it checks whether the file has more content. On a keyboard Scanner, it waits until the user presses Enter.


FileNotFoundException and the throws Declaration

There is a catch. When you create a Scanner from a File, the file might not exist. Java forces you to acknowledge this. The simplest way is to add throws FileNotFoundException to your method signature:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

void main() throws FileNotFoundException {
    Scanner scanner = new Scanner(new File("grades.txt"));

    while (scanner.hasNextLine()) {
        System.out.println(scanner.nextLine());
    }

    scanner.close();
}

throws FileNotFoundException means: “This method might fail because a file does not exist, and I am letting the caller deal with it.” For main(), the caller is the Java runtime, which prints a stack trace and terminates the program:

Exception in thread "main" java.io.FileNotFoundException:
    grades.txt (No such file or directory)
        at java.base/java.io.FileInputStream.open0(Native Method)
        at java.base/java.util.Scanner.<init>(Scanner.java:639)
        at Main.main(Main.java:6)

This is fine for small programs, but it produces an ugly crash. We will fix that with try-catch shortly.

Check Your Understanding

What happens if you try to create new Scanner(new File("missing.txt")) without a throws declaration or try-catch?


Reading Until End of File

The most common pattern is reading a file one line at a time using a while loop with hasNextLine():

Scanner scanner = new Scanner(new File("grades.txt"));

while (scanner.hasNextLine()) {
    String line = scanner.nextLine();
    System.out.println(line);
}

scanner.close();

Three parts to this pattern:

  1. hasNextLine() checks if there is another line (returns false at end of file)
  2. nextLine() reads and returns the next line
  3. close() releases the file when you are done

Reading Token by Token

If your file contains individual values separated by whitespace, you can read them directly with typed methods. Given a file scores.txt:

85 92 78
95 88

You can read every integer without worrying about line boundaries:

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

while (scanner.hasNextInt()) {
    int score = scanner.nextInt();
    System.out.println("Read: " + score);
}

scanner.close();

hasNextInt() and nextInt() work across line boundaries. Scanner treats spaces, tabs, and newlines all as whitespace separators. The loop reads all five integers without you needing to think about lines.

Method Pair Reads
hasNextLine() / nextLine() One full line of text
hasNext() / next() One whitespace-delimited token (as String)
hasNextInt() / nextInt() One integer token
hasNextDouble() / nextDouble() One double token

Key Insight: Always call the hasNext method before calling the next method. If you call nextLine() when there are no more lines, you get a NoSuchElementException at runtime. The hasNext / next pair is a guard-and-consume pattern — check first, then read.


Complete Example: Grade File Statistics

Here is a complete program that reads student grades from a file, computes statistics, and prints a report. This is the running example for the lesson.

Given grades.txt:

85 92 78 95 88 76 91 84 97 73
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class GradeStats {
    public static void main(String[] args) throws FileNotFoundException {
        File inputFile = new File("grades.txt");

        if (!inputFile.exists()) {
            System.out.println("Error: " + inputFile.getAbsolutePath()
                + " not found.");
            return;
        }

        Scanner scanner = new Scanner(inputFile);

        int count = 0;
        int sum = 0;
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;

        while (scanner.hasNextInt()) {
            int score = scanner.nextInt();
            sum += score;
            count++;
            if (score > max) { max = score; }
            if (score < min) { min = score; }
        }

        scanner.close();

        if (count == 0) {
            System.out.println("No scores found in file.");
            return;
        }

        double average = (double) sum / count;

        System.out.println("--- Grade Report ---");
        System.out.println("Scores read: " + count);
        System.out.printf("Average:     %.1f%n", average);
        System.out.println("Highest:     " + max);
        System.out.println("Lowest:      " + min);
    }
}

Output:

--- Grade Report ---
Scores read: 10
Average:     85.9
Highest:     97
Lowest:      73

Notice the structure: open the file, validate it exists, read in a loop, close, then process. The hasNextInt() loop consumes every integer in the file regardless of how they are arranged across lines.


Try-Catch: Handling Exceptions Yourself

The throws declaration pushes the problem to the caller. If you want to handle the error right here — show a friendly message instead of a stack trace — use try-catch:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class GradeStatsSafe {
    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(new File("grades.txt"));

            int count = 0;
            int sum = 0;

            while (scanner.hasNextInt()) {
                sum += scanner.nextInt();
                count++;
            }

            scanner.close();

            if (count > 0) {
                System.out.printf("Average: %.1f%n", (double) sum / count);
            }
        } catch (FileNotFoundException e) {
            System.out.println("Error: Could not open grades.txt");
            System.out.println("Details: " + e.getMessage());
        }

        System.out.println("Program finished.");
    }
}

If grades.txt does not exist, execution jumps from the Scanner constructor directly to the catch block. The rest of the try block is skipped. After the catch block, the program continues normally — it prints “Program finished.” either way.

The variable e in the catch block is the exception object. e.getMessage() returns a description of what went wrong. e.printStackTrace() prints the full stack trace (useful for debugging).

When to Use Which

Situation Use
Helper method that reads a file throws — let the caller decide
Main method in a quick script throws — acceptable for simple programs
Main method in a user-facing program try-catch — show a friendly message
Interactive program try-catch inside a do-while — reprompt the user

Key Insight: Helper methods should use throws because they do not know what the caller wants to do about the error. The top-level caller (usually main) should use try-catch because it can show a message to the user or retry.


Checked vs. Unchecked Exceptions

Java splits exceptions into two categories:

Checked exceptions are things that can go wrong through no fault of your code: the file does not exist, the network is down, the disk is full. Java forces you to handle these at compile time. FileNotFoundException is checked — that is why your code will not compile without throws or try-catch.

Unchecked exceptions are programming bugs: null pointers, array out of bounds, wrong input type. Java does not force you to handle these. The fix is to write correct code, not to catch them.

Exception Type Cause
FileNotFoundException Checked File does not exist
ArrayIndexOutOfBoundsException Unchecked Bug: bad index
NullPointerException Unchecked Bug: calling method on null
InputMismatchException Unchecked Scanner expected int, got text
NoSuchElementException Unchecked Called next() with nothing left

Common Pitfall: Do not catch Exception (the parent of all exceptions) unless you have a specific reason. Catching too broadly hides bugs. Catch the specific exception you expect — FileNotFoundException, not Exception. That way, actual bugs still crash loudly so you can find and fix them.

Check Your Understanding

Which of the following is a checked exception?


Try-With-Resources: Automatic Closing

Every Scanner or PrintStream you open must be closed when you are done. Failing to close can cause data loss (buffered writes never flushed), resource leaks (the OS limits open files), and locked files on some operating systems.

But what if an exception occurs between opening and closing? The close() call might never execute. Java solves this with try-with-resources: resources declared in the parentheses of a try statement are automatically closed when the block ends, even if an exception occurs.

Manual close (the old way)

Scanner scanner = null;
try {
    scanner = new Scanner(new File("grades.txt"));
    // process the file...
} catch (FileNotFoundException e) {
    System.out.println("File not found!");
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

Try-with-resources (the better way)

try (Scanner scanner = new Scanner(new File("grades.txt"))) {
    while (scanner.hasNextLine()) {
        System.out.println(scanner.nextLine());
    }
    // scanner is automatically closed when this block ends
} catch (FileNotFoundException e) {
    System.out.println("File not found!");
}

The resource is declared inside parentheses after try: try (Scanner scanner = ...). When execution leaves the try block — whether normally or via exception — Java calls scanner.close() automatically. You never forget to close, and the code is shorter.

You can declare multiple resources separated by semicolons:

try (Scanner in = new Scanner(new File("input.txt"));
     PrintStream out = new PrintStream(new File("output.txt"))) {

    while (in.hasNextLine()) {
        out.println(in.nextLine().toUpperCase());
    }
    // both 'in' and 'out' are automatically closed
} catch (FileNotFoundException e) {
    System.out.println("Error: " + e.getMessage());
}

Key Insight: Prefer try-with-resources for all file operations. It is safer (guaranteed close even on exceptions) and shorter (no finally block needed). Any class that implements AutoCloseable — including Scanner, PrintStream, PrintWriter, and all stream classes — works with this syntax.

Check Your Understanding

What is the main advantage of try-with-resources over manually calling close()?


Complete Example: Robust Grade Reader

Putting it all together — try-with-resources, try-catch, and the hasNext loop:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class RobustGradeReader {
    public static void main(String[] args) {
        File inputFile = new File("grades.txt");

        try (Scanner scanner = new Scanner(inputFile)) {
            int count = 0;
            int sum = 0;
            int max = Integer.MIN_VALUE;
            int min = Integer.MAX_VALUE;

            while (scanner.hasNextInt()) {
                int score = scanner.nextInt();
                sum += score;
                count++;
                if (score > max) { max = score; }
                if (score < min) { min = score; }
            }

            if (count == 0) {
                System.out.println("No scores found in file.");
                return;
            }

            double average = (double) sum / count;

            System.out.println("--- Grade Report ---");
            System.out.println("Scores read: " + count);
            System.out.printf("Average:     %.1f%n", average);
            System.out.println("Highest:     " + max);
            System.out.println("Lowest:      " + min);

        } catch (FileNotFoundException e) {
            System.out.println("Error: Could not open "
                + inputFile.getAbsolutePath());
            System.out.println("Make sure grades.txt is in the project root.");
        }
    }
}

This version handles the missing file gracefully (no stack trace), closes the Scanner automatically (try-with-resources), and guards against an empty file (count check). Compare this to the first example — same logic, but production-quality error handling.


Quick Reference

Task Code
Create a File reference File f = new File("path");
Check if file exists if (f.exists()) { ... }
Debug path issues f.getAbsolutePath()
Open file for reading Scanner sc = new Scanner(f);
Read line by line while (sc.hasNextLine()) { sc.nextLine(); }
Read ints while (sc.hasNextInt()) { sc.nextInt(); }
Read doubles while (sc.hasNextDouble()) { sc.nextDouble(); }
Declare exception void myMethod() throws FileNotFoundException
Try-catch try { ... } catch (FileNotFoundException e) { ... }
Try-with-resources try (Scanner sc = new Scanner(f)) { ... }
Close a Scanner sc.close(); (or use try-with-resources)

Summary

A File object represents a path on disk — it does not open or read anything. Wrapping a File in a Scanner lets you read its contents with the same methods you use for keyboard input: nextLine(), nextInt(), next(), and their hasNext counterparts.

FileNotFoundException is a checked exception. Java will not compile your code unless you handle it with throws (pass it to the caller) or try-catch (handle it yourself). Use throws in helper methods; use try-catch in main or any method that can show a message to the user.

Try-with-resources guarantees that your Scanner is closed even if an exception interrupts execution. Prefer it over manual close() calls.

The hasNext / next pair is a guard-and-consume pattern: always check before reading. hasNextLine() and hasNextInt() return false at end of file, giving you a clean loop termination without exceptions.

Next lesson: File processing patterns — counting lines before allocating arrays, processing mixed data types, and managing multiple Scanners.