file-io 16 min read

File-Class Power Tools: Append, Mkdirs, Inspect, Delete

What the File class can do beyond exists() and getAbsolutePath()

In a nutshell

So far you have used a File object as a path that gets handed to a Scanner or a PrintStream. That is most of what you need. But the File class can do more, and a handful of those operations show up on lab problems and APE problems often enough that they are worth a focused lesson.

This lesson covers four practical jobs that most “read a file, do work, write a file” programs eventually need:

  1. Append to a file instead of overwriting it (so a log keeps growing run after run).
  2. Create the parent directory before writing if it doesn’t exist (so new PrintStream(new File("output/results/2026/scores.txt")) doesn’t blow up).
  3. Inspect a file with the predicates and accessors File provides (isFile, isDirectory, length, lastModified).
  4. Manage a file: rename, delete, list the contents of a directory.

These are not new I/O classes. They are extra methods on the same File you already know. The whole lesson is about getting more value out of one class you’ve already met.

Today in three sentences. new PrintStream(new File(path)) truncates by default, so for log-style files you wrap it around a FileOutputStream with the append flag set to true. Parent directories don’t appear automatically; if you need a folder, call getParentFile().mkdirs() first. The File class also has the predicates and accessors you’d expect (isFile, length, delete, renameTo, listFiles); knowing they exist lets you write file-management code without bringing in any new library.

After this lesson, you will be able to:

  • Open a file in append mode using new PrintStream(new FileOutputStream(path, true)).
  • Ensure a parent directory exists with f.getParentFile().mkdirs() before writing.
  • Use isFile, isDirectory, length, and lastModified to inspect a file you’ve been handed.
  • Delete and rename files programmatically, and iterate over a directory’s contents with listFiles().

From CSCD 110. Python uses open(path, "a") for append, os.makedirs(dirpath, exist_ok=True) for creating folders, and os.remove / os.rename for management. Java does the same jobs through method calls on File and constructor flags on streams. The vocabulary is different; the operations are not.


Append mode: keeping yesterday’s writes

new PrintStream(new File("log.txt")) opens the file for writing from the start. Anything that was previously in the file is gone the moment the constructor runs. That’s right for a fresh report; it’s wrong for a log that should accumulate across runs.

Append mode says: open the file, but park the cursor at the end so new writes go after the existing contents. There is no “second PrintStream constructor” that does this directly, but the byte-stream side of the family has a FileOutputStream constructor that takes a boolean append flag, and PrintStream accepts a FileOutputStream as its source. Wrap one in the other:

PrintStream log = new PrintStream(new FileOutputStream("log.txt", true));
log.println("[" + new Date() + "] program ran");
log.close();

Read the chain inside-out. new FileOutputStream("log.txt", true) opens the file in append mode (true is the magic word). new PrintStream(...) wraps that stream so you get the familiar print / println / printf API. The true flag is the only difference between this and the truncating version you’ve been using.

Each subsequent run of the program adds another line to the file. Yesterday’s content is preserved; today’s is appended. This is the shape every log file in the world has.

If you want to be explicit that you are appending text and not binary data, the equivalent recipe with the character-stream classes from lesson 7f is new PrintWriter(new FileWriter(path, true)). The FileWriter constructor’s second argument is the same append flag. Either chain works; pick the one whose top-level class matches the rest of your program.

Common pitfall: forgetting the true and accidentally clobbering the log. A program that wrote new PrintStream(new FileOutputStream("log.txt")) (no second argument, defaults to false) wipes the file every time you run it. The truncate happens silently when the constructor runs. If you noticed that “the log only ever has one line in it”, check the boolean.

Common pitfall: opening a PrintStream directly in append mode without the FileOutputStream wrapper. There is no new PrintStream(File, true) constructor. The append flag lives on FileOutputStream. Forgetting that and trying to pass a boolean to PrintStream directly produces a “no such constructor” compile error. Read the error, reach for the wrapper.

Check your understanding. A program runs three times in a row. After each run, you check log.txt and see exactly the line that the most recent run wrote (no history). Where is the bug?

Reveal answer

The program is opening the file with truncating mode: probably new PrintStream(new File("log.txt")) or new PrintStream(new FileOutputStream("log.txt")) (defaulting to false). The fix is to switch to append mode: new PrintStream(new FileOutputStream("log.txt", true)). The true is the entire fix.


Parent directories don’t appear by magic

Try this without preparing the directory first:

PrintStream out = new PrintStream(new File("output/2026/scores.txt"));

If the folder output/2026/ does not already exist on disk, the constructor throws FileNotFoundException with a message like (No such file or directory). The PrintStream constructor will create the file scores.txt, but it will not create the folder it lives in. That’s a separate operation.

