Room 15 of 15
· about 35 minutes
Counting a file, then filling an array that fits it exactly
Room 13 told you why this takes two passes. This room is the two passes
themselves, the counting method and the filling method, and the rule both of
them follow.
Tasks checked0 of 5
XP earned on this path0
Room complete
Notes
The while (hasNext()) token-mode EOF loop
The token-mode analog of the hasNextLine loop: read tokens one at a time until the file's token supply is exhausted. The shape is identical; only the methods change.
final Scanner sc = new Scanner(new File(filename));
while (sc.hasNext()) {
final String token = sc.next();
// ... process the token ...
}
sc.close();
The guard hasNext() looks ahead in the buffer, skipping whitespace, and reports whether there is any non-whitespace token remaining. The body's next() consumes one. The loop terminates when hasNext() returns false: meaning the rest of the file is empty or all-whitespace.
The typed-EOF variants
When the tokens are all of one type, the typed guards work the same way:
// Sum all integers in a file
int sum = 0;
while (sc.hasNextInt()) {
sum += sc.nextInt();
}
hasNextInt() returns true only when the next token both (a) exists and (b) parses as an int. The loop stops on the first non-integer (or at EOF). This is the safest token loop because the guard ensures nextInt will not throw InputMismatchException.
The typed-guard pattern is also the idiom for "read integers until a sentinel":
// File ends with "STOP" instead of EOF
while (sc.hasNextInt()) {
int v = sc.nextInt();
// ... process v ...
}
// The cursor now sits before "STOP" (the first non-integer token)
// sc.next() would return "STOP" if a caller needed to consume it.
The pattern handles both EOF and sentinel termination without an if inside the loop: hasNextInt() is false in both cases.
Comparing the four common shapes
| Loop | When to use | |------|-------------| | while (sc.hasNextLine()) { sc.nextLine() } | Lines, one record per line | | while (sc.hasNext()) { sc.next() } | Free-form whitespace-delimited tokens, type unknown | | while (sc.hasNextInt()) { sc.nextInt() } | Integer-only stream; terminates at first non-int | | for (int i = 0; i < n; i++) { sc.next() } | Exactly n reads: count already known |
The first three are EOF loops (variable number of iterations); the fourth is a counted loop (fixed number). All four read from a Scanner; the differences are about what stops the reading.
Performance
The guard methods do not skip past tokens: they only peek. hasNext() is O(token-skip distance) in the worst case (when there is a lot of whitespace between tokens), and effectively O(1) for typical files. Calling the guard once per iteration adds no asymptotic cost; the loop body's next() is what advances the cursor.
In other languages
- Python: the for-line iterator handles the line case; token-mode is
for word in line.split() per line. - C:
while (fscanf("%d", &n) == 1) { ... }: fscanf returns the count of successfully parsed items, including 0 (parse failed) and EOF (-1). The loop tests for "parsed something." - C++:
while (cin >> n) { ... }: stream conversion to bool returns false at EOF or on parse error. Same shape; different mechanism.
Pass 1: the counting loop
The first pass walks the file from start to end, increments a counter for each data line (or token), and returns the count. It does not store the values themselves: the values come back in Pass 2. The signature:
public static int countLines(final String filename) throws FileNotFoundException {
final Scanner sc = new Scanner(new File(filename));
int count = 0;
while (sc.hasNextLine()) {
sc.nextLine(); // consume the line; throw it away
count++;
}
sc.close();
return count;
}
Four design commitments visible in this shape:
1. The method takes a String filename, not a Scanner. The short version of the discipline is that each pass opens its own Scanner. 2. sc.nextLine() is called, not just sc.hasNextLine(). The hasNextLine guard only peeks at the buffer; without a corresponding nextLine call inside the loop, the cursor never advances and the loop runs forever. The two methods always pair: hasNextLine to test, nextLine to consume. 3. The result of sc.nextLine() is discarded. Pass 1 cares about the count, not the values. Discarding by sc.nextLine(); (no assignment) is idiomatic; the JDK does the same in its own counting utilities. 4. The Scanner is closed before the return. The OS file descriptor is released. The rule that each pass opens its own Scanner covers the close discipline, and the try-with-resources form is the modern idiom that avoids the manual close.
Variant: counting tokens, not lines
When the data is whitespace-delimited rather than line-delimited:
public static int countInts(final String filename) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) {
int count = 0;
while (sc.hasNextInt()) {
sc.nextInt(); // consume the token, discard the value
count++;
}
return count;
}
}
The shape is the same; only the guard and the read change. hasNextInt paired with nextInt for typed tokens; hasNext paired with next for generic tokens.
Variant: skipping a header line before counting
Lab 11's typed-file format puts a type tag on the first line and the data on subsequent lines. The count should exclude the tag:
public static int countDataLines(final String filename) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) {
sc.nextLine(); // discard the type tag
int count = 0;
while (sc.hasNextLine()) {
sc.nextLine();
count++;
}
return count;
}
}
The discard happens before the loop, not inside it. Putting it inside requires a "first iteration is special" branch and is harder to read.
In other languages
- Python:
with open(filename) as f: count = sum(1 for _ in f): one-liner using generator counting. - C: loop with
fgets returning non-NULL; increment a counter; same shape as Java's. - Unix shell:
wc -l filename does exactly this from the command line; the wc source code is the canonical reference for "how to count lines in a file."
Pass 2: allocate the array, fill with a counted for
Pass 2 receives the count from Pass 1, opens a fresh Scanner on the same file, allocates the array of exactly that size, fills it with a counted for loop, closes the Scanner, and returns the array.
public static int[] readInts(final String filename, final int count) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) {
final int[] arr = new int[count]; // allocate exactly count slots
for (int i = 0; i < count; i++) { // counted for, not EOF loop
arr[i] = sc.nextInt();
}
return arr;
}
}
Four design commitments visible in this shape:
1. The method takes both filename AND count. Pass 1 returned the count; the caller supplies both pieces to Pass 2. Alternative designs (Pass 2 calls Pass 1 internally) couple the two passes and make testing harder. 2. The allocation uses the exact count, not a "safe overestimate." new int[count] produces an array sized to fit the data exactly: no slack, no overflow. The reason two passes are needed covered why this matters. 3. The fill loop is a counted for, not an EOF loop. Now that the size is known, the loop bound is the array's length. Using while (sc.hasNextInt()) here would also work if the count was correct, but the counted for makes the contract explicit ("exactly count reads"). If the file has fewer values than count claims (a Pass-1 bug), the counted-for fails loudly with NoSuchElementException at the right slot. 4. The cross-pass type-tag handling. If Pass 1 skipped a header tag, Pass 2 must do the same. The skip happens before the counted loop starts: sc.nextLine(); once on entry, then the counted for. Both passes must agree on the header structure.
Variant: reading whitespace-delimited tokens
public static double[] readDoubles(final String filename, final int count) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) {
final double[] arr = new double[count];
for (int i = 0; i < count; i++) {
arr[i] = sc.nextDouble();
}
return arr;
}
}
Same shape with nextDouble replacing nextInt. The element type of the array (double[]) matches the typed read; mixing them produces compile errors.
Variant: reading String lines
public static String[] readLines(final String filename, final int count) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) {
final String[] arr = new String[count];
for (int i = 0; i < count; i++) {
arr[i] = sc.nextLine();
}
return arr;
}
}
The Scanner's mode (token vs line) is set by which next is called. Each Pass 2 variant matches its Pass 1 counterpart: nextLine counts → nextLine reads; nextInt counts → nextInt reads.
The contract between Pass 1 and Pass 2
The two passes must agree on three things:
1. The file format. Both passes open the same file; both must read it the same way (line-mode or token-mode). 2. The header convention. If Pass 1 skips the header, Pass 2 does too. If Pass 1 counts the header as a data line, Pass 2 reads it. 3. The count's meaning. Pass 1's count must equal Pass 2's iteration count. A Pass-1 bug (off by one, wrong skip rule) produces a Pass-2 crash or silent data corruption.
The pair is fragile because the agreement is informal: Java's type system does not enforce that Pass 1 and Pass 2 share a contract. The CSCD 210 discipline: write the two as paired helpers (countLines + readLines), keep them in the same class, and review them together when either changes.
In other languages
- Python:
with open(filename) as f: lines = [next(f) for _ in range(count)]: list comprehension with explicit count. More commonly, lines = list(f) (one-pass, dynamic-size). - C: identical pattern:
fopen → fscanf count times → fclose. The C version is the conceptual source. - Java streams:
Files.lines(Path.of(filename)).limit(count).toArray(String[]::new): one-pass, Stream-based. CSCD 211 territory.
Each pass opens its own Scanner
The discipline that ties the count-allocate-fill pattern together: Pass 1 and Pass 2 each construct, use, and close their own Scanner. Neither pass receives a Scanner as a parameter; neither pass leaves a Scanner open for the other to consume.
public static int countLines(final String filename) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) { // Pass 1's own Scanner
// ... count loop ...
return count;
} // Scanner closed here
}
public static int[] readInts(final String filename, final int count) throws FileNotFoundException {
try (Scanner sc = new Scanner(new File(filename))) { // Pass 2's own Scanner: fresh
// ... fill loop ...
return arr;
} // Scanner closed here
}
Three properties of the per-pass discipline:
1. Each method takes a String filename, not a Scanner. The function signature advertises "I open my own file." Callers cannot accidentally share a partially-consumed Scanner between the passes. 2. Each Scanner is constructed inside its method. The construction is what reopens the underlying OS file at byte position 0. There is no language-level "Scanner rewind"; constructing a new one is the only way. 3. Each Scanner is closed before the method returns. Try-with-resources handles this automatically. Without try-with-resources, the explicit sc.close() after the loop body (paired with a try/finally cleanup) does the same.
What students want the pattern to be (but is not)
Three antipatterns students reach for, each rejected:
// Antipattern 1: share the Scanner via parameter
public static int countLines(Scanner sc) { ... } // WRONG
public static int[] readInts(Scanner sc, int count) { ... } // WRONG
// After Pass 1 walks to EOF, Pass 2 gets an exhausted Scanner.
// Antipattern 2: store the Scanner as a static field
private static Scanner sc; // WRONG
// Same problem: Pass 1 exhausts; Pass 2 sees EOF.
// Antipattern 3: call sc.reset() between passes
sc.reset(); // WRONG
// Scanner.reset() resets configuration (delimiters, locale).
// It does NOT rewind the stream. The Scanner is still at EOF.
The correct pattern is the verbose one: two methods, each opening its own Scanner.
What "opening a fresh Scanner on the same file" actually does
At the OS level, each new Scanner(new File(filename)) call:
1. Opens the file via FileInputStream (allocates a new file descriptor at the OS level). 2. Wraps it in an InputStreamReader (character decoding). 3. The Scanner reads from that Readable and maintains its own internal CharBuffer (Scanner does its own buffering; it does not wrap a separate BufferedReader). 4. Returns a Scanner whose cursor is at byte position 0.
The two passes therefore use two independent OS file descriptors. There is no shared state between them. If the file is modified between passes (rare in CSCD 210, common in production), the second pass sees the modified version.
Why this works for CSCD 210 inputs but is suspect at scale
The two-passes-with-fresh-Scanner pattern is correct for files that do not change between passes, which is true for every CSCD 210 input. In production code, the assumption fails: a log file actively being written to may have more lines on the second pass than the first did. The mitigations (file locking, snapshot-and-process, atomic append patterns) are out of scope for CSCD 210 but worth knowing exist.
In other languages
- C:
fopen is called twice on the same path; each call returns an independent FILE*. Same discipline. - Python:
with open(filename) as f1: and with open(filename) as f2: open independently. The pattern is identical. - Bash:
wc -l file.txt > tmp; <count from tmp>; <process file.txt>: the count is captured between two reads of the file.
The "fresh resource per pass" discipline is universal in stream-based file processing.