Java 装饰模式在 IO 中应用
饰模式在保持目标类自身特性的同时,根据需要动态地增加目标类的功能。下面结合 java IO 来了解装饰模式的实现方式。
IO 中最基本的一种是输入流,java 类库中提供了抽象类 InputStream 作为输入流的基类,相关实现类的 UML 图如下所示(图片来源 Java IO 知识整理)。
初看可能会被种类繁多的输入流吓到(实际上,这只是 java IO 中的字节类输入流而已,除此之外还有字节类输出流(OutputStream),字符类输入流(Reader),字符类输出流(Writer)。如果考虑面向缓冲区的 java NIO 那就有更过的类了。),但是类结构是比较简单的,除了抽象基类 InputStream 之外,主要还有两层。
输入流的来源(UML 图 InputStream 下面第一层)
1) FileInputStream,构造方法接受一个文件名或文件对象,形成文件输入流
2) ByteArrayInputStream, 构造方法接受一个 byte 类型的数组,形成字节输入流
3) StringBufferInputStream (@Deprecated, 从 jdk 1.1 开始,应使用 StringReader 将 String 转换为输入流), 构造方法接受一个 String,将其转换为字节输入流
4) PipedInputStream, 构造方法接受一个 PipedOutputStream,将输出流转化为输入流。PipedInputStream 和 PipedOutputStream 用于实现不同线程之间的通信。
5) ObjectInputStream,作用是从输入流中读取 Java 对象和基本数据。只有实现了 Serializable 或 Externalizable 接口的对象才能被ObjectInputStream/ObjectOutputStream 所操作,它们在对象序列化与反序列化中起着非常重要的作用。装饰类(UML 图 InputStream 下面第二层)
java IO 为装饰类定义了基类 FilterInputStream,实现了 InputStream 中定义的方法,方便实际的装饰类继承。
1) DataInputStream, 构造方法接受一个 InputStream,该 InputStream 一般是上述输入流来源类的实例,可以为其增加 readBoolean(), readUTF(), readLine() 等功能。
2) BufferedInputStream,构造方法接受一个 InputStream,该 InputStream 一般是上述输入流来源类的实例,可以在执行 read 操作时,一次将一定字节数的输入流读入缓冲区,避免每次读一个字节都要与非常慢的磁盘进行交互,提高效率。
查看 FileInputStream 和装饰类 DataInputStream 的源代码,我们可以一探装饰模式的实现方式。
public class FileInputStream extends InputStream{
//...
//构造方法,传入的是文件名,新建文件对象后调用另外的构造方法
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
//构造方法,打开文件对象,准备好输入流
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
//...
/**
* Reads up to b.length bytes of data from this input
* stream into an array of bytes. This method blocks until some input
* is available.
*
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or
* -1 if there is no more data because the end of
* the file has been reached.
* @exception IOException if an I/O error occurs.
*/
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
public void close() throws IOException {
//...
}
}
可以看到,FileInputStream 主要是打开文件,引入输入流,而在对输入流的处理上,功能是比较简单的。为了使其功能更加丰富,我们大概有以下几种做法。
1) 直接在类中定义更多的方法。但是不要忘了,除了 FileInputStream,我们还有更多的输入流来源类,这样我们可能需要在每一个输入流来源类中定义更多的方法,而实际上,这些方法所做的操作都是一样的。此外,有一些功能在起始状态下是不能预见的,如果出现了新的功能需求,就要在修改类的话,是非常不合理的。
2) 采用继承。继承可以在不修改已有类的基础上增加新的功能,但需要组合各种各样的功能,很容易造成类爆炸。
使用装饰模式可以很好地解决以上问题。例如装饰类 DataInputStream
public
class DataInputStream extends FilterInputStream implements DataInput {
//装饰一个 InputStream
public DataInputStream(InputStream in) {
super(in);
}
//增加从输入流中读取 boolean 类型数据的功能
public final boolean readBoolean() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return (ch != 0);
}
//增加从输入流中读取 byte 类型数据的功能
public final byte readByte() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return (byte)(ch);
}
//...
//增加从输入流中读取 short 类型数据的功能
public final short readShort() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (short)((ch1 << 8) + (ch2 << 0));
}
//...
public final char readChar() {
//...
}
public final char readInt() {
//...
}
//增加从输入流中读取 UTF-8 字符数据的功能
public final char readUTF() {
//...
}
}
这样,在我们想要从文件中读取数据时,可以使用装饰器,便于读取。需要注意的是,这里文件的内容是以字节流方式读入的,如果包含中文字符则无法正常读取,此时应当使用字符流 BufferedReader 读入,或者使用适配器 InputStreamReader 对将输入字节流转换为字符流后进行读取。
String filename = "."+ File.separator + "input.txt";
DataInputStream in = new DataInputStream(
new FileInputStream(filename));
//也可以对装饰后的类进行再次装饰,以便其拥有更多的功能
//DataInputStream in = new DataInputStream(
// new BufferedInputStream(new FileInputStream(filename)));
while(in.available()!=0)
System.out.print((char) in.readByte());
in.close();
这样就实现了 DataInputStream 对 FileInputStream 的装饰。在实际应用中,用户可以根据需要,用各种各样的装饰器装饰不同的输入流源。
正如之前提到的,java IO 类库除了输入流之外还有字节类输出流(OutputStream),字符类输入流(Reader),字符类输出流(Writer)。InputStream 和 OutputStream 的类结构对称,Reader 和 Writer 类分别作为对 InputStream 和 OutputStream 的升级和补充,在 jdk 1.2 版本中推出。Reader 和 InputStream、Writer 和 OutputStream 又通过 Adapter (适配器模式)联系在一起,这样 java IO 的各个组成元素彼此独立、而又层次分明地结合在了一起。