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:
- a base case that returns without recursing;
- 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);
}
- 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.
- Forgetting
close()and finding an empty output file. - Mixing
nextInt()andnextLine()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.