file-io 24 min read

APE File I/O Walkthrough: Six Worked Problems

Read the spec sentence by sentence, tag each one with the four-step pattern, translate to code

In a nutshell

The Practice APE has a File I/O section with six methods to write. Each method is graded by a JUnit test that compares your output against an exact expected string. The good news: every one of the six follows the same recipe, the four-step pattern from lesson 7e (validate, operate, handle errors, update state). The art of doing well on this section is not Java cleverness; it is reading the spec carefully and translating each sentence to its tag.

This lesson walks all six exercises. It is the capstone of the file-I/O unit. By the end you will be able to:

  • Take an unfamiliar APE-style File I/O method spec and tag each sentence with one of the four steps before writing any code.
  • Recognize the half-dozen named anti-patterns that lose points on the APE (specificity ordering wrong, validation order wrong, “habit” checks added that the spec didn’t ask for, exact error message off by a space).
  • Translate the construct-recognition vocabulary the APE uses (“if X, throw Y”, “for each token”, “set field to”) into the Java construct it implies, automatically.
  • Walk into the exam having already practiced every shape the section can take.

Today in three sentences. The Practice APE File I/O section asks you to write six methods on two classes (InputFile and OutputFile) that share state through public static fields. Every method is a four-step recipe (validate, operate, handle errors, update state); the spec spells out which checks, which messages, which fields. The skill is reading the spec sentence by sentence and disagreeing with the spec only when you are absolutely certain it is wrong (and even then, trusting the JUnit tests, because that is what the grader runs).

From CSCD 210. You have already met the four-step pattern in lesson 7e and the file-class operations in lesson 7g. This lesson assumes both. The added skill is reading the prose of an APE spec the way you’d read code: as a precise instruction with no extra slack.


The four-step pattern, applied to APE prose

Every APE File I/O method has up to four parts in this order:

  1. Validate. Check that the inputs are usable. Common shapes: null check, empty-string check, “already-open” state check, file-exists check. Order matters: a null check goes before any check that would dereference the value (otherwise the dereference NPE-s before your nice error message can fire).
  2. Operate. Do the actual work: construct a Scanner or PrintStream, read or write, close.
  3. Handle errors. Decide what to do if the operate step throws or fails. The spec will tell you exactly which error message string to set, and exactly which value to return.
  4. Update state. If the class has fields like isOpen, errorMsg, inputFile, the spec will tell you which to flip and when. Updates happen after the operation succeeds (and the spec usually says so explicitly).

Reading skill: take the prose of a problem and tag each sentence with its step. Here is a tiny example.

Write a method int countNonBlankLines(String filename) that returns the number of non-blank lines. (Step 2: operate.) If the filename is null, throw IllegalArgumentException. (Step 1: validate.) If the file does not exist, return -1. (Step 3: handle errors.)

That tagging is the whole skill. Once each sentence has its label, the code falls out almost mechanically: a precondition check, a Scanner loop, an early-return on the file-missing case.

The construct vocabulary the APE uses, with the Java construct each phrase implies:

Spec phrase Java construct
“If X, throw Y with message M if (X) throw new Y(M);
“If X, set errorMsg to S and return” if (X) { errorMsg = S; return; }
“Set field to value field = value;
“Return value return value;
“Try to create / open try { ... } catch (...) { ... }
“Loop until end of file” while (sc.hasNext()) { ... } (or hasNextLine, etc.)
“For each token” while (sc.hasNext()) plus a single sc.next() inside
“Read a line / a token / an integer” sc.nextLine() / sc.next() / sc.nextInt()
“Close the file” resource.close();
“Build a string” / “append” result += ... (string concatenation)

That table is your decoder ring. Memorize the left column, recognize it on sight.

Common pitfall: writing the operate step before the validate step. If filename is null, the line new File(filename) throws NullPointerException before your nice “filename cannot be null” IllegalArgumentException ever runs. Validate first, every time. The order in the spec is the order in the code.


Exercise 1: openInputFile (the long version)

The first APE exercise is the most fully spelled out. Once you do it, the rest are variations.