The right move is to materialize the parent directory before opening the stream. Two File methods do most of the work:

  • f.getParentFile() returns the folder portion of the path as another File. For output/2026/scores.txt, that’s output/2026.
  • f.mkdirs() creates the directory chain (every missing folder along the path) and returns true if it created anything. The s is important: mkdir (no s) only creates the last folder; mkdirs creates every missing parent on the way down.

The idiom:

File outFile = new File("output/2026/scores.txt");
File parent = outFile.getParentFile();
if (parent != null) {
    parent.mkdirs();   // safe if the folder already exists; returns false in that case
}
PrintStream out = new PrintStream(outFile);

Two notes worth keeping. First, mkdirs is idempotent: if the directory already exists, the call does nothing and returns false. You don’t have to check whether the folder is already there; the call is safe either way. Second, getParentFile returns null when the file path has no parent component (a bare filename like "scores.txt"). Guarding with if (parent != null) is the safe form even though most lab paths will have a parent.

This three-line dance shows up in real-world batch-processing code constantly: backup scripts that organize output by year/month, save-game systems that put each user in their own subfolder, log rotators that create a new directory per day. The pattern is so common that it has a name: “ensure the directory exists.”

Common pitfall: mkdir instead of mkdirs. mkdir only creates one level. If output/ exists but output/2026/ does not, parent.mkdir() succeeds. If neither exists, parent.mkdir() returns false because it cannot create output/2026/ while output/ is still missing. mkdirs creates the whole chain in one call. Use the plural unless you have a specific reason not to.

Common pitfall: calling mkdirs() on the file path instead of its parent. outFile.mkdirs() would try to create output/2026/scores.txt/ as a directory, which is almost certainly not what you want and will collide with the actual file you’re about to write. The directory you want to create is the parent, not the file itself.

Check your understanding. A program writes a daily report to reports/<year>/<month>/<day>.txt. The first day it runs the program crashes with FileNotFoundException. After that, every subsequent day it works. What’s happening, and how do you fix it once and for all?

Reveal answer

The first run is the day the year-and-month folder doesn’t exist yet. The constructor for PrintStream cannot create those parent folders on its own. After the first day fails, presumably someone created the folders manually, so subsequent days find them already in place. The permanent fix is the getParentFile().mkdirs() dance before the PrintStream constructor: File f = new File(path); f.getParentFile().mkdirs(); PrintStream out = new PrintStream(f);. Now the year and month folders are created on demand on day 1, and the program never has the problem again.


Inspecting a file

Some programs need to look at a file before they read it. Maybe you only want to process files larger than a kilobyte. Maybe you want to skip directories that snuck into a list of files. Maybe you want to print the file’s modified time in a report. The File class has predicates and accessors for these jobs; you’ve already met exists() and getAbsolutePath(). Here is the rest of the useful set.

Method Returns What it tells you
f.exists() boolean Is there anything (file or directory) at this path?
f.isFile() boolean Is this path an actual file (not a directory or special node)?
f.isDirectory() boolean Is this path a directory?
f.canRead() boolean Does the running process have permission to read it?
f.canWrite() boolean Does the running process have permission to write to it?
f.length() long Size of the file in bytes. (Returns 0 for directories.)
f.lastModified() long Milliseconds since 1970-01-01 of the last write.
f.getName() String The bare filename (no folders). output/2026/scores.txt returns scores.txt.
f.getParent() String The folder portion as a string. (See also getParentFile().)
f.getAbsolutePath() String The full path resolved against the current working directory.

A small example that ties several of these together: print a one-line summary of every file that the program has been handed.

public static void describe(File f) {
    if (!f.exists()) {
        System.out.println(f.getName() + ": does not exist");
        return;
    }
    if (f.isDirectory()) {
        System.out.println(f.getName() + ": directory");
    } else {
        System.out.println(f.getName() + ": " + f.length() + " bytes");
    }
}

Three important details. (1) exists() is checked first: every other accessor is undefined for a path that doesn’t exist on disk. (2) isDirectory() and isFile() are mutually exclusive when the path exists, but neither is true if the path is missing. (3) length() returns 0 for directories regardless of contents; if you want to know how much disk a folder is using, you have to walk its contents and sum the sizes yourself.

This kind of pre-flight inspection is the bread and butter of any program that processes a folder full of unknown files. Real example: a batch script that processes every .txt in a directory checks isFile() and getName().endsWith(".txt") before opening each one, skipping directories and non-text files cleanly.

Common pitfall: assuming exists() implies isFile(). exists() is true for both files and directories. If you wrote new Scanner(new File(path)) immediately after exists() and the path turned out to be a folder, the constructor throws an exception. Pair exists() with isFile() for read-side validation: the file exists and it is actually a file, not a directory.

