Java IO 设计模式总结
一、装饰器模式
装饰器模式(Decorator Pattern)
装饰器模式 是一种结构型设计模式,旨在允许动态地为对象添加额外的功能,而无需修改其代码。通过装饰器模式,可以在不改变原有对象的情况下,拓展其功能。该模式通常通过继承关系比较复杂的类来增强功能,特别适合 IO 流的扩展场景。
装饰器模式的核心思想是通过组合而非继承的方式,给原始对象添加额外功能。对于 IO 流来说,FilterInputStream
和 FilterOutputStream
就是装饰器模式的核心,分别用于增强 InputStream
和 OutputStream
的功能。
关键类
FilterInputStream
和FilterOutputStream
是装饰器模式的核心类。它们通过继承自InputStream
和OutputStream
,并通过组合方式增强了原始流对象的功能。- 装饰器类常见的有:
BufferedInputStream
(字节缓冲输入流)和BufferedOutputStream
(字节缓冲输出流)DataInputStream
和DataOutputStream
ZipInputStream
和ZipOutputStream
示例:使用 BufferedInputStream
装饰 FileInputStream
BufferedInputStream
是 FilterInputStream
的子类,它通过内部缓存字节数据来提高读取效率。例如,可以使用 BufferedInputStream
来增强 FileInputStream
的功能,从而避免每次读取时都直接与文件进行 I/O 操作。
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
int content;
long skip = bis.skip(2); // 跳过前2个字节
while ((content = bis.read()) != -1) {
System.out.print((char) content); // 输出读取的字符
}
} catch (IOException e) {
e.printStackTrace();
}
为什么不使用 BufferedFileInputStream
?
考虑如果为每个流子类都定制一个新的装饰类,如 BufferedFileInputStream
,这会导致类的膨胀,特别是 InputStream
和 OutputStream
类层次结构非常庞大。如果每个子类都要创建一个新的装饰类,这将显得非常不方便且重复。
装饰器模式的核心优势是通过组合多个装饰器,可以灵活地增强流对象的功能。例如,我们可以将 BufferedInputStream
和 ZipInputStream
组合在一起:
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("file.txt"));
ZipInputStream zis = new ZipInputStream(bis);
在这个例子中,ZipInputStream
和 BufferedInputStream
作为两个装饰器,按顺序装饰了原始的 FileInputStream
对象,增强了其功能。
装饰器模式的灵活性
装饰器模式允许我们根据需要叠加多个装饰器。例如,使用 BufferedWriter
可以在 Writer
对象上添加缓冲功能,而 BufferedWriter
本身可以再包装一个 OutputStreamWriter
来处理字符编码:
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file.txt"), "UTF-8"));
这种方式的好处是,我们可以根据需求叠加更多的功能,而不需要修改原始流类的代码。
总结
装饰器模式是一种灵活的设计模式,能够通过组合多个装饰器来动态地扩展对象的功能,尤其适用于 I/O 流操作。通过这种方式,我们能够在不修改原始类的情况下,动态地给对象添加功能,从而避免了庞大的继承层次结构。
二、适配器模式(Adapter Pattern)
适配器模式 主要解决接口不兼容的问题,使得原本由于接口不同而无法一起工作的类可以协同工作。可以类比为日常使用的电源适配器,它将不同的插头接口转换为统一的格式,使得各种电器都能够使用同一个插座。
在适配器模式中,存在两个关键角色:
- 适配者(Adaptee):原本不兼容的类或者接口,通常需要被适配的功能。
- 适配器(Adapter):用来协调适配者与客户端之间接口差异的类。适配器通过继承(类适配器)或组合(对象适配器)来实现。
在 IO 流的场景中,字符流和字节流具有不同的接口,适配器模式能够使它们之间的协同工作成为可能。具体来说,InputStreamReader
和 OutputStreamWriter
就是实现字节流与字符流转换的适配器。
字节流与字符流的适配
- 字节流:处理原始字节数据的流,操作的是 8 位字节(
InputStream
和OutputStream
)。 - 字符流:处理字符数据的流,操作的是 16 位 Unicode 字符(
Reader
和Writer
)。
由于字节流和字符流的接口不同,它们无法直接互操作。适配器模式的关键在于,通过使用 InputStreamReader
和 OutputStreamWriter
这两个适配器,可以将字节流与字符流连接起来,从而让字节流可以操作字符数据,反之亦然。
适配器模式的实现
1. InputStreamReader
InputStreamReader
是字节流到字符流的适配器,它通过内部使用 StreamDecoder
对字节进行解码,将字节流转换为字符流。
// InputStreamReader 是适配器,FileInputStream 是被适配的类
InputStreamReader isr = new InputStreamReader(new FileInputStream("file.txt"), "UTF-8");
InputStreamReader
的源码实现:
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);
}
}
public int read() throws IOException {
return sd.read();
}
}
InputStreamReader
使用StreamDecoder
对字节流进行解码,将字节转换为字符。StreamDecoder
是实现字节流到字符流转换的核心工具,它负责解码字节流中的数据并将其转换为字符。
2. OutputStreamWriter
OutputStreamWriter
是字符流到字节流的适配器,它通过内部使用 StreamEncoder
对字符进行编码,将字符流转换为字节流。
// OutputStreamWriter 是适配器,FileOutputStream 是被适配的类
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("file.txt"), "UTF-8");
OutputStreamWriter
的源码实现:
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);
}
}
public void write(int c) throws IOException {
se.write(c);
}
}
OutputStreamWriter
使用StreamEncoder
对字符流进行编码,将字符转换为字节。StreamEncoder
是实现字符流到字节流转换的核心工具,它负责将字符转换为字节并写入到字节流中。
字节流与字符流的转换示例
以下是使用 InputStreamReader
和 OutputStreamWriter
将字节流与字符流之间进行转换的实际示例:
// 字节流转字符流
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")) {
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 字符流转字节流
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {
BufferedWriter bw = new BufferedWriter(osw);
bw.write("Hello, World!");
} catch (IOException e) {
e.printStackTrace();
}
小结
适配器模式解决了接口不兼容的问题,在字节流与字符流之间起到了桥梁的作用。InputStreamReader
和 OutputStreamWriter
通过适配器模式实现了字节流与字符流的相互转换,使得它们能够协同工作。通过这种模式,字节流和字符流可以无缝转换,简化了代码的编写,同时保持了系统的灵活性和可扩展性。
三、适配器模式与装饰器模式的区别
虽然适配器模式和装饰器模式在实现上有相似之处(都使用了对象组合或继承来扩展原始类的功能),但它们的目的、使用场景和实现方式有明显不同。以下是这两种模式的主要区别:
1. 目的与关注点
- 装饰器模式:
- 主要关注 增强功能。
- 通过在原有对象的基础上,动态地添加额外的行为或功能来扩展其功能。
- 功能增强 是装饰器模式的核心目标,通常用于某个对象已经有基本功能时,想要添加更多功能,而不修改原有代码。
- 适配器模式:
- 主要关注 接口兼容性。
- 用来解决接口不兼容的问题,将一个类的接口转换成客户端希望的另一个接口,使得原本接口不兼容的类能够一起工作。
- 接口转换 是适配器模式的核心目标,通常用于处理两个原本无法互操作的系统或者类之间的接口不一致问题。
2. 工作方式与使用方式
装饰器模式:
- 装饰器模式是在运行时对对象进行包装,原始对象的功能被装饰器增强。
- 装饰器类通常会 继承相同的接口或者抽象类,然后将增强后的方法委托给原始对象。
- 装饰器模式支持多个装饰器嵌套使用,可以逐步增加对象的功能。
示例:
InputStream in = new FileInputStream("input.txt"); BufferedInputStream bis = new BufferedInputStream(in); // 装饰器 DataInputStream dis = new DataInputStream(bis); // 进一步装饰
适配器模式:
- 适配器模式通过组合或继承的方式,在不同接口之间提供适配层,使得两个不兼容的类能够协同工作。适配器模式并不关心对原始功能的增强,而是聚焦于将接口适配成目标接口。
- 适配器模式通常 不改变原有类的行为,而是通过适配器内部转换接口来进行适配。
示例:
InputStream in = new FileInputStream("input.txt"); Reader reader = new InputStreamReader(in); // 适配器
3. 继承关系
装饰器模式:
- 装饰器类通常需要 继承相同的抽象类或实现相同的接口,通过委托方式增强原始类的功能。
- 装饰器类和被装饰类通常是同一个类层次结构中的不同实例。
适配器模式:
- 适配器类与被适配类 不一定需要继承相同的抽象类或接口。适配器的作用是将不兼容的接口转换成目标接口,适配器与适配者通常不会有直接的继承关系。
- 适配器与被适配类之间的关系可以是 组合关系,即适配器通过包含被适配类的实例来实现接口转换。
4. 例子与应用场景
装饰器模式:
- 适用于需要 动态增强对象功能 的场景。
- 例如,IO流中的 BufferedInputStream 和 BufferedOutputStream,用于在原始的字节流或字符流基础上增加缓冲功能。
- 另一个例子是 日志记录器,可以通过装饰器动态添加日志记录功能。
示例:
OutputStream os = new FileOutputStream("output.txt"); BufferedOutputStream bos = new BufferedOutputStream(os); // 缓冲功能增强 DataOutputStream dos = new DataOutputStream(bos); // 数据写入功能增强
适配器模式:
- 适用于需要 接口转换 的场景,尤其是在 不兼容接口的类 需要协作时。
- 例如,将字节流(
InputStream
)转为字符流(Reader
)的 InputStreamReader 或者将Runnable
转换为Callable
的 RunnableAdapter。
示例:
// 字节流转字符流 InputStream in = new FileInputStream("input.txt"); Reader reader = new InputStreamReader(in); // 适配器
5. 是否会改变原有类的行为
装饰器模式:
- 通过 包装 来增强对象的功能,但不会改变原始类的行为。原始类仍然存在,可以在任何时候进行替换或者修改。
- 可以多次嵌套装饰器,从而逐步增强对象的功能。
适配器模式:
- 适配器本身 不会改变原始类的行为,它只是改变类与类之间的接口,使其能够兼容并合作。
总结:
特性 | 装饰器模式 | 适配器模式 |
---|---|---|
主要目标 | 动态地增强原始类功能 | 解决接口不兼容问题,使不兼容的类可以协同工作 |
核心思想 | 通过包装原始对象来增强功能 | 通过适配器转换接口,使不兼容的类能够协作 |
接口要求 | 装饰器和原始类通常实现相同的接口或继承相同的抽象类 | 适配器和适配者通常不需要继承相同的接口或抽象类 |
是否改变原始类的行为 | 不改变原始类行为,增强其功能 | 不改变原始类行为,只转换接口 |
适用场景 | 增强功能(如IO流的缓冲、日志等) | 使不兼容的接口可以协同工作(如字节流与字符流之间的转换) |
适配器模式与装饰器模式在结构和实现上有一定的相似性,但其应用场景、目的和实现方式有所不同。装饰器模式更注重功能扩展,而适配器模式则侧重于接口兼容性。
三、工厂模式
工厂模式 是一种创建对象的设计模式,它将对象的创建过程封装在工厂类中,客户端通过工厂方法来获取对象实例,而不是直接使用构造器来实例化对象。这种模式的主要好处是可以隐藏对象创建的具体实现,提供灵活的实例化方式,并且能有效解耦客户端与具体类的依赖。
工厂模式的类型
- 简单工厂模式(Simple Factory Pattern):
- 提供一个工厂类,根据不同的输入(通常是参数),返回不同的对象实例。
- 工厂类负责创建对象,客户端只需要调用工厂方法来获取对象实例,而无需了解对象的创建细节。
- 工厂方法模式(Factory Method Pattern):
- 提供一个抽象工厂接口,具体的工厂子类实现该接口以创建具体的对象。
- 每个子类可以创建不同类型的产品,允许扩展和定制化。
- 抽象工厂模式(Abstract Factory Pattern):
- 提供一个抽象工厂接口,通过不同的工厂类来创建一系列相关产品对象(可能是多个对象,组合使用)。
NIO 中的工厂模式应用
在 NIO(New I/O)库中,工厂模式被广泛应用来创建文件、路径、输入输出流等对象,以下是几个常见的例子:
1. Files.newInputStream()
静态工厂方法
Files.newInputStream()
是 NIO 中 Files
类的静态工厂方法,用于创建 InputStream
对象。通过此方法可以方便地打开一个文件并获得与该文件相关的字节输入流。
Path path = Paths.get("example.txt");
InputStream is = Files.newInputStream(path);
- 优点:
- 隐藏了如何打开文件的实现细节。
- 客户端只需调用该方法即可获取
InputStream
对象,免去了关心底层文件系统的方式。
- 工厂模式应用:
Files.newInputStream()
是工厂模式的一个典型例子,它根据输入的路径创建并返回一个具体的InputStream
实例。
2. Paths.get()
静态工厂方法
Paths.get()
是 NIO 中 Paths
类的静态工厂方法,用于创建 Path
对象。通过此方法,客户端可以根据指定的文件路径来创建一个 Path
实例。
Path path = Paths.get("example.txt");
优点:
- 通过
Paths.get()
,可以根据不同的操作系统和文件系统实现来创建路径对象,简化了路径处理的工作。 - 不需要知道底层具体的文件系统实现和路径格式。
- 通过
工厂模式应用:
Paths.get()
实际上是通过工厂模式创建一个Path
实例,可以适应不同的文件路径需求。
3. ZipFileSystem
的 getPath()
方法
在 NIO 中,ZipFileSystem
是一个用于处理 ZIP 文件的文件系统类,它也是一个典型的应用工厂模式的例子。通过 ZipFileSystem
类的 getPath()
方法,可以创建一个表示 ZIP 文件内文件路径的 Path
对象。
URI uri = URI.create("jar:file:/path/to/archive.zip");
FileSystem fs = FileSystems.newFileSystem(uri, env);
Path path = fs.getPath("/somefile.txt");
优点:
ZipFileSystem
允许客户端通过 URI 创建一个文件系统,进而获取路径对象。- 提供了对压缩文件的透明访问方式,客户端无需直接处理 ZIP 文件的内部结构。
工厂模式应用:
- 这里的
getPath()
方法充当了工厂的角色,通过它可以根据压缩文件的路径创建Path
实例。
- 这里的
总结
- 工厂模式 用于提供一种创建对象的方式,使得客户端与具体类的实例化过程解耦。
- 在 NIO 中,工厂模式被应用于创建文件相关的对象,如
InputStream
、Path
等。- 静态工厂:
Files.newInputStream()
、Paths.get()
。 - 简单工厂:
ZipFileSystem.getPath()
。
- 静态工厂:
- 工厂模式的主要优点是 隐藏实现细节,并提供 灵活的对象创建方式,使得客户端可以专注于业务逻辑而不需要关心对象的具体创建过程。
四、观察者模式
观察者模式(Observer Pattern)是一种设计模式,它定义了对象之间的一对多依赖关系,使得一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。该模式常用于事件驱动的系统中,适用于多个对象同时对某一事件作出响应的场景。
在 NIO 中,文件目录监听服务就是基于 观察者模式 来实现的,具体体现在 WatchService
和 Watchable
接口的设计中。
NIO 中的观察者模式应用
NIO 中的 文件目录监听服务(WatchService
)使用了观察者模式来监听文件系统的变化,例如文件的创建、删除和修改等。WatchService
是 观察者,而 Path
类实现了 Watchable 接口,是 被观察者。文件系统的变化会通过 WatchService
被观察到,并触发相应的事件。
1. Watchable
接口
Watchable
接口定义了一个 register
方法,用于将对象注册到 WatchService
中,这样 WatchService
就能观察文件或目录的变化。
public interface Watchable {
WatchKey register(WatchService watcher,
WatchEvent.Kind<?>[] events,
WatchEvent.Modifier... modifiers)
throws IOException;
}
- 被观察者:实现了
Watchable
接口的类(如Path
)是被观察者,它们的变化会触发通知。 register
方法:用于将文件路径注册到WatchService
中,表示开始监听文件系统的变化。这个方法接受一个WatchService
实例和要监听的事件类型,如文件创建、删除、修改等。
2. WatchService
接口
WatchService
是观察者模式中的 观察者,它负责监听文件目录的变化。当文件或目录发生变化时,它会接收到事件,并可以进行相应的处理。
// 创建 WatchService 对象
WatchService watchService = FileSystems.getDefault().newWatchService();
// 注册路径到 WatchService,指定监听的事件
Path path = Paths.get("workingDirectory");
WatchKey watchKey = path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
- 观察者:
WatchService
是事件的接收者,它监听并处理多个文件路径的变化。 - 事件监听:
WatchKey
可以接收到事件,并通过pollEvents()
方法获取到具体的事件类型,如文件创建、删除或修改。
3. WatchKey
类
WatchKey
代表一个具体的监听事件。通过 WatchKey
,可以获取到文件系统变化的详细信息,比如具体是哪个文件被修改、创建或删除。
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
// 输出事件的具体信息,如文件的名称、事件类型等
System.out.println("Event: " + event.kind() + " " + event.context());
}
key.reset(); // 重置 WatchKey,以便继续监听
}
- 事件类型:常见的事件有:
ENTRY_CREATE
:文件或目录被创建。ENTRY_DELETE
:文件或目录被删除。ENTRY_MODIFY
:文件或目录被修改。
4. 事件通知与轮询
WatchService
使用一个 守护线程(daemon thread)来定期轮询文件系统的变化,检测目录中文件的变化并触发相应的事件。简化后的源码如下所示:
class PollingWatchService extends AbstractWatchService {
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);
}
}
}
- 定时轮询:
PollingWatchService
使用ScheduledExecutorService
定期轮询文件系统的变化,并调用poll()
方法获取事件。 - 事件处理:每次轮询后,获取到的事件会通过
WatchKey
返回给调用者进行处理。
观察者模式的优势
- 解耦:通过观察者模式,
WatchService
与Path
类之间解耦,WatchService
只关心监听哪些事件,而不关心具体的文件或目录。 - 扩展性:可以方便地添加新的监听事件,或者对多个路径进行监听。
- 灵活性:
WatchService
允许动态调整监听的事件类型和监听的路径。
总结
- 观察者模式 在 NIO 中用于实现文件目录的变化监听,通过
WatchService
和Watchable
接口实现。 Watchable
是被观察者,WatchService
是观察者。- 事件通知:
WatchKey
负责封装文件变化的具体信息,WatchService
会通过轮询机制定期检测并通知事件。 - 优势:解耦、扩展性强、灵活性高,适用于事件驱动的应用场景,如文件系统监听。