Concepts / Understanding I/O Streams, Readers, and Writers in Java

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.

  • Programming

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.

  • A new object that the PrintWriter owns

  • The Writer that was passed into the PrintWriter's constructor

Reveal answer
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

Definition

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.

Your programholds and calls thePrintWriterPrintWriterouter wrapper withconvenient methodsWriterinner object passed to theconstructorcalls methods ondirects output to
Which object contains which, and how do the outer wrapper and the inner wrapped stream relate when one stream is wrapped inside another?

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.

Key point

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).

java
Notes on the code
  • charSink is the existing Writer being wrapped
  • new PrintWriter(charSink) creates the wrapper and connects it to charSink
  • out is the variable you call methods on
newcreates a new objectPrintWriterthe wrapper classcharSinkexisting Writer beingwrapped
In the expression new PrintWriter(someWriter), which part creates the wrapper, which part is the existing stream being wrapped, and how do they connect?

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.

print() on outcall made on thePrintWriterPrintWriter methodhandles the datacharSink Writerthe Writer passed to theconstructordelivers to methodhands work to wrapped Writer
When print() is called on a PrintWriter that wraps a Writer, which objects does the data pass through in order, and at which step does the output stop matching what you expected?
Worked example

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?

  1. 1
    Identify the call

    The call is made on out, which is the PrintWriter variable, not on charSink.

  2. 2
    Pass through the PrintWriter

    The PrintWriter's print method handles the data and provides the convenient behavior for the value being printed.

  3. 3
    Reach the wrapped Writer

    The PrintWriter directs the output to charSink, the Writer it was constructed from. This is the object the characters are sent to.

  4. 4
    Check 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?

Result

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 fails

    The wrapped Writer is the object the PrintWriter is constructed from and outputs to. The characters are sent to that Writer.

    Do this instead

    Think 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 like

    charSink.print("Hello") when the convenient methods were meant to come from out

    Why it fails

    The convenient methods belong to the wrapper. Calling the inner object skips the layer you built.

    Do this instead

    Confirm that the call is made on the PrintWriter variable, such as out.

  • Common mistake

    Passing the wrong Writer to the constructor

    Why it fails

    The Writer you pass in is the object your output ultimately goes to, so a wrong choice sends output to the wrong destination.

    Do this instead

    Confirm which Writer was passed to the constructor before tracing anything else.

  • Common mistake

    Calling a method before the PrintWriter has been constructed

    Why it fails

    A call can only be made on a PrintWriter that already exists, so the trace has no wrapper to follow yet.

    Do this instead

    Confirm 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.

  1. 1
    Confirm which Writer was passed to the constructor.
  2. 2
    Confirm that the call was made on the PrintWriter variable and not on the wrapped Writer.
  3. 3
    Confirm that the PrintWriter was constructed before the call was made.
  4. 4
    The first step where the expected object and the actual object differ is where the output diverges.
EASY

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 PrintWriter hands 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.
  • PrintWriter is 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 PrintWriter and pass the Writer to the constructor, as in new PrintWriter(charSink).
  • When you call a PrintWriter method, the wrapped Writer is the object the characters are sent to, not a separate object owned by the PrintWriter.
  • 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.