file-io 13 min read

Writing Files with PrintStream

System.out is a PrintStream — pointing one at a file is the only new step

In a nutshell

Writing to a file is the most familiar topic in this unit. You’ve been doing it all quarter — System.out.println(...) writes to a file. The “file” just happens to be the console.

Specifically: System.out is an instance of the java.io.PrintStream class. The class has a constructor that takes a File and gives you the same object, pointed at a file on disk instead of the console. Same API. Same print, println, printf.

Three things to get from this lesson:

  1. System.out is a PrintStream. This isn’t a metaphor. The Java standard library writes literally that: public static final PrintStream out. Knowing this makes the rest of the lesson easy.
  2. new PrintStream(new File(path)) builds a file PrintStream. Same constructor shape as new Scanner(new File(path)). Same throws FileNotFoundException.
  3. Close it when done with out.close(). Without that call, the file may be empty or missing its last few lines. Closing flushes pending output to disk.

Today in three sentences. Writing to a file uses the same print/println/printf methods you’ve used all quarter. The only new step is pointing the PrintStream at a file. Close it when you’re done so the writes actually land on disk.

After this lesson, you will be able to:

  • Construct a PrintStream that writes to a file and use print, println, printf on it.
  • Explain why System.out.println(...) and out.println(...) are the same method call, just with different receivers.
  • Identify the bug where missing out.close() produces an empty or truncated file.
  • Combine reading and writing in one program with a single throws FileNotFoundException declaration covering both.

From CSCD 110. Python’s print(x, file=f) is the same idea routed through a parameter; Java’s idea is to put the writing through an object (out) whose only job is to be a place writes go. The object can be the console (System.out) or a file (new PrintStream(new File(...))). Same API, different destination.


System.out is already a PrintStream

Read this Java standard-library declaration:

package java.lang;

public final class System {
    public static final PrintStream out;
    public static final PrintStream err;
    // ...
}

System.out is a static field of type PrintStream. Every call you’ve ever made — System.out.println("hi"); — has been a method call on a PrintStream instance. There has not been any “console magic” hiding in println. The instance happens to be wired up at JVM startup to send its output to your terminal.

That means everything you already know about print, println, printf, and format is already PrintStream knowledge:

System.out.print("no newline");
System.out.println("with newline");
System.out.printf("%-10s %5.2f%n", "ratio:", 3.14);

When we make a new PrintStream pointed at a file, the same calls work the same way. The terminal output stops happening; the file output starts happening; the program doesn’t know the difference.

Mental model: PrintStream is a one-way pipe with a familiar API. One end is the print / println / printf methods. The other end is wherever the PrintStream was wired up to send bytes — the console for System.out, a file for new PrintStream(new File(path)). The pipe doesn’t change shape; only what’s at the far end changes.


Pointing a PrintStream at a file

The constructor pattern matches the Scanner-on-File pattern from lesson 7b:

PrintStream out = new PrintStream(new File("report.txt"));

That’s the entire new piece of syntax. Now use out exactly the way you use System.out:

out.println("Sorted: [62, 70, 78, 85, 91]");
out.println("Median: 78");
out.printf("Mean:   %.2f%n", 79.2);
out.print("Notes (no newline): ");
out.println("done.");

The output ends up in report.txt instead of the terminal. Open the file in any text editor and you see the lines you printed.

A few practical notes.

The constructor throws FileNotFoundException. Same checked exception as Scanner-on-File. If you already have throws FileNotFoundException on main (from your reading code), you’re covered for the writing code too. One declaration covers all the file-related calls in main.

The file is created if it doesn’t exist; truncated to empty if it does. Constructing new PrintStream(new File("report.txt")) opens the file for writing from the start. Anything that was previously in report.txt is gone the moment the constructor runs. If you wanted to append instead, you’d use a different constructor (out of scope for this course).

Don’t pass a File for a folder. new PrintStream(new File("/Users/jessica/data/")) will fail at runtime — you can’t write a file at a directory path. The path must name an actual (or potential) file.

Common pitfall: opening a PrintStream on the same file you’re reading from. Doing both at once almost never produces what you want — the writes happen as the reads happen, and the file you’re reading is being mangled in real time. Read from input.txt, write to output.txt. Two separate paths.

Check your understanding. What’s in out.txt after this code runs (assuming the file did not exist before)?

PrintStream out = new PrintStream(new File("out.txt"));
out.println("first");
out.print("no");
out.print("newline");
out.println();
out.println("third");
out.close();
Reveal answer
first
nonewline
third

Three lines. Line 2 is nonewline because two print calls (no newline) followed by an empty println (just a newline) produce a single line with nonewline on it. The println() with no argument simply emits a \n.


Closing matters

