JavaGuide/docs/java/io/io-design-patterns.md

322 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Java IO 设计模式总结
category: Java
tag:
- Java IO
- Java基础
---
这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。
## 装饰器模式
**装饰器Decorator模式** 可以在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能在一些继承关系比较复杂的场景IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, `FilterInputStream` (对应输入流)和`FilterOutputStream`(对应输出流)是装饰器模式的核心,分别用于增强 `InputStream` 和`OutputStream`子类对象的功能。
我们常见的`BufferedInputStream`(字节缓冲输入流)、`DataInputStream` 等等都是`FilterInputStream` 的子类,`BufferedOutputStream`(字节缓冲输出流)、`DataOutputStream`等等都是`FilterOutputStream`的子类。
举个例子,我们可以通过 `BufferedInputStream`(字节缓冲输入流)来增强 `FileInputStream` 的功能。
`BufferedInputStream` 构造函数如下:
```java
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
```
可以看出,`BufferedInputStream` 的构造函数其中的一个参数就是 `InputStream`
`BufferedInputStream` 代码示例:
```java
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
int content;
long skip = bis.skip(2);
while ((content = bis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
```
这个时候,你可以会想了:**为啥我们直接不弄一个`BufferedFileInputStream`(字符缓冲文件输入流)呢?**
```java
BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt");
```
如果 `InputStream`的子类比较少的话,这样做是没问题的。不过, `InputStream`的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。
如果你对 IO 流比较熟悉的话,你会发现`ZipInputStream` 和`ZipOutputStream` 还可以分别增强 `BufferedInputStream``BufferedOutputStream` 的能力。
```java
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ZipInputStream zis = new ZipInputStream(bis);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
ZipOutputStream zipOut = new ZipOutputStream(bos);
```
`ZipInputStream` 和`ZipOutputStream` 分别继承自`InflaterInputStream` 和`DeflaterOutputStream`。
```java
public
class InflaterInputStream extends FilterInputStream {
}
public
class DeflaterOutputStream extends FilterOutputStream {
}
```
这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。
为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 `InputStream` 和`OutputStream`。
对于字符流来说,`BufferedReader` 可以用来增加 `Reader` (字符输入流)子类的功能,`BufferedWriter` 可以用来增加 `Writer` (字符输出流)子类的功能。
```java
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`是适配器。
```java
// InputStreamReader 是适配器FileInputStream 是被适配的类
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
// BufferedReader 增强 InputStreamReader 的功能(装饰器模式)
BufferedReader bufferedReader = new BufferedReader(isr);
```
`java.io.InputStreamReader` 部分源码:
```java
public class InputStreamReader extends Reader {
//用于解码的对象
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
// 获取 StreamDecoder 对象
sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
// 使用 StreamDecoder 对象做具体的读取工作
public int read() throws IOException {
return sd.read();
}
}
```
`java.io.OutputStreamWriter` 部分源码:
```java
public class OutputStreamWriter extends Writer {
// 用于编码的对象
private final StreamEncoder se;
public OutputStreamWriter(OutputStream out) {
super(out);
try {
// 获取 StreamEncoder 对象
se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
// 使用 StreamEncoder 对象做具体的写入工作
public void write(int c) throws IOException {
se.write(c);
}
}
```
**适配器模式和装饰器模式有什么区别呢?**
**装饰器模式** 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。
**适配器模式** 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 `StreamDecoder` (流解码器)和`StreamEncoder`(流编码器)就是分别基于 `InputStream``OutputStream` 来获取 `FileChannel`对象并调用对应的 `read` 方法和 `write` 方法进行字节数据的读取和写入。
```java
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
// 省略大部分代码
// 根据 InputStream 对象获取 FileChannel 对象
ch = getChannel((FileInputStream)in);
}
```
适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
另外,`FutureTask` 类使用了适配器模式,`Executors` 的内部类 `RunnableAdapter` 实现属于适配器,用于将 `Runnable` 适配成 `Callable`
`FutureTask`参数包含 `Runnable` 的一个构造方法:
```java
public FutureTask(Runnable runnable, V result) {
// 调用 Executors 类的 callable 方法
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
```
`Executors`中对应的方法和适配器:
```java
// 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
// 适配器
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
```
## 工厂模式
工厂模式用于创建对象NIO 中大量用到了工厂模式,比如 `Files` 类的 `newInputStream` 方法用于创建 `InputStream` 对象(静态工厂)、 `Paths` 类的 `get` 方法创建 `Path` 对象(静态工厂)、`ZipFileSystem` 类(`sun.nio`包下的类,属于 `java.nio` 相关的一些内部实现)的 `getPath` 的方法创建 `Path` 对象(简单工厂)。
```java
InputStream is = Files.newInputStream(Paths.get(generatorLogoPath))
```
## 观察者模式
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 `WatchService` 接口和 `Watchable` 接口。`WatchService` 属于观察者,`Watchable` 属于被观察者。
`Watchable` 接口定义了一个用于将对象注册到 `WatchService`(监控服务) 并绑定监听事件的方法 `register`
```java
public interface Path
extends Comparable<Path>, Iterable<Path>, Watchable{
}
public interface Watchable {
WatchKey register(WatchService watcher,
WatchEvent.Kind<?>[] events,
WatchEvent.Modifier... modifiers)
throws IOException;
}
```
`WatchService` 用于监听文件目录的变化,同一个 `WatchService` 对象能够监听多个文件目录。
```java
// 创建 WatchService 对象
WatchService watchService = FileSystems.getDefault().newWatchService();
// 初始化一个被监控文件夹的 Path 类:
Path path = Paths.get("workingDirectory");
// 将这个 path 对象注册到 WatchService监控服务 中去
WatchKey watchKey = path.register(
watchService, StandardWatchEventKinds...);
```
`Path``register` 方法的第二个参数 `events` (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。
```java
WatchKey register(WatchService watcher,
WatchEvent.Kind<?>... events)
throws IOException;
```
常用的监听事件有 3 种:
- `StandardWatchEventKinds.ENTRY_CREATE`:文件创建。
- `StandardWatchEventKinds.ENTRY_DELETE` : 文件删除。
- `StandardWatchEventKinds.ENTRY_MODIFY` : 文件修改。
`register` 方法返回 `WatchKey` 对象,通过`WatchKey` 对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。
```java
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
// 可以调用 WatchEvent 对象的方法做一些事情比如输出事件的具体上下文信息
}
key.reset();
}
```
`WatchService` 内部是通过一个 daemon thread守护线程采用定期轮询的方式来检测文件的变化简化后的源码如下所示。
```java
class PollingWatchService
extends AbstractWatchService
{
// 定义一个 daemon thread守护线程轮询检测文件变化
private final ScheduledExecutorService scheduledExecutor;
PollingWatchService() {
scheduledExecutor = Executors
.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}});
}
void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {
synchronized (this) {
// 更新监听事件
this.events = events;
// 开启定期轮询
Runnable thunk = new Runnable() { public void run() { poll(); }};
this.poller = scheduledExecutor
.scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);
}
}
}
```
## 参考
- 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>
<!-- @include: @article-footer.snippet.md -->