Room 13 of 15 · about 25 minutes

Reading a file to its end, and why filling an array takes two passes

Room 7 opened the file and rooms 9 through 12 said what each kind of read takes out of it. This room reads all of it, and it is where you find out why getting those values into an array takes two walks through the file rather than one.

Tasks checked0 of 3 XP earned on this path0

What this room checks you can do

Student can write a while (sc.hasNextLine()) loop to read every line of a file, recognize the guard-and-read pairing, and adapt the pattern for counting lines (Pass 1 of count-allocate-fill) and for skipping a header before the loop.

Notes

The while (hasNextLine()) end-of-file loop

The canonical file-reading shape: keep reading lines until the file has no more lines. Java's Scanner exposes this through a paired guard-and-read:

final Scanner sc = new Scanner(new File(filename));
while (sc.hasNextLine()) {
    final String line = sc.nextLine();
    // ... process line ...
}
sc.close();

The guard hasNextLine() looks ahead in the buffer without consuming any input. The body's nextLine() actually advances the cursor. When the file ends, hasNextLine() returns false, the loop exits, and the file is closed.

This loop has three properties that make it the right default for "I do not know how long the file is":

1. It is safe. A zero-line file enters the loop body zero times. A one-line file enters once. The guard never lies about whether content remains. 2. It is symmetric with the keyboard. The same shape works for new Scanner(System.in) when the input ends at EOF (Ctrl-D on Unix, Ctrl-Z on Windows). 3. It pairs cleanly with counting. When the goal is count the lines, the body is just count++.

Two common variations

Counting the lines (Pass 1 of count-allocate-fill):

int count = 0;
while (sc.hasNextLine()) {
    sc.nextLine();       // discard the content; we only need the count
    count++;
}

Skipping a header line before the loop:

sc.nextLine();           // throw away the header
while (sc.hasNextLine()) {
    final String dataLine = sc.nextLine();
    // ...
}

The header skip happens before the loop because the EOF loop treats every remaining line as a data line. Doing the skip inside the loop on the first iteration is possible but reads worse and fails when the loop runs zero times.

Why not a for loop?

for loops are right when you know the bound in advance: exactly N iterations. EOF loops are unbounded: the file determines how many iterations there are. Trying to write this as for (int i = 0; i < fileLength; i++) requires you to know fileLength ahead of time, which usually means a separate pass to count, defeating the purpose of EOF detection.

The exception: in Pass 2 of count-allocate-fill, where Pass 1 has already produced the count, the fill loop is a for loop. The guard-and-read shape is for "how big is this file?"; the counted for is for "now read exactly that many."

In other languages

  • Python: for line in f:: the loop terminates at EOF automatically.
  • C: while (fgets(buf, sizeof buf, fp) != NULL) { ... }: sentinel-return at EOF.
  • Bash: while read -r line; do ... done < file.txt: same shape.

Why two passes: Scanner cannot rewind

A Scanner reads forward through its input stream. Once a method has called sc.hasNextLine() and sc.nextLine() until the stream is exhausted, there is no language-level way to return the Scanner to the beginning of the file. The Scanner has no rewind, reset, or seek method: those operations would require buffering the entire file in memory, which Java avoids by design.

final Scanner sc = new Scanner(new File("data.txt"));
while (sc.hasNextLine()) {                   //  walk to EOF, counting
    sc.nextLine();
    count++;
}
//  At this point, sc is exhausted. Calling sc.nextLine() here throws NoSuchElementException.
//  There is no way to make sc start over.

This forces a design choice when the size of the data is not known up front. To read N values from a file into an array sized exactly N (no slack, no overflow):

  • Option A: Two passes, two Scanners. Pass 1 counts the lines without storing them. The caller allocates new int[count] between passes. Pass 2 opens a fresh Scanner, reads each value, and fills the array. The pattern this concept area documents.
  • Option B: One pass with ArrayList<E> and .add(). Allocate a resizable list, append each value, then convert to an array if needed. More idiomatic in production code, but autoboxes for primitive types (wraps each int in an Integer object on the heap, ~16 bytes overhead per value) and obscures the explicit allocation that week 6 just taught.

CSCD 210 chooses Option A. The choice is pedagogical: the two-pass form makes the array-allocation step visible and lets students compose primitives they already know (EOF loop, counted loop, array literal). Option B is revisited in CSCD 211 once ArrayList<E> and generics are fully covered.

What "rewind" looks like in C

C's fseek(fp, 0L, SEEK_SET) does exactly what students wish Java's Scanner could do: return the stream to the start. Java's FileInputStream has getChannel().position(0) for the underlying byte stream, but Scanner's buffered abstraction does not expose it. The deliberate choice keeps the Scanner API simple at the cost of forcing the two-pass discipline for unknown-size data.

The "just guess a big size" anti-pattern

A tempting alternative: allocate an oversized array, fill what you can, return it with the unused slack. The problems:

  • Wastes memory (proportional to the slack).
  • The caller now has to know how many slots are actually used, requiring a separate count.
  • The caller's loops over the array iterate past the real data into garbage.

Two passes plus exact allocation is the clean answer.

In other languages

  • Python: the with open(f) as fp: block reads forward; fp.seek(0) rewinds. The two-pass approach works in Python but is less idiomatic: Python programmers use list.append in one pass.
  • C: fseek(fp, 0L, SEEK_SET) rewinds; the two-pass approach is also valid (and sometimes faster than dynamic allocation for primitive arrays).
  • Go: bufio.Scanner is forward-only like Java's; the two-pass approach applies. Go's append to a slice is the one-pass alternative.

What this room assumes you already have

Tasks

Do each one, then check the box. Checking a box is you saying you did it. You can uncheck a box if you check it by accident.

  1. trace
    Show the answer

    3.

  2. write
    Show the answer

    open Scanner on File, int count = 0, while (sc.hasNextLine()) { sc.nextLine(); count++; }, close, return.

  3. write
    Show the answer

    three statements with the caller allocating between them.

Self check

Type what you think the answer is. Getting it wrong costs nothing and you can try as many times as you want.

Given a zero-byte file, predict the number of loop iterations.

Practice, untimed

Open this whenever you want, before the tasks or after them. Nothing in this section is recorded and nothing here is timed.

  1. Modify the method to skip a header line, so the count is "data lines after the header."write
    Show the answer

    a single sc.nextLine() before the loop.

  2. Given a single Scanner that has already read every line of a file, predict the result of sc.hasNextLine().trace
    Show the answer

    false. The scanner cannot be made to start over without constructing a new one.

  3. Given a file whose first line is a header that should be skipped, predict the number of iterations the loop body executes if the body counts iterations.trace
    Show the answer

    lines - 1 (the skip happens before the loop).

  4. Given the suggestion "just call sc.reset() between passes," predict the outcome.trace
    Show the answer

    sc.reset() exists but does NOT rewind the stream; it resets configuration (delimiters, locale, radix) only. The Scanner is still at EOF after the call.

Optional challenge

This one is optional. Do what the room says you can do, without opening any answers, then read the two traps below and check your work against them. Each trap is copied from the notes for this room.

Student can write a while (sc.hasNextLine()) loop to read every line of a file, recognize the guard-and-read pairing, and adapt the pattern for counting lines (Pass 1 of count-allocate-fill) and for skipping a header before the loop.

How this room finishes

This room is done when all three tasks are checked and the self check is answered.