Skip to content
On this page

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 InputStreams 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:

  1. Basic InputStreams that directly provide data, such as:
  • FileInputStream
  • ByteArrayInputStream
  • ServletInputStream
  • ...
  1. InputStreams 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 FilterInputStreams, 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 InputStreams 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 of FilterInputStreams.
  • You can combine an OutputStream with any number of FilterOutputStreams.

The Filter Pattern allows for dynamically adding functionality at runtime (also known as the Decorator Pattern).

Filter Pattern has loaded