什么是适配器模式?
适配器模式是结构型设计模式之一,它的核心思想非常简单 ------引入一个 "中间转换层",将一个接口转换成客户端期望的另一个接口
在 Java 开发中,我们经常会遇到这样的场景:现有代码模块的接口与新需求的接口不兼容,无法直接复用;或者对接第三方组件时,其暴露的方法与我们系统的接口规范不一致。此时,强行修改原有代码会破坏封装性、增加耦合度,甚至引发未知 bug。而适配器模式,正是解决这类 "接口不兼容" 问题的最优方案之一。
在Java中的典型使用
字节转换流:InputStreamReader
一、适配器模式的核心定义与角色
官方定义:将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作,其别名为包装器(Wrapper)。
适配器模式的核心是 转换,这种转换依赖于四个核心角色,缺一不可,我们结合生活中的电源适配器案例,就能快速理解:
1. 目标接口(Target)
客户端期望使用的接口,是适配器最终要提供的接口规范。比如手机充电需要的 5V 直流充电接口,就是目标接口 ------ 客户端(手机)只认这个接口,不符合规范就无法工作。在 Java 中,目标接口通常是一个抽象类或接口,定义了客户端需要的方法。
2. 被适配者(Adaptee)
现有存在的、接口不兼容的类或模块,是我们需要适配的对象。比如墙上的220V 交流电源,它只能提供 220V 交流电,无法直接满足手机的充电需求,就是被适配者。被适配者本身功能完整,只是接口不符合客户端的期望。
3. 适配器(Adapter)
核心转换类,也是适配器模式的核心。它既要实现目标接口,又要持有被适配者的引用,通过自身的逻辑,将被适配者的接口转换成目标接口的规范。比如手机充电头,它一端对接 220V 交流电源(被适配者),另一端提供 5V 直流接口(目标接口),中间完成电压转换,就是适配器。
4. 客户端(Client)
使用目标接口的对象,它只关注目标接口的方法,不关心被适配者和适配器的内部逻辑。比如手机,它只需要通过 5V 接口充电,不需要知道充电头如何将 220V 转换成 5V。
四个角色的关系可以总结为:客户端调用适配器的目标接口方法 → 适配器调用被适配者的方法 → 适配器将结果转换为目标接口格式 → 客户端获得符合预期的结果
二、适配器模式的两种实现方式(Java 实战)
在 Java 中,适配器模式有两种主流实现方式,核心区别在于适配器与被适配者的关联方式 ------ 继承 vs 组合 。其中,组合方式因低耦合、高灵活度,成为实际开发中的首选。
方式一:对象适配器(推荐)
对象适配器通过 组合 的方式持有被适配者的引用,适配器实现目标接口,在目标接口的方法中调用被适配者的方法,并完成转换。 这种方式符合合成复用原则,耦合度低,且能避免 Java 单继承的限制。
实战场景:字节流转字符流的简化实现
我们模拟 JDK 中InputStreamReader的核心逻辑,实现一个简单的字节流到字符流的适配器,直观感受对象适配器的工作流程。
1、定义目标接口(Target):字符流接口 Reader
java
// 目标接口:客户端期望的字符流接口
public interface Reader {
// 读取字符到字符数组中,返回读取的字符数
int read(char[] cbuf);
}
2、定义被适配者(Adaptee):字节流类 InputStream
java
// 被适配者:现有的字节流,接口不兼容(只能读字节)
public class InputStream {
// 读取字节到字节数组中,返回读取的字节数
public int read(byte[] b) {
// 模拟读取数据:假设读取到"Java适配器模式"的字节
String data = "Java适配器模式";
byte[] bytes = data.getBytes();
// 将模拟数据复制到传入的字节数组
System.arraycopy(bytes, 0, b, 0, bytes.length);
return bytes.length;
}
}
3、实现适配器(Adapter):InputStreamReader
java
// 适配器:将字节流适配成字符流(实现目标接口,持有被适配者引用)
public class InputStreamReader implements Reader {
// 组合:持有被适配者(字节流)的引用
private final InputStream inputStream;
// 构造方法:传入被适配者对象
public InputStreamReader(InputStream inputStream) {
this.inputStream = inputStream;
}
// 实现目标接口方法:完成字节→字符的转换
@Override
public int read(char[] cbuf) {
// 1. 调用被适配者的方法,读取字节
byte[] bytes = new byte[cbuf.length];
int readLen = inputStream.read(bytes);
// 2. 核心转换:将字节数组转换成字符数组
if (readLen > 0) {
String str = new String(bytes, 0, readLen);
str.getChars(0, readLen, cbuf, 0);
}
return readLen;
}
}
3、客户端调用(Client)
java
// 客户端:只使用目标接口(Reader),不关心适配逻辑
public class Client {
public static void main(String[] args) {
// 1. 实例化被适配者(字节流)
InputStream inputStream = new InputStream();
// 2. 实例化适配器,包装字节流
Reader reader = new InputStreamReader(inputStream);
// 3. 客户端使用字符流接口,完成读取
char[] buffer = new char[20];
int len = reader.read(buffer);
System.out.println("读取到的字符:" + new String(buffer, 0, len));
}
}
运行结果
读取到的字符:Java适配器模式
从代码可以看出,客户端只与Reader接口交互,完全不知道InputStream的存在,适配器完美隐藏了转换逻辑,实现了解耦。
方式二:类适配器
类适配器通过 继承 的方式,让适配器继承被适配者,同时实现目标接口。这种方式耦合度高,因为 Java 是单继承语言,适配器继承被适配者后,就无法再继承其他类,灵活性极差,仅适用于简单场景。
类适配器实现
java
// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends InputStream implements Reader {
@Override
public int read(char[] cbuf) {
// 继承被适配者的read方法,读取字节
byte[] bytes = new byte[cbuf.length];
int readLen = super.read(bytes);
// 字节→字符转换
if (readLen > 0) {
String str = new String(bytes, 0, readLen);
str.getChars(0, readLen, cbuf, 0);
}
return readLen;
}
}
类适配器的缺点很明显:单继承限制导致扩展性差,适配器与被适配者耦合过紧,一旦被适配者修改,适配器也需要同步修改,不符合 "开闭原则"。因此,实际开发中几乎不使用类适配器。
三、JDK 中的适配器模式实战:InputStreamReader 深度解析
前面我们模拟了InputStreamReader的简化实现,而 JDK 中真正的InputStreamReader,正是适配器模式的经典应用,其底层逻辑与我们手写的版本完全一致,只是增加了编码处理、缓冲区优化等细节。
1. JDK 中角色对应
- 目标接口(Target) :
Reader(抽象类,定义字符流的核心方法) - 被适配者(Adaptee) :
InputStream(抽象类,定义字节流的核心方法) - 适配器(Adapter) :
InputStreamReader(继承 Reader,持有 InputStream 引用)
2. 源码核心逻辑(简化)
java
public class InputStreamReader extends Reader {
// 持有被适配者:字节流
private final InputStream in;
// 字节→字符解码引擎(JDK优化,处理编码问题)
private final StreamDecoder sd;
// 构造方法传入被适配者
public InputStreamReader(InputStream in) {
super();
this.in = in;
this.sd = StreamDecoder.forInputStreamReader(in, this, Charset.defaultCharset());
}
// 实现Reader的read方法,底层调用解码引擎转换
@Override
public int read(char[] cbuf) throws IOException {
return sd.read(cbuf);
}
}
可以看到,JDK 的实现依然遵循对象适配器 的核心逻辑:持有被适配者(InputStream),实现目标接口(Reader),通过StreamDecoder完成字节到字符的转换(解决编码问题,比如 UTF-8、GBK 等)。这也印证了对象适配器的实用性和灵活性。
除了InputStreamReader,JDK 中还有很多适配器模式的应用,比如:
OutputStreamWriter:将字节流适配成字符流(与InputStreamReader对称)Arrays.asList():将数组适配成 List 集合- Spring MVC 的
HandlerAdapter:适配不同类型的 Controller,统一请求处理接口
四、适配器模式的优缺点与适用场景
任何设计模式都有其适用范围,适配器模式也不例外,我们需要明确其优缺点,才能在合适的场景中使用。
核心优点
- 解耦:将客户端与被适配者分离,客户端无需修改代码,就能使用不兼容的模块,符合 "开闭原则"。
- 复用性:复用现有不兼容的类,无需重写代码,降低开发成本。
- 扩展性:新增适配逻辑时,只需新增适配器类,不改动原有代码,灵活性高。
- 单一职责:适配逻辑集中在适配器类中,让被适配者和目标接口各司其职,符合 "单一职责原则"。
潜在缺点
- 增加代码复杂度:引入适配器会增加一个中间层,可能导致代码层级变多,可读性下降。
- 适配过度:如果接口差异过大,适配器的转换逻辑会非常复杂,反而增加维护成本。
适用场景
- 现有类的接口与客户端需求不兼容,且无法修改现有类(比如第三方组件、老系统代码)。
- 需要复用多个不相关的类,让它们统一适配到一个目标接口,供客户端统一调用。
- 系统升级或重构时,需要兼容老系统的接口,避免大量修改客户端代码。
五、适配器模式的核心总结
适配器模式的本质是 转换与兼容,它不是创造新功能,而是通过中间层,让原本不兼容的代码协同工作。我们可以用三句话快速记住它:
- 核心作用:解决接口不兼容问题,实现解耦与复用。
- 实现首选:对象适配器(组合),避免类适配器(继承)的单继承限制和高耦合。
- 经典案例 :JDK 的
InputStreamReader,完美诠释了适配器模式的核心逻辑。