File Input with Scanner
Reading files, handling exceptions, and try-with-resources
After this lesson, you will be able to:
- Create a
Scannerthat reads from a file instead of the keyboard - Handle
FileNotFoundExceptionwith boththrowsandtry-catch - Use
hasNextLine()andhasNext()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 afor line in file:loop. Java is more explicit: you create aFileobject to represent the path, wrap it in aScannerto 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 youopen()and implicit closing withwith, Java gives younew 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
.javafile 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 usegetAbsolutePath()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.
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:
hasNextLine()checks if there is another line (returnsfalseat end of file)nextLine()reads and returns the next lineclose()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
hasNextmethod before calling thenextmethod. If you callnextLine()when there are no more lines, you get aNoSuchElementExceptionat runtime. ThehasNext/nextpair 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
throwsbecause they do not know what the caller wants to do about the error. The top-level caller (usuallymain) should usetry-catchbecause 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, notException. That way, actual bugs still crash loudly so you can find and fix them.
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
finallyblock needed). Any class that implementsAutoCloseable— includingScanner,PrintStream,PrintWriter, and all stream classes — works with this syntax.
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.