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.