The Decorator Pattern in Java I/O

Naveen AnamaneniUpdated · 5 min read

In java.io, you rarely use one stream class alone. You write things like new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"), UTF_8)): a stream wrapped inside another, wrapped inside another. Each layer adds one feature: reading from a file, turning bytes into characters, buffering, reading lines. This design is the decorator pattern (see "Structural Design Patterns"): objects with the same interface are wrapped around each other, and each adds behavior before passing calls on. In this lesson, you will see how the pattern is built into java.io and write a decorator of your own.

The Two Kinds of Stream Classes

KindRoleExamples
Concrete streams (the core)Connect to a real source or destinationFileInputStream, ByteArrayInputStream, StringReader
Decorators (wrappers)Take another stream and add a featureBufferedInputStream, DataInputStream, InputStreamReader, BufferedReader, PrintWriter

Every decorator is an InputStream (or Reader, and so on) and has an InputStream inside. That's why you can stack them in any combination.

Each wrapper is a stream that holds a stream and adds one feature

Why a Decorator Instead of Subclasses?

With 4 sources (file, array, socket, pipe) and 4 features (buffering, data types, line reading, pushing back), subclasses would need a class for every combination: BufferedDataFileInputStream, BufferedDataArrayInputStream and so on, 64 classes and more. With decorators, there are only 8 classes, combined freely at runtime.

Example: Building Up a Stack of Decorators

Let's see an example that adds features one layer at a time.

Layers.java▶ Try it
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
 
public class Layers {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream raw = new ByteArrayOutputStream();
        try (DataOutputStream out = new DataOutputStream(raw)) {
            out.writeUTF("Navika");
            out.writeInt(91);
        }
 
        InputStream core = new ByteArrayInputStream(raw.toByteArray());
        InputStream buffered = new BufferedInputStream(core);
        DataInputStream data = new DataInputStream(buffered);
 
        System.out.println(data.readUTF() + " " + data.readInt());
        System.out.println(data instanceof InputStream);
        System.out.println(buffered instanceof InputStream);
    }
}
Output
Navika 91
true
true

Explanation: The core stream reads bytes from an array. BufferedInputStream wraps it to add buffering, and DataInputStream wraps that to add readUTF and readInt. Each layer is itself an InputStream, so it can be wrapped again or passed anywhere an InputStream is expected.

Example: Writing Your Own Decorator

FilterInputStream and FilterReader are base classes for decorators: they pass every call to the wrapped stream, and you override only what you want to change.

UpperCaseReader.java▶ Try it
import java.io.BufferedReader;
import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
 
public class UpperCaseReader extends FilterReader {
    public UpperCaseReader(Reader in) {
        super(in);
    }
 
    @Override
    public int read() throws IOException {
        int c = super.read();
        return c == -1 ? -1 : Character.toUpperCase(c);
    }
 
    @Override
    public int read(char[] buf, int off, int len) throws IOException {
        int n = super.read(buf, off, len);
        for (int i = off; i < off + Math.max(n, 0); i++) {
            buf[i] = Character.toUpperCase(buf[i]);
        }
        return n;
    }
 
    public static void main(String[] args) throws IOException {
        try (BufferedReader r = new BufferedReader(
                new UpperCaseReader(new StringReader("navika\nsrihitha")))) {
            r.lines().forEach(System.out::println);
        }
    }
}
Output
NAVIKA
SRIHITHA

Explanation: UpperCaseReader extends FilterReader, so it is a Reader that holds a Reader. It overrides both read methods to convert characters to upper case after reading them from the wrapped reader. Because it's a normal Reader, it slots between a StringReader and a BufferedReader with no other changes.

Common Mistakes

Closing the inner stream and still using the outer one. Close only the outermost stream; it closes the rest.

Wrapping in the wrong order, such as a BufferedReader around an InputStream. Bridge with InputStreamReader first.

Overriding only read() in a custom filter. Buffered callers use read(char[], int, int), so override both.

Interview Corner

How is the decorator pattern used in java.io?

java.io separates concrete streams that connect to a source or sink, such as FileInputStream, from decorator streams that wrap another stream of the same base type and add behavior, such as BufferedInputStream, DataInputStream and InputStreamReader. Because each decorator both is and holds an InputStream (or Reader, and so on), features can be combined at runtime by nesting constructors, avoiding a class for every combination. FilterInputStream, FilterOutputStream, FilterReader and FilterWriter are base classes for writing new decorators.

Practice

  1. Easy: Stack PrintWriter, BufferedWriter and FileWriter to write three lines to a file, and explain what each layer adds.
  2. Harder: Write a CountingInputStream that extends FilterInputStream and counts the bytes read through it. Wrap a FileInputStream with it, read a file, and print the count.

Quick check: Decorator pattern in java.io

4 questions
1What does a decorator stream hold inside?
2Which class is a concrete stream, not a decorator?
3Why does java.io use decorators instead of subclasses for features?
4Which base class helps you write a Reader decorator?

Key Takeaways

Was this tutorial helpful?

Lesson 351 of 482