Java PrintStream
How PrintStream powers System.out and System.err and how to use it for byte-oriented formatted output.
Java PrintStream
PrintStream is the class that's been sitting under your code since chapter 1. System.out is a PrintStream. System.err is a PrintStream. Every System.out.println(...) you have ever written has gone through this class.
It has the same surface as the PrintWriter you just met — print, println, printf, format — and the same swallow-the-exception behaviour. The difference is what it sits on top of: PrintStream extends OutputStream (bytes), while PrintWriter extends Writer (characters). For file output, the byte/character distinction from earlier in this part still applies: characters in, characters out, and encoding lives at the boundary.
Why two classes with the same API?
History. Java 1.0 had PrintStream but no Writer hierarchy at all — every "print" went to a byte stream. Java 1.1 introduced the Reader/Writer hierarchy for proper character handling and added PrintWriter so file-writing code could use the same API on characters. PrintStream couldn't be retired because System.out and System.err were already typed as PrintStream in published APIs, and changing them would have broken every program in the world.
So both exist. The practical rule:
- Use
PrintWriterfor files. The character-oriented hierarchy is where encoding belongs. - Use
PrintStreamwhen you have to — i.e., whenSystem.out/System.erris the target, or when you're writing to anOutputStreamyou don't want to wrap.
The "have to" cases are narrow. Most of the time you can do this:
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8);
out.println("hello");and forget PrintStream exists.
The API
Identical to PrintWriter:
void print(boolean | char | int | long | float | double | String | Object);
void println(...); // adds the platform line separator
PrintStream printf(String format, Object... args);
PrintStream format(String format, Object... args);
PrintStream append(CharSequence s);Plus the inherited OutputStream methods (write(int), write(byte[]), flush, close). The same trap from BufferedWriter and PrintWriter applies: println writes System.lineSeparator(), which is \r\n on Windows. Write \n explicitly when the output needs to be portable.
Constructors
new PrintStream(OutputStream out); // platform default charset
new PrintStream(OutputStream out, boolean autoFlush, Charset cs); // explicit charset
new PrintStream(File file, Charset charset); // open a file
new PrintStream(String filename, Charset charset);As with PrintWriter, the no-charset constructors fall back to the JVM default encoding — the same portability hazard described in the character-streams chapter. Always pass a charset.
The autoFlush flag has the same semantics as PrintWriter: when on, println, printf, format, and write(byte[], int, int) on a newline trigger a flush. print does not. Off by default.
The swallowed IOException (still)
Same design as PrintWriter. None of the print/println/printf methods throw IOException. A failed write sets an error flag you read with checkError(). The trade-off is the same: convenient for casual code, dangerous if you don't check.
For System.out/System.err specifically, the swallow is the right call — there's nothing useful to do when a write to the terminal fails. For a file-backed PrintStream, prefer PrintWriter, or check checkError() before close.
System.out and System.err
These two are PrintStream instances created during JVM startup. They wrap the OS's stdout and stderr file descriptors. Their character encoding follows stdout.encoding (Java 18+) or file.encoding (older), which is why pipe-redirected output sometimes mojibakes on a Windows console — the console's code page doesn't match the JVM's idea of the encoding.
You can replace them with System.setOut(PrintStream) and System.setErr(PrintStream), which is occasionally useful for capturing output in tests:
ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream original = System.out;
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
try {
runTheCodeUnderTest();
assertEquals("expected\n", captured.toString(StandardCharsets.UTF_8));
} finally {
System.setOut(original);
}For production code, leave them alone. Logging frameworks (java.util.logging, SLF4J/Logback) take a different, structured approach to writing diagnostic output.
print(Object) and null
A subtle behaviour shared with PrintWriter: print(Object o) calls String.valueOf(o), which returns the four-character string "null" for a null reference rather than throwing NullPointerException. That's the reason
System.out.println(maybeNullList); // prints "null", not NPEworks. Convenient for casual logging; misleading if you're writing the string back into a data file you'll re-parse later — "null" as a string is indistinguishable from the literal word "null."
write(int) writes a byte, not a character
PrintStream is an OutputStream. The inherited write(int b) writes the low-order byte:
System.out.write(65); // writes 'A' — the byte 0x41
System.out.write('é'); // writes a single byte 0xE9 — NOT UTF-8 for 'é'The second line is wrong on a UTF-8 terminal — 'é' is two bytes in UTF-8 (0xC3 0xA9), and you wrote one. Don't use write(int) on a PrintStream for characters; use print/println, which go through the configured charset.
A worked example: System.out redirected and inspected
The program below captures System.out into a ByteArrayOutputStream so you can see exactly what bytes the JVM emits when you call println. It runs the same println("Café") with two different charsets to make the encoding behaviour concrete, demonstrates checkError() on a failing stream, and finally shows the difference between print(Object) for a null reference and a deliberate null check.
What to take from the run:
System.setOut(new PrintStream(buffer, ...))captured what would otherwise have gone to the console. Tests use this pattern all the time. Restore the original before you print your report — otherwise the report goes into the buffer too, and confusion follows.- The "Café" line emitted 5 bytes in UTF-8 (
43 61 66 C3 A9) and 4 bytes in ISO-8859-1 (43 61 66 E9). Same input, different byte widths, both correct — encoding is the byte → character mapping, andPrintStreamhonours the charset you gave its constructor. The default-charset constructor would pick whichever of these the JVM happened to be running with. - The broken-stream block proved the swallow:
printlnreturned normally, the underlyingIOExceptionvanished, andcheckError()was the only way to find out the write had failed. Same contract asPrintWriter. If you care about the failure, you must ask. - The null-reference print produced the four-character string
null, not aNullPointerException. That's howprintln(someList)works even whensomeListisnull— convenient, but it means you can't tell the literal text "null" apart from a null reference once it's on disk. PickObjects.requireNonNullor an explicit null check at the boundary if that distinction matters. - Nothing in the example called a
PrintWriter. ForSystem.out, you don't need one —PrintStreamis the type Java already gave you, the API is identical, and the autoflush-on-printlnbehaviour is what you want at the terminal.
What's next
The first thirteen chapters of this part have covered every shape of streaming I/O: bytes, characters, buffering, primitives, formatted text. They all transmit content — bytes and chars. The next chapter, Java Serialization, is about transmitting graphs of objects — a whole linked structure of references, written to a stream and reconstructed on the other side, with one annotation on the class.
Practice
`PrintWriter` and `PrintStream` have nearly identical APIs (`print`, `println`, `printf`). When writing text to a file, which should you generally prefer, and why?