Understanding I/O Streams, Readers, and Writers in Java
Java's I/O package lets you add capabilities to a stream by wrapping it in another stream object, and the wrapper is still a stream.
A Question Before the Definition
This article starts with a question rather than a definition. Imagine a short program that creates a Writer and then places a PrintWriter around it. As you follow the program, keep track of two things: which object receives each character output call, and which object the PrintWriter's methods hand their work to. The rest of the article explains the answers to those two questions.
What do you think happens?
When you call a PrintWriter method, does the text appear in a new object that the PrintWriter owns, or does it go to the Writer you handed in? Write down your guess before reading on.
Reveal answer
The Writer that was passed into the PrintWriter's constructor.
The wrapped Writer is the object the PrintWriter is constructed from and outputs to. The PrintWriter provides convenient methods, but the characters are sent on to the Writer you handed in.
Wrapping a Stream Inside Another Stream
Java's I/O package lets you add capabilities to a stream by wrapping it in another stream object. The wrapper is itself a stream, so you still read from it or write to it. What changes is the set of operations available: the wrapper offers fancier operations than a basic stream does.
The key idea is that wrapping does not replace the original stream. Instead, a new object is built around it. The inner object is the stream you already have, and the outer object is the wrapper you construct. Your code then talks to the outer object, which offers the extra operations, while the inner object remains the one that the characters are finally sent to.
The diagram shows the relationship as a chain. Your program holds the PrintWriter, the PrintWriter directs output to the Writer it wraps, and the wrapper supplies the convenient methods. Keeping these two roles separate is what lets you answer the trace question correctly.
What PrintWriter Adds to a Writer
PrintWriter is a subclass of Writer that provides convenient methods for outputting human-readable character representations of Java's basic data types. The key phrase is subclass of Writer. Because PrintWriter extends Writer, it belongs to the same family of character output classes. Its distinctive contribution is its set of methods for writing basic data types in a readable form.
This means that if you already have a Writer, or an object of any of its subclasses, and you want to use PrintWriter's methods to send data to it, you do not rebuild the Writer. You wrap it. The existing Writer keeps its role as the destination, and the PrintWriter adds the methods you want.
A PrintWriter does not replace the Writer it wraps. It is built from that Writer, calls on the PrintWriter are handled by it, and the characters are delivered to the wrapped Writer.
Writing the Wrapping Constructor
Wrapping is done by constructing a new PrintWriter and passing the existing Writer into the constructor as its input. The source gives the wrapping step as a single constructor call, which you can read from left to right, one piece at a time: new PrintWriter(charSink).
charSinkis the existing Writer being wrapped- new
PrintWriter(charSink)creates the wrapper and connects it tocharSink - out is the variable you call methods on
The source states the assumption directly: if charSink is of type Writer, then this line wraps it. The variable names are the source's own, and the same pattern holds regardless of the name you choose. What matters is the position of the Writer inside the parentheses: it is the object the output ultimately goes to.
Tracing a Character Output Call
Now follow a single output call through the pair. Read the wrapping as a path that output follows. A call is made on the PrintWriter, the PrintWriter's method handles the data, and the result is delivered to the Writer that was passed to the constructor. The example below uses the same objects as the code shown earlier. It is generated for this article to show the shape of the code, so the focus is on which object is responsible at each step rather than on any verified program output.
Tracing where a print call goes
Given the wrapping code above, where do the characters from out.print("Hello") go, and which object is responsible at each step?
- 1Identify the call
The call is made on out, which is the
PrintWritervariable, not oncharSink. - 2Pass through the PrintWriter
The
PrintWriter's print method handles the data and provides the convenient behavior for the value being printed. - 3Reach the wrapped Writer
The
PrintWriterdirects the output tocharSink, the Writer it was constructed from. This is the object the characters are sent to. - 4Check for divergence
If the text does not appear as expected, compare each step: was the right Writer passed in, was the call made on out, and was out constructed before the call ran?
The characters are handed from the PrintWriter to the Writer passed into its constructor. The Writer you handed in is the destination, not a copy owned by the PrintWriter.
Mistakes That Make Output Diverge
Most errors with PrintWriter come from losing track of which object is which. The items below describe common confusions, each with a correction tied to the source.
Common mistake Believing the PrintWriter owns a separate copy of the text
Why it failsThe wrapped Writer is the object the
PrintWriteris constructed from and outputs to. The characters are sent to that Writer.Do this insteadThink of the PrintWriter as the front you call methods on, with the Writer you passed in as the destination.
Common mistake Calling methods on the wrapped Writer instead of the PrintWriter variable
What it looks likecharSink.print("Hello") when the convenient methods were meant to come from out
Why it failsThe convenient methods belong to the wrapper. Calling the inner object skips the layer you built.
Do this insteadConfirm that the call is made on the PrintWriter variable, such as out.
Common mistake Passing the wrong Writer to the constructor
Why it failsThe Writer you pass in is the object your output ultimately goes to, so a wrong choice sends output to the wrong destination.
Do this insteadConfirm which Writer was passed to the constructor before tracing anything else.
Common mistake Calling a method before the PrintWriter has been constructed
Why it failsA call can only be made on a
PrintWriterthat already exists, so the trace has no wrapper to follow yet.Do this insteadConfirm that the PrintWriter was constructed before the call was made.
Checking the Chain Step by Step
When a program's character output is not what you expected, use the chain from this article as a checklist. Work through the steps in order and stop at the first place where the object you think is responsible differs from the object actually involved. That is where the trace diverges.
- 1Confirm which Writer was passed to the constructor.
- 2Confirm that the call was made on the
PrintWritervariable and not on the wrapped Writer. - 3Confirm that the
PrintWriterwas constructed before the call was made. - 4The first step where the expected object and the actual object differ is where the output diverges.
A program creates a Writer named sink, then writes: PrintWriter out = new PrintWriter(sink); out.println("Total"); Trace where the characters go. Name the object that receives the call, the object that handles the data, and the object that the characters are finally delivered to. Then say which of the three checklist steps you would check first if the text never appeared in sink.
Hints
- The call is made on out, so start there.
- The
PrintWriterhands its work to the Writer it was constructed from. - Sink is the object passed into the constructor, so it is the destination.
Key Takeaways
- Java's I/O package lets you add capabilities to a stream by wrapping it in another stream object, and the wrapper is still a stream.
PrintWriteris a subclass of Writer that provides convenient methods for outputting human-readable character representations of Java's basic data types.- To wrap a Writer, construct a new
PrintWriterand pass the Writer to the constructor, as in newPrintWriter(charSink). - When you call a
PrintWritermethod, the wrapped Writer is the object the characters are sent to, not a separate object owned by thePrintWriter. - When output is unexpected, check which Writer was passed in, whether the call was made on the
PrintWriter, and whether it was constructed first, then find the first step where the expected and actual objects differ.