Check your understanding. Given File f = new File("data"); (no extension), what does f.isDirectory() return?

Reveal answer

It depends on whether data exists on disk and what kind of thing it is. If data is a folder, true. If data is a file (extensions are conventional in Java, not required), false. If data does not exist at all, false. The class File cannot tell file from directory just from the path string; the answer is determined by what’s actually on disk at the moment the call runs.


Managing a file: delete, rename, list

The last batch of File methods are the management operations. Each is a single method call.

Delete. f.delete() removes the file (or empty directory) and returns true on success, false on failure. Failure modes include “file doesn’t exist”, “another process has it open”, and “you don’t have permission.” There is no exception; you have to check the return value if you care.

File temp = new File("scratch.tmp");
boolean deleted = temp.delete();
if (!deleted) {
    System.out.println("could not delete " + temp.getAbsolutePath());
}

Rename or move. f.renameTo(File dest) renames the file at f’s path to dest’s path. If dest is in a different folder, this acts as a move. Returns true on success, false on failure. Like delete, it does not throw; it just reports.

File draft = new File("report.draft.txt");
File final_ = new File("reports/2026/report.txt");
final_.getParentFile().mkdirs();
boolean moved = draft.renameTo(final_);

List a directory. dir.listFiles() returns an array of File objects, one for each entry in the directory. The order is unspecified by the JDK (most file systems return alphabetical, but you cannot rely on it). Returns null if the path is not a directory or you don’t have permission to list it.

File dir = new File("reports/");
File[] entries = dir.listFiles();
if (entries != null) {
    for (File entry : entries) {
        if (entry.isFile() && entry.getName().endsWith(".txt")) {
            System.out.println(entry.getName() + " (" + entry.length() + " bytes)");
        }
    }
}

Read that example carefully. It’s the recognition shape of a batch processor: walk every entry in a folder, filter to the ones that match a pattern, do work on each. Real programs that fit this shape include log analyzers (every .log in a folder), grading scripts (every student’s submission), photo organizers (every .jpg), and ETL pipelines (every CSV from yesterday’s drop). You will write code with this shape long after CSCD 210 ends.

A small but important quirk: listFiles() returns null (not an empty array) if the path is not a valid directory. You have to null-check before iterating, or you’ll get a NullPointerException on the for loop. The defensive form if (entries != null) { ... } is the idiomatic guard.

Common pitfall: ignoring the return value of delete or renameTo. Both methods report failure through their return value, not through an exception. If you write f.delete(); and don’t check the result, you have no way to know whether the file was actually deleted. The fix is to capture the boolean and act on it (or at least log a warning). For one-off scripts the consequences are mild; for production code they are not.

Check your understanding. A program tries to rename tmp.txt to archive/tmp.txt. The folder archive/ does not yet exist. What does tmp.renameTo(new File("archive/tmp.txt")) return, and why?

Reveal answer

Returns false. renameTo cannot create parent directories; if the destination’s folder doesn’t exist, the rename fails silently and reports false. The fix is the same as the write-side fix: ensure the parent exists first. new File("archive/tmp.txt").getParentFile().mkdirs(); tmp.renameTo(new File("archive/tmp.txt"));. The mkdirs call materializes the folder; the rename then has somewhere to land.


Wrap up and what’s next

Recap.

  • Append mode preserves existing contents. The recipe is new PrintStream(new FileOutputStream(path, true)); the true is the entire append flag.
  • The parent directory is not created automatically. Before opening a PrintStream on a nested path, call f.getParentFile().mkdirs() to ensure the folder exists.
  • The File class has predicates and accessors you’ll use in real programs: isFile, isDirectory, length, lastModified, getName, getParent. Use exists plus isFile together when you need to validate a read target.
  • File management is method calls on File: delete, renameTo, listFiles. Each reports failure through its return value, not an exception, so check the boolean.
  • These operations turn “I can read and write one file” into “I can manage a folder full of them,” which is the shape of most real batch programs.

What you can do now. Write programs that maintain a log file across runs, organize output into year-and-month subfolders, validate a file before opening it, delete temporary scratch files, and walk a directory listing the entries that match a pattern.

Next up: APE File I/O Walkthrough. The capstone lesson for the unit. Six APE-style File I/O exercises, dissected sentence by sentence with the four-step pattern. By the end you will have seen every major shape an APE problem can take and named the trap for each one.


  • The java.io.File Javadoc lists every method on the class. The recognition table here covers the ones used in CS1 and most internships.
  • The java.io.FileOutputStream Javadoc documents the append-flag constructor. Its character-stream cousin FileWriter has the same boolean append parameter.
  • Reges & Stepp, Building Java Programs, Chapter 6 mentions a handful of File methods; this lesson covers the practical superset most lab and APE problems ask about.