Spec (paraphrased). Write boolean openInputFile(String fileName) that opens an input file. The class has public static fields inputFile (a Scanner), file (a File), isOpen (a boolean), errorMsg (a String).

If fileName is null, throw IllegalArgumentException with the message "file name passed to openInputFile is null". (Step 1.)

If fileName is the empty string, throw IllegalArgumentException with "file name passed to openInputFile is empty". (Step 1.)

If the file is already open, set errorMsg to "input file is already open" and return false. (Step 1, the state-conflict check.)

Construct a File from fileName. If the file does not exist, set errorMsg to "input file does not exist: " + fileName.getName() and return false. (Step 3, anticipated.)

Try to construct a Scanner on the file. (Step 2.) If the constructor throws FileNotFoundException, set errorMsg to "an exception occurred in openInputFile: " + e.getMessage() and return false. (Step 3.)

On success, set isOpen to true and return true. (Step 4 plus return.)

The translated code:

public static boolean openInputFile(String fileName) {
    // Step 1: validate
    if (fileName == null) {
        throw new IllegalArgumentException("file name passed to openInputFile is null");
    }
    if (fileName.equals("")) {
        throw new IllegalArgumentException("file name passed to openInputFile is empty");
    }
    if (isOpen) {
        errorMsg = "input file is already open";
        return false;
    }

    file = new File(fileName);
    if (!file.exists()) {
        errorMsg = "input file does not exist: " + file.getName();
        return false;
    }

    // Step 2: operate
    try {
        inputFile = new Scanner(file);
        // Step 4: update state
        isOpen = true;
        return true;
    } catch (FileNotFoundException e) {
        // Step 3: handle errors
        errorMsg = "an exception occurred in openInputFile: " + e.getMessage();
        return false;
    }
}

Read it line by line. Each block has a comment naming its step. The code’s structure matches the spec’s structure exactly; that is the point.

Two design notes that lose points on real exams.

Null check before empty check. If fileName is null, the line fileName.equals("") throws NullPointerException because you cannot call .equals on null. Reverse the order and the program crashes before it can throw the friendly IllegalArgumentException the spec wanted. Validate in dependency order: checks that would dereference go after the checks that protect them.

Trust the JUnit, not the Javadoc. The Javadoc on the APE has been known to disagree with the JUnit test in tiny ways (a stray double space, an off-by-one in punctuation). Whatever string the JUnit compares against is the one that scores points. Always read the test file carefully and copy the expected message character for character.

Common pitfall: adding a file.canRead() or file.isFile() check the spec didn’t ask for. It feels safer; it loses points. The grader compares your behavior against the exact spec. An extra check that returns false when the spec says “throw” gives different output than the test expects. Habit-driven over-checking is a recurring APE losing pattern.

Check your understanding. Suppose you reverse two lines so the empty-string check runs before the null check. What does the program do when openInputFile(null) is called?

Reveal answer

The line fileName.equals("") runs first. Calling .equals on null throws NullPointerException. The program crashes with the wrong exception type and an unhelpful message. The grader scores it as a test failure even though both checks “exist” in the source code, because the wrong one fired first. This is the canonical “validate in dependency order” lesson; null checks always go before any check that would call a method on the value.


Exercise 2: openOutputFile (subtle differences from Exercise 1)

The second exercise looks like the first but the spec changes two things, both of which lose points if you write Exercise 1 from muscle memory.

Difference 1: error handling for null and empty changes from throw to errorMsg + return. The spec says: if fileName is null, set errorMsg to "file name passed to openOutputFile is null" and return. Notice: return, not throw. The same shape of input gets a different response. The reason is that openOutputFile returns nothing meaningful (void); its only output channel is errorMsg.

Difference 2: there is no “file does not exist” check. Output files are created if they don’t exist; that’s how PrintStream works. The spec deliberately omits the existence check that Exercise 1 had, and the JUnit test deliberately exercises a fresh-file case to make sure you didn’t add one.

The translated code:

