CSCD210

Printstream and printwriter

Skillcscd210-printstream-and-printwriterTextbookBJP Ch 6

Constructing a PrintStream on a File

Student can construct a PrintStream over a File, declare or catch the FileNotFoundException the constructor throws, write output with println/printf, and close() the stream to flush buffered bytes.

To write to a file, construct a PrintStream over a File and then use the same println / print / printf methods you have used on System.out.

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

public static void writeReport(final String filename) throws FileNotFoundException {
    final PrintStream out = new PrintStream(new File(filename));
    out.println("Report");
    out.println("------");
    out.printf("Pi: %.4f%n", Math.PI);
    out.close();   // flush + release the OS file handle
}

Four things to know about this constructor:

1. It creates the file if it does not exist. Unlike the read side (where a missing file causes FileNotFoundException), the write side's FileNotFoundException only fires when the path is invalid: typically because a directory in the path does not exist, or the program lacks permission to write there. A missing leaf file is created.

2. It overwrites any existing file with the same name. Opening for write truncates the file to length zero before any data is written. If you wanted to append instead, you would need the (File, String) or (File, Charset) overload with append=true, but the simple constructor truncates.

3. It is declared to throw FileNotFoundException (same as the Scanner(File) constructor on the read side). The handle-or-declare rule (JLS §11.2) applies; every method that constructs a PrintStream(File) must declare or catch.

4. Buffering means close() is mandatory. PrintStream buffers output internally. Bytes you "printed" may still be in the buffer when the program exits, and if the JVM crashes before close() runs, those bytes are lost. Calling close() flushes the buffer to disk and releases the OS file handle. Forgetting to close is the classic "my file is empty even though I printed to it" bug.

When the directory does not exist

new PrintStream(new File("missing-dir/out.txt")) throws FileNotFoundException with the message "missing-dir/out.txt (No such file or directory)". The constructor does not create parent directories. Pre-flight by checking the parent exists, or use f.getParentFile().mkdirs() if creating the tree is acceptable.

In other languages

  • Python: open(filename, "w") truncates; open(filename, "a") appends. Use a with block to auto-close.
  • C: fopen(filename, "w") truncates; fopen(filename, "a") appends. fclose is mandatory.
  • Bash: > file.txt truncates; >> file.txt appends.

System.out is a PrintStream

Student can identify System.out as a value of type java.io.PrintStream, and write a method that takes a PrintStream parameter so the caller can choose between console and file output.

System.out is not a special language construct: it is a public static final field on the class System, and its declared type is java.io.PrintStream. Every method students have used since Week 1 (println, print, printf) is a method on PrintStream.

package java.lang;

public final class System {
    public static final PrintStream out = ...;   // bound at JVM startup
    public static final PrintStream err = ...;
    // ...
}

System.out is therefore one PrintStream among others. Any method that takes a PrintStream parameter can be called with System.out, which writes to the console, or with a PrintStream opened on a file. The same println body writes to whichever destination the caller supplied.

public static void printReport(final PrintStream out, final int[] values) {
    out.println("Count: " + values.length);
    out.println("Min: " + min(values));
    // ...
}

// Caller A: write to the console
printReport(System.out, data);

// Caller B: write to a file
final PrintStream fileOut = new PrintStream(new File("report.txt"));
printReport(fileOut, data);
fileOut.close();

// Caller C (Lab 11 style): print to both
printReport(System.out, data);
printReport(fileOut, data);
fileOut.close();

This is the single most useful refactor introduced in Week 7. Earlier labs hard-coded System.out.println(...) inside every print method, which meant those methods could only ever produce console output. Once the method takes a PrintStream parameter, the destination is no longer baked into the method: it becomes the caller's decision.

Why is System.out a PrintStream and not a PrintWriter?

Historical reasons. PrintStream came first (Java 1.0) and handled bytes. PrintWriter arrived in Java 1.1 and handled characters with proper encoding support. For backward compatibility, System.out kept its original type. For new file output, either type works at the CS1 level; CSCD 210 uses PrintStream because it matches System.out and supports the same printf/println API the students already know.