When you call out.println(...), your text doesn’t necessarily land on disk immediately. PrintStream buffers writes — it accumulates them in memory and writes them to disk in larger chunks for efficiency. The buffer flushes to disk:

  • When the buffer fills up.
  • When you call out.flush() explicitly.
  • When you call out.close() (which flushes, then closes the file).
  • When the program exits normally (the JVM’s shutdown hooks flush most streams).

What happens if your program crashes, or exits in an unusual way, with text still in the buffer? That text never reaches the disk. The file ends up empty, or missing the last few lines you thought you wrote.

The fix is simple: call out.close() when you’re done writing. That single call flushes the buffer and releases the file handle.

PrintStream out = new PrintStream(new File("report.txt"));
out.println("Hello");
out.println("World");
out.close();   // <-- without this, "Hello" and "World" might not be in report.txt

For the small programs in this course, you’ll often get away with skipping close() because the program exits cleanly. But the habit of closing matters — there will be a lab or exam where the missing close produces an empty file, and the bug looks invisible because the code that produced the output looks correct.

Common pitfall: blank or truncated output file. If you ran your program and report.txt is empty (or shorter than you expected), the most likely cause is a missing out.close(). Add the close call. Re-run. The output will be there.

Mental model: close() is “flush and shut.” Like running water through a pipe one last time before turning off the valve. The valve being shut is the file handle being released; the running-through is the buffer flushing. Both matter.

Check your understanding. A program writes 1,000 lines to a file with out.println(...), never calls out.close(), and the JVM exits via System.exit(0) (an abrupt non-error exit). Will all 1,000 lines be in the file?

Reveal answer

Maybe, maybe not. System.exit(0) skips most of the JVM’s normal shutdown sequence; some of the buffered writes may never flush. The safe answer is to call out.close() (or out.flush()) before any potential abrupt exit. Don’t rely on the JVM to clean up after you. The “habit of closing” exists exactly to avoid this case.


End-to-end: reading, computing, writing

Putting Scanner-on-File and PrintStream-on-File together. The program reads a file of test scores, computes the mean, and writes a one-line report to a different file.

scores.txt:

78
85
62
91
70

The program:

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

public class MeanReport {

    public static void main(String[] args) throws FileNotFoundException {
        Scanner sc = new Scanner(new File("scores.txt"));

        int sum = 0;
        int count = 0;
        while (sc.hasNextInt()) {
            sum += sc.nextInt();
            count++;
        }
        sc.close();

        double mean = (double) sum / count;

        PrintStream out = new PrintStream(new File("mean.txt"));
        out.printf("Read %d scores. Mean = %.2f%n", count, mean);
        out.close();
    }
}

After the program runs, mean.txt contains:

Read 5 scores. Mean = 77.20

Three things to notice.

One throws covers both. main is declared throws FileNotFoundException. Both the Scanner constructor and the PrintStream constructor can throw it; one declaration handles both.

The Scanner closes before the PrintStream opens. Not strictly required, but a tidy habit: each resource is closed where its work ends.

The PrintStream closes at the end. Without that line, mean.txt could end up empty.

Check your understanding. Why does the program use sc.hasNextInt() instead of sc.hasNextLine() for the read loop?

Reveal answer

Because the data is one number per line, and hasNextInt() + nextInt() reads the number directly. Using hasNextLine() + nextLine() would read each line as a String, and you’d have to convert with Integer.parseInt(line) for each one. Both work; the int-token form is more idiomatic when the file is a list of integers. Use the reader that matches the data’s natural shape.


Wrap up and what’s next

Recap.

  • System.out is a PrintStream. You’ve been writing PrintStream code all quarter; only the destination is new.
  • new PrintStream(new File(path)) constructs a PrintStream wired to a file. Throws FileNotFoundException, covered by the same throws you already have for Scanner.
  • Use print, println, printf exactly the way you do on System.out.
  • Close with out.close() to flush the buffer. Missing close → empty or truncated file.
  • Read from one path, write to a different path. Don’t open a write PrintStream on a file you’re still reading.

What you can do now. Write end-to-end programs that read input from one file and emit a report to another. Use printf to format the output cleanly. Recognize the empty-output-file bug as a missing-close issue.

Next up: From File to Array: the Count-Allocate-Fill Pattern. The dominant pattern for the rest of this week and for the APE: when you don’t know the size of the array in advance, count the lines first, allocate an array of that exact size, then fill it on a second pass. End-to-end worked example reading from a file, sorting, and writing back.


  • The PrintStream Javadoc lists every constructor and method. The (File) constructor and the print/println/printf family are the load-bearing ones for this course.
  • Reges & Stepp, Building Java Programs, Chapter 6 section 6.4 on output to files.