public static void openOutputFile(String fileName) {
    if (fileName == null) {
        errorMsg = "file name passed to openOutputFile is null";
        return;
    }
    if (fileName.equals("")) {
        errorMsg = "file name passed to openOutputFile is empty";
        return;
    }
    if (isOpen) {
        errorMsg = "output file is already open";
        return;
    }

    try {
        outputFile = new PrintStream(fileName);
        isOpen = true;
    } catch (FileNotFoundException e) {
        errorMsg = "exception occurred opening file named " + fileName;
    }
}

Notice what is not there: no file.exists() check, no IllegalArgumentException. Adding either gives different output than the JUnit expected.

Common pitfall: copying Exercise 1’s solution and “improving” it. The two methods look like they should be the same. They are not. Read each spec independently. Differences between sibling methods are deliberate and exist precisely to test whether you can read carefully.

Check your understanding. Why doesn’t openOutputFile need a file.exists() check?

Reveal answer

Because new PrintStream(fileName) creates the file if it does not exist. That is how output files work: opening one is opening a write target, and writing implies creating. The exception case the try/catch handles is not “file missing” (which would be normal); it is “the path is invalid in some other way” (e.g., the parent directory doesn’t exist, or you don’t have permission to write there). For input files, missing-file is an error; for output files, missing-file is the default starting state.


Exercises 3 and 4: closing files and cross-class field access

The third and fourth exercises have you write closeInputFile and closeOutputFile, in a different class than the one that holds the fields. That detail is the new lesson.

Spec (close input). Write closeInputFile() in a class called InputFileClose. If InputFile.isOpen is true, close InputFile.inputFile, then set InputFile.inputFile, InputFile.file, and InputFile.errorMsg to null, and set InputFile.isOpen to false. Otherwise, set InputFile.errorMsg to "attempt to close input file that is not open".

The shape:

public static void closeInputFile() {
    if (InputFile.isOpen) {
        InputFile.inputFile.close();
        InputFile.inputFile = null;
        InputFile.file = null;
        InputFile.errorMsg = null;
        InputFile.isOpen = false;
    } else {
        InputFile.errorMsg = "attempt to close input file that is not open";
    }
}

Read carefully. The class doing the close (InputFileClose) has no fields of its own. Every read and write reaches across to InputFile’s public static fields. The syntax is the same syntax you’d use for Math.PI or Integer.MAX_VALUE: ClassName.fieldName. Public static fields are reachable by any class that knows the class name.

Two ordering rules that lose points if reversed.

Close before nulling. If you write InputFile.inputFile = null; first and then try to call InputFile.inputFile.close(), the close call throws NullPointerException. You set the only reference to the Scanner to null, so you can no longer reach the underlying file handle. The OS resource leaks (the file stays open); the program may crash; the JUnit fails. Always release the resource first, then release the reference. This is a universal rule that applies to database connections, network sockets, and graphics contexts as much as it does to file handles.

Forgetting the prefix. If you write isOpen = false; inside InputFileClose (without the InputFile. prefix), the compiler reports cannot find symbol. InputFileClose has no field called isOpen. The spec is explicit about which fields belong to which class; respect the prefixes.

Spec (close output). Same shape as close input, except the not-open branch has no else clause in this exercise. The spec says: if open, do the close; if not open, the spec says nothing, so do nothing. The JUnit test confirms by checking that errorMsg remains null after a not-open close call.

public static void closeOutputFile() {
    if (OutputFile.isOpen) {
        OutputFile.outputFile.close();
        OutputFile.errorMsg = null;
        OutputFile.outputFile = null;
        OutputFile.isOpen = false;
    }
    // No else: the spec is silent, so do nothing.
}

Common pitfall: adding a “by habit” else from Exercise 3. Two sibling methods look so similar that students copy the close-input code and tweak the field names. The else clause is the trap. The spec for closeOutputFile doesn’t mention the not-open case; doing anything in that case (even setting errorMsg) gives wrong output and fails the JUnit. When the spec says nothing, do nothing.

Check your understanding. A student writes InputFile.inputFile = null; before the call to close(). What happens at runtime, and why is this almost worse than a crash?

Reveal answer

