这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。
装饰器模式
装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream
(对应输入流)和FilterOutputStream
(对应输出流)是装饰器模式的核心,分别用于增强 InputStream
和OutputStream
子类对象的功能。
我们常见的BufferedInputStream
(字节缓冲输入流)、DataInputStream
等等都是FilterInputStream
的子类,BufferedOutputStream
(字节缓冲输出流)、DataOutputStream
等等都是FilterOutputStream
的子类。
举个例子,我们可以通过 BufferedInputStream
(字节缓冲输入流)来增强 FileInputStream
的功能。
BufferedInputStream
构造函数如下:
1 | public BufferedInputStream(InputStream in) { |
可以看出,BufferedInputStream
的构造函数其中的一个参数就是 InputStream
。
BufferedInputStream
代码示例:
1 | try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) { |
这个时候,你可以会想了:为啥我们直接不弄一个BufferedFileInputStream
(字符缓冲文件输入流)呢?
1 | BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt"); |
如果 InputStream
的子类比较少的话,这样做是没问题的。不过, InputStream
的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。
如果你对 IO 流比较熟悉的话,你会发现ZipInputStream
和ZipOutputStream
还可以分别增强 BufferedInputStream
和 BufferedOutputStream
的能力。
1 | BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName)); |
ZipInputStream
和ZipOutputStream
分别继承自InflaterInputStream
和DeflaterOutputStream
。
1 | public |
这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。
为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 InputStream
和OutputStream
。
对于字符流来说,BufferedReader
可以用来增加 Reader
(字符输入流)子类的功能,BufferedWriter
可以用来增加 Writer
(字符输出流)子类的功能。
1 | BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); |
IO 流中的装饰器模式应用的例子实在是太多了,不需要特意记忆,完全没必要哈!搞清了装饰器模式的核心之后,你在使用的时候自然就会知道哪些地方运用到了装饰器模式。
适配器模式
适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
InputStreamReader
和 OutputStreamWriter
就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。InputStreamReader
使用 StreamDecoder
(流解码器)对字节进行解码,实现字节流到字符流的转换, OutputStreamWriter
使用StreamEncoder
(流编码器)对字符进行编码,实现字符流到字节流的转换。
InputStream
和 OutputStream
的子类是被适配者, InputStreamReader
和 OutputStreamWriter
是适配器。
1 | // InputStreamReader 是适配器,FileInputStream 是被适配的类 |
java.io.InputStreamReader
部分源码:
1 | public class InputStreamReader extends Reader { |
java.io.OutputStreamWriter
部分源码:
1 | public class OutputStreamWriter extends Writer { |
适配器模式和装饰器模式有什么区别呢?
装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。
适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 StreamDecoder
(流解码器)和StreamEncoder
(流编码器)就是分别基于 InputStream
和 OutputStream
来获取 FileChannel
对象并调用对应的 read
方法和 write
方法进行字节数据的读取和写入。
1 | StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { |
适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
另外,FutureTask
类使用了适配器模式,Executors
的内部类 RunnableAdapter
实现属于适配器,用于将 Runnable
适配成 Callable
。
FutureTask
参数包含 Runnable
的一个构造方法:
1 | public FutureTask(Runnable runnable, V result) { |
Executors
中对应的方法和适配器:
1 | // 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法 |
工厂模式
工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 Files
类的 newInputStream
方法用于创建 InputStream
对象(静态工厂)、 Paths
类的 get
方法创建 Path
对象(静态工厂)、ZipFileSystem
类(sun.nio
包下的类,属于 java.nio
相关的一些内部实现)的 getPath
的方法创建 Path
对象(简单工厂)。
1 | InputStream is = Files.newInputStream(Paths.get(generatorLogoPath)) |
观察者模式
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 WatchService
接口和 Watchable
接口。WatchService
属于观察者,Watchable
属于被观察者。
Watchable
接口定义了一个用于将对象注册到 WatchService
(监控服务) 并绑定监听事件的方法 register
。
1 | public interface Path |
WatchService
用于监听文件目录的变化,同一个 WatchService
对象能够监听多个文件目录。
1 | // 创建 WatchService 对象 |
Path
类 register
方法的第二个参数 events
(需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。
1 | WatchKey register(WatchService watcher, |
常用的监听事件有 3 种:
StandardWatchEventKinds.ENTRY_CREATE
:文件创建。StandardWatchEventKinds.ENTRY_DELETE
: 文件删除。StandardWatchEventKinds.ENTRY_MODIFY
: 文件修改。
register
方法返回 WatchKey
对象,通过WatchKey
对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。
1 | WatchKey key; |
WatchService
内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化,简化后的源码如下所示。
1 | class PollingWatchService |
参考
- Patterns in Java APIs:http://cecs.wright.edu/~tkprasad/courses/ceg860/paper/node26.html
- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式:https://time.geekbang.org/column/article/204845
- sun.nio 包是什么,是 java 代码么? - RednaxelaFX https://www.zhihu.com/question/29237781/answer/43653953