The Decorator Pattern in Java I/O
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
| Kind | Role | Examples |
|---|---|---|
| Concrete streams (the core) | Connect to a real source or destination | FileInputStream, ByteArrayInputStream, StringReader |
| Decorators (wrappers) | Take another stream and add a feature | BufferedInputStream, 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.
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.
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);
}
}Navika 91
true
trueExplanation: 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.
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);
}
}
}NAVIKA
SRIHITHAExplanation: 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
- Easy: Stack
PrintWriter,BufferedWriterandFileWriterto write three lines to a file, and explain what each layer adds. - Harder: Write a
CountingInputStreamthat extendsFilterInputStreamand counts the bytes read through it. Wrap aFileInputStreamwith it, read a file, and print the count.
Key Takeaways
Was this tutorial helpful?