In other languages

  • Python: sys.stdout is a file-like object; print accepts a file= keyword to redirect.
  • C: stdout is a FILE*; fprintf(stdout, ...) is printf(...) and fprintf(my_file, ...) redirects.
  • Go: os.Stdout is an io.Writer; functions take io.Writer parameters by convention so they work with any destination.

The "function takes a writer/stream parameter" pattern is the same idea in every language: Java just chose PrintStream as the parameter type.

Flush and close discipline for output streams

Student can state the output guarantee for PrintStream and PrintWriter (bytes are guaranteed on disk only after flush() or close()), name close() and flush() as the two calls that provide the guarantee, and explain why a program that forgets close() can end with an empty or partial output file.

When a Java program writes to a PrintStream or PrintWriter on a file, nothing guarantees the writes reach the disk immediately. Java (and the underlying OS) may buffer output to amortize the cost of disk writes: many small println calls can accumulate into one large block that is written together. Whether a given stream holds output back is a class and version detail; the guarantee that the bytes are on disk comes only from a flush, and close() and flush() are the two calls that provide it.

final PrintStream out = new PrintStream(new File("results.txt"));
out.println("first line");
out.println("second line");
out.println("third line");
//  At this moment nothing guarantees the three lines are on disk.
//  Measured on JDK 25 (2026-08-12): this PrintStream had already
//  written them through; a PrintWriter held 0 bytes until flush().

out.close();   //  flushes the buffer, then closes the file.
//  Now "results.txt" has the three lines.

Two operations guarantee the buffer's content reaches the disk:

1. out.close(): flushes the buffer, then releases the underlying OS file descriptor. After close, the stream is unusable; further writes throw or silently fail. This is the normal way to finalize output. 2. out.flush(): pushes the current buffer content to disk without closing the stream. Use when output must be visible to other processes (or to the user) before the program ends, but the stream should remain open for more writes.

For one-shot programs that write a file and exit, close() is the right call and flush() is unnecessary. For long-running programs that progressively append (logs, status reports), flush() after each append makes the data visible to readers; close() happens once at shutdown.

Forgetting close() risks an empty or partial file

This is the single most common file-output bug in CSCD 210:

final PrintStream out = new PrintStream(new File("results.txt"));
out.println("hello");
//  program exits: no close()
//  "results.txt" was created (and truncated if it existed);
//  no guarantee "hello" is in it

Whether "hello" is on disk is unspecified. Measured on the course JDK (25, 2026-08-12): a PrintStream on a File had its bytes on disk with no close() at all, while a PrintWriter in the same experiment held 0 bytes until flush(). A program that skips close() gets whichever behavior its stream class and JDK happen to give, and the failure case shows up on someone else's machine as an empty or partial file, with no exception and no error message. The rule that holds everywhere: the bytes are guaranteed only after close() or flush(). Skipping close() also keeps the OS file handle held until the process exits.

Auto-flush variants

PrintStream and PrintWriter both have a constructor parameter for auto-flush:

final PrintStream out = new PrintStream(new FileOutputStream("results.txt"), true);  // autoFlush=true

With auto-flush on, every println (and printf) triggers a flush. This makes the file always up-to-date but costs performance: every write goes to disk. CSCD 210 labs default to auto-flush off and call close() at the end. The standard streams System.out and System.err are auto-flushed on terminal output for ergonomic reasons (so the user sees the program's progress).

The modern alternative: try-with-resources

The cleanest way to guarantee close() is the try-with-resources statement:

try (PrintStream out = new PrintStream(new File("results.txt"))) {
    out.println("first line");
    out.println("second line");
}   //  close() is called automatically, even if an exception is thrown above.

The compiler generates a finally block that calls close() on every path out of the try. No manual call needed; no way to forget it.

In other languages

  • C: fclose(fp) is the analog. Same buffering model; fflush(fp) is the explicit-flush call.
  • Python: f.close() is the explicit call; with open(...) as f: is the try-with-resources equivalent.
  • JavaScript (Node.js): fs.WriteStream has .end() and .close(); fs.writeFileSync writes synchronously without buffering.