Two things happen. (1) The next line, InputFile.inputFile.close(), throws NullPointerException because InputFile.inputFile is now null. (2) The OS resource (the open file handle) is leaked: there is no longer any way to call .close() on the underlying Scanner because we threw away the only reference to it. The program may keep running with a stuck file handle. This is “almost worse than a crash” because crashes are loud; resource leaks are silent and accumulate quietly until the program runs out of file descriptors and starts behaving mysteriously. Always release the resource first, then release the reference.


Exercise 5: read() and the specificity-ordering principle

The fifth exercise puts the reading vocabulary from lesson 7c into practice with a small twist. You loop through every token in the file; for each, you classify it as int, double, or string and append a labeled line to a result string.

Spec (paraphrased). Write String read(). If the input file is not open, return "File not open for reading". If it is open, loop until end of file. For each token: if it parses as an int, append "Integer: " + value + "\r\n"; if it parses as a double, append "Double: " + value + "\r\n"; otherwise (it is a string), append "String: " + value + "\r\n". Return the built string.

The translated code:

public static String read() {
    if (!InputFile.isOpen) {
        return "File not open for reading";
    }

    String result = "";
    while (InputFile.inputFile.hasNext()) {
        if (InputFile.inputFile.hasNextInt()) {
            result += "Integer: " + InputFile.inputFile.nextInt() + "\r\n";
        } else if (InputFile.inputFile.hasNextDouble()) {
            result += "Double: " + InputFile.inputFile.nextDouble() + "\r\n";
        } else {
            result += "String: " + InputFile.inputFile.next() + "\r\n";
        }
    }
    return result;
}

The spec turns into the cascade pattern from lesson 7c, with one APE detail to notice: the line separator is "\r\n", not \n. This is the Windows-style two-byte line terminator (carriage return plus newline). The JUnit on the APE uses \r\n exactly so the expected output looks identical on every platform; matching the JUnit means writing \r\n literally in your concatenation. Don’t substitute \n or %n and assume “the platform will figure it out.”

The cascade order matters here for the same reason as in lesson 7c: every int passes hasNextDouble, so checking hasNextDouble first would silently swallow ints and report them as doubles. Specificity ordering: most specific check first. The same principle applies in catch blocks (catch specific exceptions before general ones) and in pattern matching in functional languages. Once you internalize it, you spot it everywhere.

A short trace to anchor the pattern. Suppose the file contains the tokens a1.0 2.0 33 44.55 55 f789.123f.

Token hasNextInt? hasNextDouble? Branch fired Output appended
a1.0 no no else (string) String: a1.0\r\n
2.0 no yes double Double: 2.0\r\n
33 yes (skipped) int Integer: 33\r\n
44.55 no yes double Double: 44.55\r\n
55 yes (skipped) int Integer: 55\r\n
f789.123f no no else (string) String: f789.123f\r\n

Six tokens, three categories, every one matches the spec because the cascade is in the right order.

Common pitfall: writing System.lineSeparator() or %n instead of \r\n. Both look more “correct” on a multi-platform Java project. Both produce wrong output on the APE because the JUnit compares against the literal four characters \r\n regardless of the platform the test runs on. Match the JUnit’s literal; don’t try to be clever.


Exercise 6: writeToOutputFile(Object data)

The last exercise is the shortest. You take an Object parameter and write it to the output file via println.

Spec (paraphrased). Write void writeToOutputFile(Object data). If the output file is not open, set errorMsg to "attempt to write to file that is not open". Otherwise, call outputFile.println(data).

public static void writeToOutputFile(Object data) {
    if (!OutputFile.isOpen) {
        OutputFile.errorMsg = "attempt to write to file that is not open";
        return;
    }
    OutputFile.outputFile.println(data);
}

The recognition note: the parameter type is Object, not String. That is polymorphism: println(Object) is one of the overloads of println on PrintStream, and it calls .toString() on whatever you pass. So you can pass a String, an Integer, a Double, a Date, your own custom object, anything. The receiver does the right thing because Java is an object-oriented language and Object is the universal supertype.

