THINK FIRST·CODE LATER

← Java Programming
Chapter 17 · Week 13–14

File I/O and Recursion

Before You Start: What You Must Be Able to Do

Before the questions, make sure you can: read a text file with Scanner and write one with PrintWriter, including the imports, the checked exceptions and the closing of the stream; explain the difference between overwriting and appending; process a file line by line and token by token until the end of the data; state the two parts every recursive method must have; trace a recursive call by hand and draw the call stack; and say why a missing or unreachable base case produces a StackOverflowError.

Reading a text file

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

Scanner in = new Scanner(new File("marks.txt"));   // throws FileNotFoundException (CHECKED)
while (in.hasNextLine()) {
    String line = in.nextLine();
    System.out.println(line);
}
in.close();                                        // release the file

The same Scanner class reads the keyboard (new Scanner(System.in)) and a file (new Scanner(new File(name))); only the argument changes. FileNotFoundException is checked, so the code must sit inside a try/catch or the method must declare throws FileNotFoundException — the compiler will not let you ignore it.

hasNextLine() / nextLine() line-by-line processing, spaces included
hasNext() / next() token-by-token (whitespace separated)
hasNextInt() / nextInt() numeric tokens, with validation
close() always, in finally or with try-with-resources

The end-of-file controlled loop from Chapter 2 is exactly the while (in.hasNextX()) pattern: no sentinel value is needed, because the file itself says when the data stop.

Writing a text file

import java.io.PrintWriter;
import java.io.FileWriter;
import java.io.IOException;

PrintWriter out = new PrintWriter("report.txt");           // CREATES or OVERWRITES
out.println("Name,Grade");
out.printf("%s,%.2f%n", "Ali", 65.5);
out.close();                                               // without close(), data may be lost

PrintWriter app = new PrintWriter(new FileWriter("log.txt", true));  // true = APPEND

PrintWriter offers the same print, println and printf methods as System.out, which is why writing a file is mostly a matter of changing where the output goes. Constructing one may throw IOException (or FileNotFoundException), again checked.

Try-with-resources closes everything automatically:

try (Scanner in = new Scanner(new File("in.txt"));
     PrintWriter out = new PrintWriter("out.txt")) {
    while (in.hasNextLine()) out.println(in.nextLine().toUpperCase());
} catch (IOException e) {
    System.out.println("File problem: " + e.getMessage());
}

The File class

new File("data.txt") does not create anything on disk; it is a name. Useful queries: exists(), canRead(), length(), getName(), getAbsolutePath(), delete(). A relative name is resolved against the working directory of the program, which is the usual reason a file “is not found” although you can see it in the project folder.

Recursion

A recursive method calls itself on a smaller version of the same problem. Two parts are compulsory:

  1. a base case that returns without recursing;
  2. a recursive case that moves strictly closer to the base case.
public static int factorial(int n) {
    if (n <= 1) return 1;                 // base case
    return n * factorial(n - 1);          // recursive case
}
public static int fib(int n) {
    if (n < 2) return n;                  // fib(0)=0, fib(1)=1
    return fib(n - 1) + fib(n - 2);       // two recursive calls
}
public static String reverse(String s) {
    if (s.isEmpty()) return s;
    return reverse(s.substring(1)) + s.charAt(0);
}

Each call gets its own stack frame with its own parameters and local variables. The frames are removed in reverse order as the calls return, so factorial(4) builds 4 × (3 × (2 × 1)) on the way back up. If the base case is missing or never reached, the frames accumulate until the JVM throws a StackOverflowError.

Recursion versus iteration: any recursion can be rewritten as a loop and vice versa. Loops are usually faster and use constant memory; recursion is clearer for problems that are naturally self-similar (tree structures, divide and conquer, backtracking). Naive fib is the standard warning: it recomputes the same values exponentially often.

public static int binarySearch(int[] a, int key, int low, int high) {   // divide and conquer
    if (low > high) return -1;                                   // base case: not found
    int mid = (low + high) / 2;
    if (a[mid] == key)     return mid;                           // base case: found
    if (a[mid] < key)      return binarySearch(a, key, mid + 1, high);
    else                   return binarySearch(a, key, low, mid - 1);
}
Remember
  • File operations throw checked exceptions: handle or declare.
  • new PrintWriter(name) overwrites; new FileWriter(name, true) appends.
  • Always close(), or use try-with-resources.
  • new File(name) only names a file; it does not create one.
  • Every recursion needs a base case and progress towards it.
  • No base case (or no progress) ⇒ StackOverflowError.
Common Pitfalls
  • Forgetting close() and finding an empty output file.
  • Mixing nextInt() and nextLine() on a file exactly as on the keyboard (Chapter 5's trap applies here too).
  • Recursing on the same value (return f(n);) instead of a smaller one.
  • Using recursion where a simple loop is clearer and cheaper.

Ready? Close the notes and practise.

38 questions. Predict the output before you check — that is the skill the exam measures.