适配器模式在 JDK 和 Spring 中有哪些典型应用?
简化版
适配器模式在 JDK 和 Spring 中很常见,比如 Java I/O 中 InputStreamReader 把字节流适配成字符流,事件监听中的 WindowAdapter 提供默认适配,Spring MVC 的 HandlerAdapter 把不同类型的 Handler 适配成统一调用方式。它们都体现了接口转换或调用方式统一的思想。
详细版
典型例子包括:
InputStreamReader:把InputStream字节输入流适配成Reader字符输入流;OutputStreamWriter:把OutputStream字节输出流适配成Writer字符输出;WindowAdapter:为WindowListener多方法接口提供默认空实现;- Spring MVC
HandlerAdapter:让 DispatcherServlet 能以统一方式调用不同类型的处理器; - Spring 中各种适配器类:把用户实现、框架回调、不同处理器形态适配到统一流程中。
回答时要注意边界:有些例子体现的是适配器思想,不一定完全等同于课本 UML。比如 Spring MVC 的 HandlerAdapter 更偏框架调用适配,它的重点是让前端控制器不用关心具体 Handler 类型。
完整版教学
一、InputStreamReader:字节流到字符流的适配
InputStream 处理的是字节:
InputStream input = new FileInputStream("a.txt");
Reader 处理的是字符:
Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
InputStreamReader 的作用是把字节输入流适配成字符读取接口。客户端使用 Reader 的方式读取字符,内部则从 InputStream 读字节并按字符集解码。
这就是典型的接口转换:已有对象是字节流,客户端需要字符流。
二、OutputStreamWriter:字符输出到字节输出的适配
OutputStreamWriter 和 InputStreamReader 方向相反。
客户端希望按字符写:
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
内部实际要写到字节输出流。OutputStreamWriter 负责把字符按指定编码转换成字节。
这类例子说明适配器不只是方法名转换,还可能包含格式转换、编码转换和数据表示转换。
三、WindowAdapter:默认适配器
Java 事件监听器常常有多个方法。开发者可能只关心其中一个。
WindowAdapter 提供空实现,让开发者只重写关心的方法。
这种是默认适配器,也叫缺省适配器。它解决的是“接口方法太多,实现类被迫写很多空方法”的问题。
它和普通对象适配器不同,但都属于适配器思想的应用。
四、Spring MVC HandlerAdapter:统一调用不同 Handler
Spring MVC 中,DispatcherServlet 是前端控制器。它要处理不同形式的 Handler。
问题是:不同 Handler 的调用方式可能不同。DispatcherServlet 不应该写大量 if else 去判断每种 Handler 怎么调用。
所以 Spring MVC 引入 HandlerAdapter:
DispatcherServlet -> HandlerAdapter -> 具体 Handler
DispatcherServlet 找到合适的 HandlerAdapter 后,通过适配器统一执行 Handler。
这体现了适配器模式的核心价值:框架主流程面向统一接口,具体差异由适配器处理。
五、框架适配器常常不只是简单转发
在框架中,适配器可能还会做很多工作:
- 参数解析;
- 返回值处理;
- 异常转换;
- 生命周期对接;
- 上下文传递;
- 反射调用。
但它的核心仍然是适配:把用户提供的不同形态对象纳入框架统一执行流程。
六、回答框架应用题时要避免绝对化
很多框架类名带 Adapter,但不一定严格等于 GoF 适配器;很多类名不带 Adapter,也可能体现适配器思想。
面试中更稳妥的说法是:
这些例子体现了适配器模式的接口转换或调用统一思想,具体实现会结合模板方法、回调、反射等机制。
这样既准确,也能体现你理解设计模式不是死背类名。
七、框架适配器背后的统一入口
Spring MVC 面对多种 Handler,DispatcherServlet 先找 supports=true 的 HandlerAdapter,再用统一 handle 入口调用具体 Handler。
request -> DispatcherServlet -> find HandlerAdapter -> handle(handler) -> ModelAndView
适配器的验收标准不是“调用成功”,而是 Target 的业务语义在转换前后保持成立。参数、单位、时区、精度、空值、错误码和资源所有权都应逐项形成契约样例,否则最危险的错误会以“成功响应”的形式潜伏。
八、语义边界与契约样例验证
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 框架例子要说清谁是 Target、谁是 Adaptee,以及转换了什么。 |
| 适用边界 | InputStreamReader 做字节到字符解码,需要显式 Charset;WindowAdapter 则用空实现减少监听接口的实现负担,两者适配目的不同。 |
| 测试证据 | 通过框架统一入口传入不同 Adaptee,验证 supports 选择、调用转换和返回模型 |
| 工程代价 | 重点统计字段映射与对象分配成本、外部模型变更影响面,以及多层版本适配造成的认知债务 |
易错点:框架例子要说清谁是 Target、谁是 Adaptee,以及转换了什么。
九、常见误区与追问
- 误区:InputStreamReader 只是两个流的简单转发。 它使用 CharsetDecoder 把字节解码为字符,字符边界和错误策略都是真实语义转换。
- 误区:适配器只需要让方法签名能够编译。 真正的适配还包括单位、时区、错误码、空值、资源释放和幂等语义;签名一致不代表行为兼容。
- 追问:HandlerAdapter 为什么不是简单调用同名方法? 不同 Handler 类型调用方式不同,它先判断支持性,再统一调用并适配返回模型。
- 追问:适配器应该放在哪一层? 通常放在系统边界或防腐层,让业务层只依赖 Target,避免第三方模型和异常向内扩散。
- 追问:适配器测试为什么需要契约样例? 因为类型相同仍可能单位、精度或错误语义不同;契约样例能证明转换后的行为而不只是编译通过。
十、加强记忆
JDK 里记 InputStreamReader、OutputStreamWriter 和 WindowAdapter,Spring 里记 HandlerAdapter。前两个是字节流和字符流的转换,WindowAdapter 是默认适配,HandlerAdapter 是框架统一调用不同 Handler。核心都围绕“接口或调用方式不一致,需要中间层适配”。