CSCD210

Scanner on file

Skillcscd210-scanner-on-fileTextbookBJP Ch 6

Constructing a Scanner from a File

Student can construct a Scanner over a file using the two-step idiom new Scanner(new File(filename)), declare or catch the FileNotFoundException the constructor throws, and close the Scanner to release the file handle when finished.

A Scanner is a tokenizer plus parser: it consumes a stream of characters and hands back chunks (next()), lines (nextLine()), or typed values (nextInt(), nextDouble()). To read from disk instead of the keyboard, swap the source: pass a File to the Scanner constructor.

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

public static int countLines(final String filename) throws FileNotFoundException {
    final File f = new File(filename);          // step 1: build the path object
    final Scanner sc = new Scanner(f);          // step 2: open the file for reading
    int n = 0;
    while (sc.hasNextLine()) {
        sc.nextLine();
        n++;
    }
    sc.close();                                 // step 3: release the file handle
    return n;
}

Three things make this idiom different from the keyboard Scanner students wrote in earlier weeks:

1. The two-step construction. new File(filename) does not touch the disk; it only stores the path as an object. The disk read happens when the Scanner constructor actually opens the file. The two-step shape (new Scanner(new File(...))) is the convention because the File object also lets you call exists(), isFile(), canRead(), and other validation methods before opening.

2. You must close() what you open. Unlike the keyboard Scanner (which most programs intentionally leave open for the lifetime of the program), a file Scanner holds an OS file handle. Forgetting to close means the OS cannot reclaim it, and on some platforms it blocks other programs from accessing the file. Always close as soon as you have finished reading. (Week 7's "try-with-resources" introduces a syntax that closes for you, but the explicit close() is the foundation.)

3. The constructor is declared to throw FileNotFoundException. Every method that opens a file must either catch it or declare it on the header. The rule that FileNotFoundException is checked covers what that means.

What if I need two passes over the file?

Open a fresh Scanner each time. A Scanner does not have a rewind operation: once you have read past a line, you cannot go back without constructing a new Scanner on a new (or the same) File. The count-allocate-fill pattern, where pass 1 counts the values to learn how big an array to allocate and pass 2 fills the array, does exactly this: two Scanner objects, each closed after its pass.

In other languages

  • Python: open(filename, "r") returns a file object; iterating with for line in f: is the analog of while (sc.hasNextLine()).
  • C: FILE *fp = fopen(filename, "r") followed by fgets or fscanf. fclose is the close() equivalent.
  • JavaScript (Node): fs.readFileSync returns the whole content as a string; you then split or scan it yourself. There is no built-in lazy tokenizer like Scanner.

FileNotFoundException is checked

Student can import java.io.FileNotFoundException, declare it with throws on every method that constructs a Scanner on a File, distinguish it from FileSystemNotFoundException in java.nio.file, and explain why a pre-check (e.g., exists() or canRead()) does not remove the declaration requirement.

When new Scanner(new File(filename)) runs and the file is not on disk, the constructor throws java.io.FileNotFoundException. This is a checked exception, meaning the compiler enforces a binary rule (JLS §11.2): the enclosing method must either catch the exception or declare it on the method header with throws. Forgetting both is not a runtime concern: it is a compile error.

import java.io.File;
import java.io.FileNotFoundException;   // exactly this import; see pitfalls
import java.util.Scanner;

public static int countValues(final String filename) throws FileNotFoundException {
    final Scanner sc = new Scanner(new File(filename));
    // ...
}

Two facts about checked-exception declarations that the compiler enforces and students consistently underestimate:

1. A pre-check does not remove the throws requirement. Even if you wrote if (!new File(filename).exists()) { ... } two lines above the new Scanner(...), the compiler still demands throws FileNotFoundException on the method header. The compiler does not perform flow analysis across statements to discover that the exception is unreachable. It sees a call to a constructor that can throw FileNotFoundException and requires you to acknowledge it.

2. FileNotFoundException lives in java.io, not java.nio.file. The java.nio.file package has its own family of exceptions for the newer NIO.2 API (FileSystemNotFoundException, NoSuchFileException). They are different types with different superclasses. Importing the wrong one will not satisfy the throws requirement on a header that was written against java.io.FileNotFoundException, and every method body that mentions the type will fail to resolve.

In other languages

  • Python: open("missing.txt") raises FileNotFoundError: unchecked, no header declaration required.
  • C: fopen returns NULL instead of raising; the caller must check, no compile-time enforcement.
  • C#: FileNotFoundException exists but is unchecked (the language has no checked-exception concept).

Token reading vs. line reading

Student can choose between Scanner token methods (next, nextInt, nextDouble) and line methods (nextLine) based on the file format, and predict what each call consumes from the buffer.

Scanner has two reading modes that look similar at the call site but behave very differently. A method that picks the wrong mode reads the wrong characters, leaves the wrong characters behind, and produces output that looks "off by one" with nothing at the call site to show why.

Token mode (next(), nextInt(), nextDouble()): reads a chunk of non-whitespace characters, stops at the next whitespace (space, tab, newline). The whitespace itself stays in the buffer for the next call to consume.

Line mode (nextLine()): reads everything up to and including the next newline, returns the line without the trailing newline. The newline is removed from the buffer.

File contents:
    42 17
    -3

| Calls in order | Returned values | What is left in the buffer | |---|---|---| | nextInt(), nextInt(), nextInt() | 42, 17, -3 | (empty) | | nextLine(), nextLine() | " 42 17", " -3" | (empty) | | nextInt(), nextLine() | 42, " 17" | newline + " -3\n" |

The third row is the gotcha that produces the "nextLine returns empty string" bug: after nextInt() reads 42, the buffer still holds the space before 17, the 17 itself, the newline, and the second line. nextLine() then reads up to the first newline, which comes after 17. The return is " 17", not "-3". The trap that follows a token-mode read with nextLine carries the full diagnosis.

When to use which

| Use token mode when | Use line mode when | |---|---| | Values are separated by whitespace and you want them one at a time | The unit of meaning is a whole line (CSV row, log entry, sentence) | | You want type conversion (nextInt, nextDouble) for free | You will parse the line yourself (e.g., line.split(",")) | | You want to skip over blank lines and extra spaces automatically | You need to preserve indentation, embedded spaces, or empty lines |

The CSCD 210 typed-file convention is line mode for the type tag (because it is exactly one line) and token mode for the values (because nextInt/nextDouble parse-for-you).

In other languages

  • Python: for line in f: is line mode; f.read().split() is token mode.
  • C: fgets is line mode; fscanf("%d", ...) is token mode.
  • Bash: read -r line is line mode; read word (with default IFS) is token mode.