Appearance
Filter Pattern
The Java IO standard library provides various InputStream
types based on their source, including:
FileInputStream
: Reads data from a file, which is the ultimate data source.ServletInputStream
: Reads data from an HTTP request, which is the ultimate data source.Socket.getInputStream()
: Reads data from a TCP connection, which is the ultimate data source.- ...
If we want to add buffering functionality to FileInputStream
, we can create a derived class:
java
class BufferedFileInputStream extends FileInputStream {}
If we want to add a feature to calculate a signature, we can similarly create another derived class:
java
class DigestFileInputStream extends FileInputStream {}
If we want to add encryption/decryption functionality, we can create yet another derived class:
java
class CipherFileInputStream extends FileInputStream {}
If we want to add both buffering and signature capabilities to FileInputStream
, we would then need to derive a BufferedDigestFileInputStream
. To add buffering and encryption/decryption functionalities, we would derive a BufferedCipherFileInputStream
.
From this, we see that to add three different functionalities to FileInputStream
, we need at least three subclasses. The combinations of these three functionalities will require even more subclasses:
┌─────────────────┐
│ FileInputStream │
└─────────────────┘
▲
┌───────────┬─────────┼─────────┬───────────┐
│ │ │ │ │
┌───────────────────────┐│┌─────────────────┐│┌─────────────────────┐
│BufferedFileInputStream│││DigestInputStream│││CipherFileInputStream│
└───────────────────────┘│└─────────────────┘│└─────────────────────┘
│ │
┌─────────────────────────────┐ ┌─────────────────────────────┐
│BufferedDigestFileInputStream│ │BufferedCipherFileInputStream│
└─────────────────────────────┘ └─────────────────────────────┘
This design approach focused solely on FileInputStream
will quickly lead to an explosion of subclasses if we apply it to another type of InputStream
.
Thus, directly using inheritance to add more functionalities to various InputStream
s can quickly lead to an unmanageable complexity in the code.
To address the issue of subclass explosion caused by inheritance, the JDK first divides InputStream
into two main categories:
- Basic
InputStream
s that directly provide data, such as:
FileInputStream
ByteArrayInputStream
ServletInputStream
- ...
InputStream
s that provide additional functionalities, such as:
BufferedInputStream
DigestInputStream
CipherInputStream
- ...
When we need to add various functionalities to a "basic" InputStream
, we first identify the source of the data. For example, if we want to read from a file using FileInputStream
:
java
InputStream file = new FileInputStream("test.gz");
Next, if we want to enable buffering to improve read efficiency, we wrap the FileInputStream
with a BufferedInputStream
:
java
InputStream buffered = new BufferedInputStream(file);
Finally, if the file is compressed with gzip and we want to read the decompressed content directly, we can wrap it again with a GZIPInputStream
:
java
InputStream gzip = new GZIPInputStream(buffered);
No matter how many times we wrap the streams, the resulting object is always an InputStream
, allowing us to reference it as such and read normally:
┌─────────────────────────┐
│GZIPInputStream │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││ FileInputStream │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘
The approach of layering various "additional" functionality components on top of a "basic" component is known as the Filter Pattern (or Decorator Pattern). This pattern allows us to achieve a variety of functionality combinations with a minimal number of classes:
┌─────────────┐
│ InputStream │
└─────────────┘
▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│ FileInputStream │─┤ └─│FilterInputStream│
└────────────────────┘ │ └─────────────────┘
┌────────────────────┐ │ ▲ ┌───────────────────┐
│ByteArrayInputStream│─┤ ├─│BufferedInputStream│
└────────────────────┘ │ │ └───────────────────┘
┌────────────────────┐ │ │ ┌───────────────────┐
│ ServletInputStream │─┘ ├─│ DataInputStream │
└────────────────────┘ │ └───────────────────┘
│ ┌───────────────────┐
└─│CheckedInputStream │
└───────────────────┘
Similarly, OutputStream
also follows this pattern to provide various functionalities:
┌─────────────┐
│OutputStream │
└─────────────┘
▲ ▲
┌─────────────────────┐ │ │ ┌──────────────────┐
│ FileOutputStream │─┤ └─│FilterOutputStream│
└─────────────────────┘ │ └──────────────────┘
┌─────────────────────┐ │ ▲ ┌────────────────────┐
│ByteArrayOutputStream│─┤ ├─│BufferedOutputStream│
└─────────────────────┘ │ │ └────────────────────┘
┌─────────────────────┐ │ │ ┌────────────────────┐
│ ServletOutputStream │─┘ ├─│ DataOutputStream │
└─────────────────────┘ │ └────────────────────┘
│ ┌────────────────────┐
└─│CheckedOutputStream │
└────────────────────┘
Implementing FilterInputStream
We can also implement our own FilterInputStream
to allow our custom FilterInputStream
to be layered onto any InputStream
.
The following example demonstrates how to create a CountInputStream
, which counts the bytes being read:
java
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes("UTF-8");
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}
class CountInputStream extends FilterInputStream {
private int count = 0;
CountInputStream(InputStream in) {
super(in);
}
public int getBytesRead() {
return this.count;
}
public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count++;
}
return n;
}
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n != -1) {
this.count += n;
}
return n;
}
}
Notice that when layering multiple FilterInputStream
s, we only need to hold the outermost InputStream
. When the outermost InputStream
is closed (at the end of the try(resource)
block), the close()
methods of the inner InputStream
s will also be automatically called, eventually reaching the core "basic" InputStream
. This ensures that there is no resource leakage.
Summary
The Java IO standard library uses the Filter Pattern to enhance InputStream
and OutputStream
functionalities:
- You can combine an
InputStream
with any number ofFilterInputStream
s. - You can combine an
OutputStream
with any number ofFilterOutputStream
s.
The Filter Pattern allows for dynamically adding functionality at runtime (also known as the Decorator Pattern).