微信
支付宝
# IO-设计模式 这篇文章我们简单来看看我们从 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 Callable callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); return new RunnableAdapter(task, result); } // 适配器 static final class RunnableAdapter implements Callable { 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, Iterable, 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\> 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: - 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式: - sun.nio 包是什么,是 java 代码么? - RednaxelaFX
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Veylor
最近发布
常用SQL