The Java IO package (java.io) serves as an excellent illustration of design pattern usage, addressing key challenges such as reading and writing from various data sources and types, extensibility, and resource management. Below is an analysis of core design patterns used within the IO package, with source code examples and practical use cases.
Decorator Pattern
Purpose
Dynamically adds functionality to objects without altering their structure, offering a flexible alternative to inheritance.
Usage in IO
The extension of byte and character streams heavily relies on the decorator pattern:
- Component Interface:
InputStream/OutputStream(byte streams),Reader/Writer(character streams) — define fundamental I/O operations; - Concrete Components:
FileInputStream/FileOutputStream,FileReader/FileWriter— handle direct file access; - Decorator Base Class:
FilterInputStream/FilterOutputStream,FilterReader/FilterWriter— extend components and maintain references to core components; - Concrete Decorators:
BufferedInputStream(buffering),DataInputStream(primitive type reads),InputStreamReader(byte-to-character conversion) — add extra features to base streams.
Example Implementation
import java.io.*;
public class DecoratorExample {
public static void main(String[] args) {
try (InputStream fileStream = new FileInputStream("example.txt");
InputStream bufferedStream = new BufferedInputStream(fileStream)) {
int data;
while ((data = bufferedStream.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Key Aspects
- Decorators and components implement the same interface (e.g.,
InputStream), ensuring a consistent API; - Multiple decorators can be layered (e.g.,
BufferedInputStream+DataInputStream) for flexible combinations; - No modification to existing classes required, adhering to the open/closed principle.
Adapter Pattern
Purpose
Converts one interface into another compatible interface to resolve incompatibilities.
IO Application
Most notably seen in adapting byte streams to character streams:
- Byte streams (
InputStream/OutputStream) process binary data; - Character streams (
Reader/Writer) manage text data using character encoding; - Adapter classes:
InputStreamReader(byte → character),OutputStreamWriter(byte → character).
Source Code Snippet (InputStreamReader)
class InputStreamReader extends Reader {
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
public int read() throws IOException {
return sd.read();
}
}
Practical Example
import java.io.*;
public class AdapterExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt");
Reader reader = new InputStreamReader(fis, "UTF-8")) {
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Factory Pattern
Purpose
Encapsulates object creation logic through a unified interface, reducing coupling.
IO Application
The IO package utilizes simple factories (static factory methods):
FileChannelcreation viaFileInputStream.getChannel()orFileOutputStream.getChannel();Filesutility class (NIO):Files.newBufferedReader(),Files.newBufferedWriter();- Creation of
InputStreamsubclasses likeFileInputStream,ByteArrayInputStreamvia constructors.
Sample Code
import java.io.*;
import java.nio.file.*;
public class FactoryExample {
public static void main(String[] args) {
try (BufferedReader reader = Files.newBufferedReader(Paths.get("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
try (FileInputStream fis = new FileInputStream("example.txt")) {
System.out.println(fis.getChannel().size());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Singleton Pattern
Purpose
Ensures a class has only one instance and provides a global access point.
IO Use Case
Standard streams (stdin, stdout, stderr) are implemented as singletons in FileDescriptor:
public final class FileDescriptor {
public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);
private FileDescriptor(int fd) {
this.fd = fd;
this.handle = new Handle(fd);
}
}
Real-world Usage
System.in and System.out utilize these singleton instances:
System.out.println("Hello IO");
System.in.read();
Template Method Pattern
Purpoce
Defines the skeleton of an algorithm, deferring certain steps to subclasses, preserving the overall structure.
IO Application
Abstract classes like InputStream and Reader define method skeletons, leaving implementation to subclasses:
- Template Class:
InputStreamdefinesread()andread(byte[])algorithms; - Subclasses:
FileInputStreamimplements specific reading logic.
Source Code Example (InputStream)
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
}
// ... validation and loop logic ...
int c = read();
if (c == -1) return -1;
b[off] = (byte) c;
// ... loop to fill buffer ...
return i;
}
}
Subclass Implementation (FileInputStream)
public class FileInputStream extends InputStream {
public int read() throws IOException {
return read0();
}
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
}
Observer Pattern
Purpose
Defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified automatically.
IO Context
Historically, java.io.Observer and Observable were used (now deprecated):
- For example, monitoring file changes using
Observableto notify observers upon modification.
Simplified Illustration
import java.io.*;
import java.util.*;
class FileMonitor extends Observable {
private File file;
private long lastModified;
public FileMonitor(File file) {
this.file = file;
this.lastModified = file.lastModified();
}
public void checkForChanges() {
long current = file.lastModified();
if (current != lastModified) {
lastModified = current;
setChanged();
notifyObservers(file);
}
}
}
class FileChangeListener implements Observer {
@Override
public void update(Observable o, Object arg) {
File file = (File) arg;
System.out.println("File changed: " + file.getName());
}
}
public class ObserverExample {
public static void main(String[] args) throws InterruptedException {
File file = new File("example.txt");
FileMonitor monitor = new FileMonitor(file);
monitor.addObserver(new FileChangeListener());
while (true) {
monitor.checkForChanges();
Thread.sleep(1000);
}
}
}
Summary
- Primary Patterns: The most prevalent patterns in Java IO are the decorator and adapter, which underpin stream extensions and byte/character conversions;
- Supporting Patterns: Factory, template method, and singleton patterns assist in object instantiation, algorithm structuring, and global state handling;
- Design Philosophy: All patterns align with principles of decoupling, reusability, and openness, enabling modular and scalable I/O capabilities.