You will return to this idea in CSCD 211 when you start designing your own classes. For now, recognize the shape: when a method takes Object, it accepts everything, and it works because toString() is defined on every Java object.

Check your understanding. What does outputFile.println(42) write to the file, and how does PrintStream know to convert 42 from an int to text?

Reveal answer

Writes 42\n (or 42\r\n on Windows). PrintStream.println(int) is one of the overloaded println methods; the JDK has separate overloads for int, double, boolean, char, String, Object, and a few more. Each one knows how to format its argument as text. There is no magic; Java’s overload resolution picks the best-matching println for the argument type at compile time, and that overload contains the code that turns the value into characters. The Object overload is the catch-all that handles anything not covered by a more specific overload by calling toString().


The unit, in one page

You have now seen every shape the file-I/O unit can throw at you. Here is the one-page recap that ties this lesson back to the rest of the unit.

Read side. Build a Scanner around a new File(path). Declare throws FileNotFoundException on main. Loop with hasNext/hasNextLine/hasNextInt/hasNextDouble and read with next/nextLine/nextInt/nextDouble. Order the cascade most-specific to least-specific. Watch for the leftover-newline trap when mixing nextInt and nextLine. Close when done.

Write side. Build a PrintStream around a new File(path). Use print/println/printf exactly the way you do on System.out. Close when done so the buffer flushes. For append mode, wrap a FileOutputStream(path, true). For nested paths, call getParentFile().mkdirs() first.

Array bridge. When the file’s count is unknown, walk the file twice: once to count, once to fill into a freshly allocated array. Then do Week 6 array work and write the result.

APE pattern. Every method is four steps: validate, operate, handle errors, update state. Read the spec sentence by sentence and tag each with its step before writing code. Trust the JUnit, not the Javadoc. Don’t add checks the spec didn’t ask for. Validate in dependency order (null before equals).

Recognition. The other I/O classes (BufferedReader, PrintWriter, FileWriter, BufferedWriter, Files.readString, Files.writeString) exist for performance, encoding, and convenience reasons. You’ll see them in real code; this course writes Scanner plus PrintStream.

That’s the unit. Six lessons of mental model, two lessons of recognition, this lesson of capstone practice.


Wrap up

Recap.

  • Every APE File I/O method is a four-step recipe (validate, operate, handle errors, update state) the spec spells out almost mechanically.
  • Read each spec sentence by sentence and tag with its step before writing code; the code structure matches the tag structure.
  • Validate in dependency order (null before equals; existence before open).
  • Cross-class field access uses the ClassName.fieldName syntax, the same shape as Math.PI.
  • Specificity ordering is universal: hasNextInt before hasNextDouble, specific exceptions before general, narrow patterns before wide.
  • Match the JUnit’s literal expected strings, including \r\n line separators and exact spacing. Trust the JUnit over the Javadoc when they disagree.
  • “When the spec says nothing, do nothing.” Habit-driven extra checks lose points.
  • Sibling methods are deliberately different; read each one independently.

What you can do now. Walk into the APE File I/O section having seen every shape it can take. Tag each spec sentence with its four-step role. Translate each tag to the Java construct it implies. Resist the temptation to “improve” a spec by adding extra checks. Recognize the half-dozen named anti-patterns and avoid all of them.

Looking ahead. This is the last lesson of the file-I/O unit. The unit closes here; the next chapter is classes and objects, where you will replace the public-static-fields shape from the APE problems with proper instance fields on instances of your own classes. The four-step pattern will reappear there: every method on a class you design has the same validate/operate/handle/update structure, just on instance state instead of static state.

Revisit the File I/O series index to drop back into a specific lesson when you need it.


  • The full APE deep-dive lecture (week8-deepdive-fileio.tex in the course materials) walks each exercise in even more detail, with side-by-side wrong-way and right-way examples for every common student mistake.
  • The APE practice exam (login required, EWU SSO) is the source of truth. Download the practice File I/O section and try the six methods cold; then check your work against this lesson.
  • Reges & Stepp, Building Java Programs, Chapter 6 on file I/O is the textbook reference for every Scanner and PrintStream call this unit makes.