如果大家觉得文章有错误内容,欢迎留言或者私信讨论~
Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读取和写入。如果对 Java IO类做一下分类,我们可以从下面两个维度将它划分为四类。具体如下所示:
针对不同的读写和写入常见, Java IO 又在四个父类基础只是,扩展了很多子类,具体如下所示:
在我最初接触 java IO 的时候,就对 Java IO 的一些用法产生了很大的疑惑,比如要实现这样一个功能:打开文件 test.txt,从中读取数据。其中InputStream
是一个抽象类,FileInputStream
是专门用来读取文件流的子类。BufferedInputStream
是一个支持带缓存功能的数据读取类,可以提高数据读取的效率,代码看起来是这样:
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,需要先创建一个 FileInputStream
while (bin.read(data) != -1) {
//...
}
这最开始让我非常疑惑,为什么 Java IO 要做这么麻烦的操作,难道不能设计一个继承 FileInputStream
并且支持缓存的 BufferedFileInputStream
的类呢?这样我们就可以像下面的代码一样,直接创建一个 BufferedFileInputStream
类对象,使用起来不是更加简单吗?
InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
这样基于继承的解决方案, 如果 InputStream
只有一个子类FileInputStream
的话,那么我们在它的继承上再设计一个子孙类,那么也还是可以接受的。但实际上,继承InputStream
的子类有很多,那么我们需要给这些子类都派生支持缓存读取的类。
除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的DataInputStream
类,支持按照基本数据类型(int、boolean、long 等)来读取数据:
FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();
在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出DataFileInputStream
、DataPipedInputStream
等类,这样慢慢的开发下去,一旦出现“破窗效应”,那就会组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
装饰器模式的思想是组合大于继承, 使用组合来替代继承。针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。不过,我对代码做了简化,只抽象出了必要的代码结构,如果你感兴趣的话,可以直接去查看 JDK 源码:
public abstract class InputStream {
//...
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
//...
}
public long skip(long n) throws IOException {
//...
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
public class BufferedInputStream extends InputStream {
protected volatile InputStream in;
protected BufferedInputStream(InputStream in) {
this.in = in;
}
//...实现基于缓存的读数据接口...
}
public class DataInputStream extends InputStream {
protected volatile InputStream in;
protected DataInputStream(InputStream in) {
this.in = in;
}
//...实现读取基本类型数据的接口
}
从上面的 Java io 设计来看,装饰器模式对于简单的组合关系,还有两个比较特殊的地方。第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。 比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据:
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。 相较于代理模式,装饰器模式的用途在于附加跟原始类相关的增强功能;而代理类模式则是附加跟原始类无关的功能。清晰两者的区分就能把握住如何去运用两种设计模式。