新闻详情

新闻详情

首页 / 资讯中心 / 详情

适配器模式与装饰器模式的区别:从接口转换到功能增强的Java实战解析

发布时间:2026/9/29 14:08:39来源:尧图网络
适配器模式与装饰器模式的区别:从接口转换到功能增强的Java实战解析
Java面试里适配器模式和装饰器模式是一对出了名的双胞胎几乎每隔几场面试就会出现一次“请说说适配器模式和装饰器模式的区别”。我面过不少候选人定义背得头头是道一画类图、一写代码就露馅。这篇文章不是来凑八股文的咱们直接从实际代码和真实场景里把这俩掰开揉碎看清楚。如果你正准备跳槽、或者在项目里纠结“这个包装类到底算什么”读完应该能有个很明确的判断框架。先说结论方向适配器模式解决的是“接口不兼容”的问题它的核心动作是转换装饰器模式解决的是“功能不够用”的问题它的核心动作是增强。听起来简单但一到具体代码里很多人就开始犯迷糊尤其当两者都有一个共同的别名“包装Wrapper”时。所以这篇文章我会从问题起源、代码结构、JDK源码实例、面试答题技巧、手写demo五个维度来讲保证你看完能直接用。1. 先弄明白这两个模式各自解决什么问题1.1 适配器接口和接口之间需要一位翻译适配器模式最原始的动机是让原本因为接口不匹配而无法合作的类能一起工作。你可以把它想象成插头转换器你从国外带回来一个电器插头是欧标的但国内插座是国标的物理上根本插不进去。转换器做的事就是把欧标插头转换成国标插口能接收的形态。它不改电器本身也不改变电器的功能纯粹是让“接口”对接上。映射到Java里最典型的场景是系统里有一批老代码它们暴露的接口是OldService但新的业务模块只认NewService接口。你不能改老代码也不应该逼迫调用方去适配老接口于是写一个Adapter实现NewService内部转而调用OldService的方法。调用方感觉不到老类的存在它只面对NewService。这个过程中对象的“能力”没有增加任何新东西只是把已有的能力用另一个接口暴露出来。用一句话记忆适配器是换了一层皮让原本互不相识的两个接口能够对话。1.2 装饰器功能要叠加又不允许改源码装饰器模式的动机完全不同。它面对的场景是你已经有一个功能完整的类但是想给它加一些附加能力比如给文件流加缓冲、给查询加缓存、给支付加日志。最直接的办法是改源码但违背了开闭原则而且改动可能影响所有使用方。装饰器模式的做法是把这个类包装一层新写一个类保持和原类相同的接口同时在调用原始方法前后增加额外逻辑。与适配器最本质的区别在于装饰器转了一圈接口没变。调用方拿到的对象依然可以当作原来的类型使用甚至是在原来类型上“升级”了。功能变多了但外貌没变。这就像给手机套了一个防摔壳手机还是那个手机接口充电口、耳机口都还在但你多了一层防摔能力。注意手机壳不是把手机变成了平板它还是手机只是“加强版手机”。适配器是改变接口装饰器是保持接口并增强行为。这是区分它们的总纲领。1.3 记不住定义先记住一句话方向很多博客喜欢列定义、列结构、列优缺点读者看完就忘。我建议你先记住两句粗糙但好用的话适配器进行接口转换为的是让原本不兼容的东西能用同一个插座。装饰器进行行为增强为的是让原有的功能更强大但外观不变。面试的时候不管多紧张先把这两句话砸出去再展开细节面试官马上知道你是懂行的。接下来第二步才是用代码结构去验证你的判断。2. 从代码结构上找差异比背定义靠谱2.1 判断标准一操作前后的接口变没变这是最简单、也是最有区分度的一条。拿一个对象A经过某个“包装类”B之后如果使用方拿到的类型从X变成了Y那这个B就是适配器。比如数组是int[]经过Arrays.asList拿到的是List接口从数组变成了List接口所以它本质上是适配器应用再比如InputStreamReader内部接收字节流对外暴露的是字符流Reader接口接口从字节流变成了字符流因此它是适配器。反过来如果对象的类型在包装前后始终保持一致比如FileInputStream包装成BufferedInputStream两者都是InputStream子类那这是装饰器。调用方从头到尾只看到InputStream只是实例换了性能变了接口没变。这个判断标准最直接面试时只要说出“看接口变没变”基本就能把九成场景分清。2.2 判断标准二目标接口角色的存在与否适配器模式有三个核心角色目标接口Target、被适配者Adaptee、适配器Adapter。适配器一定要实现“目标接口”内部持有“被适配者”。这里的关键词是“目标接口”——它是外部已经约定好的标准Adapter必须向这个标准看齐。装饰器模式也有三个角色抽象组件Component、具体组件ConcreteComponent、装饰器Decorator。注意装饰器和具体组件实现的是同一个抽象接口装饰器内部持有的也是这个抽象接口类型。从类型体系来看装饰器和被装饰者是一家人它们是同一棵继承树上的兄弟。所以代码结构上又有一个判断技巧如果“包装类”和“被包装类”处在同一个接口/抽象类继承体系里并且对外暴露的类型不变那是装饰器如果“包装类”实现的是一个“别的接口”和被包装类根本不在一个体系里那多半是适配器。2.3 判断标准三代码里有没有super调用与递归式增强装饰器模式有一个非常典型的实现特征装饰器类往往有一个抽象基类抽象基类持有抽象组件引用并把接口方法默认转发给组件具体装饰器重写方法在super.xxx()调用前后插入增强逻辑。换句话说装饰器调用链上可能出现“一层包一层”的递归效果。适配器模式则不太会有这种逐层递归的包装链。它更多是“一对一转换”Adapter内部持有一个Adaptee把目标接口的方法调用转发给Adaptee的具体方法。你不会看到一个适配器包装另一个适配器包装另一个适配器还保持接口不变的链式结构——如果有链式那多半是适配器之后套装饰器或装饰器套装饰器。看代码时如果发现构造参数和自身实现的是同一个接口并且构造参数也被当作同一个接口使用那基本可以认定是装饰器如果构造参数类型是实现细节、返回值类型是另一套接口那就是适配器。2.4 一分钟判定表为了方便记忆我做了一个对照表面试前可以快速过一眼维度适配器模式装饰器模式核心目的接口转换让不兼容的接口协同工作功能增强让对象在不改接口的前提下增加能力接口变化变了客户端看到的是另一个接口不变客户端看到的还是原接口别称Wrapper包装Wrapper包装典型结构实现目标接口持有被适配者实现组件接口持有组件引用类层次关系与被适配者不在同一继承体系与被装饰者在同一继承体系增强行为不关心转发为主核心在调用前后加逻辑链式包装少见很常见一层套一层JDK例子Arrays.asList、InputStreamReaderBufferedInputStream、Collections.synchronizedList表格背下来只是及格真正拉开差距的是你有没有理解背后的设计意图。下一节我们用JDK源码实例把这张表验证一遍。3. Java生态里的活教材IO流、集合工具、Spring MVC3.1 IO流里藏着答案刚学Java IO的时候大家肯定都写过这样的代码// 适配器视角字节流 - 字符流 InputStreamReader reader new InputStreamReader(new FileInputStream(demo.txt)); BufferedReader br new BufferedReader(reader); // 装饰器视角无缓冲 - 有缓冲 BufferedInputStream bis new BufferedInputStream(new FileInputStream(demo.txt));第一行FileInputStream是字节流InputStreamReader把它适配成字符流。使用方拿到的是Reader操作单位从字节变成了字符接口已经改变所以它是适配器。不信你可以去看JDK源码InputStreamReader内部有一个StreamDecoder它把字节解码成字符整个过程目标是让“字节流”变“字符流”妥妥的适配。第二行BufferedInputStream呢它内部用了一个缓冲区数组重写了read()方法调用父类/组件的read()来填充缓冲区。但不管包装前还是包装后类型都是InputStream调用方仍然用的是InputStream的API只是读起来更快了。这就是教科书级的装饰器应用。还有一种辅助判断方式装饰器通常和原组件拥有同一个父类或接口适配器则经常是“跨体系”的。InputStreamReader继承自ReaderFileInputStream继承自InputStream两个体系不同所以是适配器。BufferedInputStream继承自InputStreamFileInputStream也继承自InputStream一家人装饰器。3.2 Collections工具类的隐蔽操作Collections.synchronizedList(new ArrayList())这个方法无数人用过但很少人意识到它是装饰器模式。它会返回一个SynchronizedList这个内部类实现了List接口同时内部持有传入的List引用所有方法加锁后转发给内部List。接口没变能力增强了线程安全这绝对是装饰器。再看Collections.unmodifiableList(list)返回的UnmodifiableList同样实现List接口内部持有原list但把所有修改方法比如add、remove都抛异常。接口没变行为变了或约束了这属于装饰器的变体——它增强的是“防御性”或“限制性”而不是“性能增强”。所以在面试里Collections工具类的几个方法可以当作一个加分案例来展开因为能同时体现“接口不变”和“行为增强/约束”两个特点。3.3 从集合工具到适配器Arrays.asListArrays.asList也是一个高频考点。你把一个数组传给这个方法拿到的是一个List。数组本身没有List接口asList内部有一个实现List接口的内部类ArrayList注意不是java.util.ArrayList而是Arrays内部的一个私有静态类它把数组包装成List视图。接口从数组变成了List所以这是适配器手段。很多新人以为它是装饰器因为它“包装”了数组——这就是被别称Wrapper误导的典型例子。还有一个容易被忽略的点asList返回的List不支持结构修改操作因为底层还是数组。这又说明了适配器的另一个特征“转换”不等于“功能增强”它只是让你换了一种方式访问并没有给数组新增动态扩容能力。3.4 Spring MVC里的适配器身影Spring MVC中的HandlerAdapter是适配器模式的经典工业级案例。DispatcherServlet持有的是HandlerAdapter接口引用但实际的Handler可能是Controller方法、可能是HttpRequestHandler、可能是HandlerMethod五花八门。DispatcherServlet不关心每个Handler的具体类型它只调用统一的HandlerAdapter.handle(...)方法。每个具体的Adapter比如RequestMappingHandlerAdapter、HttpRequestHandlerAdapter内部把不同Handler的调用方式转换成统一的ModelAndView返回。这个过程就是典型的“接口转换”适配器在框架里承担了翻译官的角色。如果把SpringMVC的这堆Adapter想象成装饰器那就完全说不通了因为装饰器的前提是“保持接口不变”而Handler的种类不同接口本来就千差万别。适配器的作用正是抹平这些差异。4. 面试这样答区分度一下就出来4.1 一套可以现场复用的回答范式很多候选人面试的时候一紧张就开始“适配器就是适配器装饰器就是装饰器”来回说面试官听不出重点。我建议你按这个顺序来组织口头答案第一步一句话定位。适配器解决接口不兼容装饰器解决功能增强。第二步强调接口变化。适配器会改变对外接口装饰器保持接口不变。第三步举JDK例子。适配器举InputStreamReader装饰器举BufferedInputStream这两个例子几乎零成本说明问题。第四步升华到设计思想。适配器是为了“复用已有实现”装饰器是为了“遵循开闭原则动态扩展功能”。第五步如果还有余力提一嘴代理模式的对比装饰器和代理模式在代码结构上相似但装饰器强调增强功能代理强调控制访问、延迟加载、拦截等两者的侧重点和业务意图不一样。这样一套下来面试官能听到你的记忆框架也能看到你的底层理解而不是死记硬背。4.2 三个最容易被带偏的误区误区一把所有“包装类”都叫装饰器。因为两种模式都有Wrapper别名很多人一看到构造器接收同类就把帽子扣到装饰器上。必须识别接口是否变化这是第一优先级。误区二把动态代理当装饰器。JDK动态代理、CGLIB代理生成了代理类看起来也是在方法前后加逻辑结构上和装饰器有点像。但装饰器是静态的包装代理模式更强调对目标访问的控制。在实际项目中Spring AOP大量使用动态代理来实现日志、事务面试里你如果说AOP是装饰器模式面试官会追问很可能发现你混淆了意图和实现手段。正确说法是Spring AOP基于代理模式装饰器模式和代理模式在类结构上有相似之处但装饰器属于结构型模式、目的是增强能力代理属于行为模式、目的是控制访问。误区三认为适配器只能用于类不兼容。有时候适配器也用于“数据格式不匹配”比如把JSON格式适配成XML格式。但只要包装后的“接口”变了它就是适配器思路。别因为对方说“我是在做数据转换”就想不起来适配器。4.3 一句话讲出一个加分的深层理解我面试时对候选人的要求不是背出定义而是看能不能说出为什么要引入这两种模式。一个比较加分的说法是“适配器模式解决的是外部接口的统一问题往往是在设计已经存在、但无法修改的情况下通过适配层让新老代码共存装饰器模式解决的是内部功能的增量演进问题是对同一接口不断做功能叠加让功能扩展不需要侵入原有类。”这句话能让你在众多只会说“一个是转换一个是增强”的候选人里跳出来因为它点出了两个模式在“设计时间点”上的差异适配器往往出现在“集成阶段”装饰器则出现在“演进阶段”。5. 动手写一遍两个小案例彻底打通5.1 案例一适配器——把圆形桩塞进方形孔假设你有一个旧类计算圆形桩的半径// 老接口圆形桩 class RoundPeg { private double radius; public RoundPeg(double radius) { this.radius radius; } public double getRadius() { return radius; } } // 新接口方形桩方形孔只认方形桩 interface SquarePeg { double getSide(); } class SquareHole { private double side; public SquareHole(double side) { this.side side; } public boolean fits(SquarePeg peg) { return peg.getSide() side; } }现在有一个SquareHole你要判断RoundPeg能不能放进这个方形孔。很明显SquareHole只接受SquarePeg接口。直接传入RoundPeg不行这时就写个适配器// 适配器实现新接口内部包装旧类 class RoundPegAdapter implements SquarePeg { private RoundPeg roundPeg; public RoundPegAdapter(RoundPeg roundPeg) { this.roundPeg roundPeg; } Override public double getSide() { // 把圆形桩的直径转换为方桩的边长 return roundPeg.getRadius() * 2; } }调用方式public class AdapterDemo { public static void main(String[] args) { RoundPeg roundPeg new RoundPeg(3); SquarePeg squarePeg new RoundPegAdapter(roundPeg); SquareHole hole new SquareHole(5); System.out.println(半径3的圆桩能否塞进边长5的方孔 hole.fits(squarePeg)); } }这个例子里的关键点RoundPegAdapter实现的是SquarePeg接口而不是RoundPeg的子接口。它把getRadius()转换成getSide()接口从圆桩变方桩方向是“转换”。如果你把这段代码改成装饰器的逻辑那应该是实现RoundPeg并重写getRadius这就不叫适配器了。5.2 案例二装饰器——给咖啡加奶加糖再来写装饰器场景是咖啡店。抽象组件就是咖啡接口interface Coffee { double cost(); String description(); } // 基础咖啡 class Espresso implements Coffee { Override public double cost() { return 15; } Override public String description() { return 浓缩咖啡; } }装饰器基类要做的就是实现同一个接口内部持有接口引用默认转发方法abstract class CoffeeDecorator implements Coffee { protected Coffee coffee; public CoffeeDecorator(Coffee coffee) { this.coffee coffee; } Override public double cost() { return coffee.cost(); } Override public String description() { return coffee.description(); } }加奶装饰器class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } Override public double cost() { return super.cost() 3; } Override public String description() { return super.description() 加奶; } }加糖装饰器class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); } Override public double cost() { return super.cost() 1; } Override public String description() { return super.description() 加糖; } }调用方式public class DecoratorDemo { public static void main(String[] args) { Coffee coffee new Espresso(); coffee new MilkDecorator(coffee); coffee new SugarDecorator(coffee); System.out.println(coffee.description() 价格 coffee.cost()); } }运行结果浓缩咖啡加奶加糖价格19.0。关键点在于整个过程中变量的静态类型一直没离开过Coffee每次包装后拿到的还是Coffee类型。装饰器把增强逻辑写在调用链上一层层叠加上去这就是装饰器模式的编码感觉。5.3 两个案例放一起对照着看把两份代码放在一起对比几个差异特别直观对比点RoundPegAdapterCoffeeDecorator实现接口实现的是 SquarePeg不是RoundPeg实现的是Coffee接口和Espresso一致构造参数接收 RoundPeg接收 Coffee返回接口变成了SquarePeg依然是Coffee增强逻辑无仅转换有叠加价格、叠加描述核心语义让圆形桩能被方形孔接受让咖啡更丰富/贵我见过很多人看各种博客说“装饰器是特殊的适配器”这种说法我认为不够准确容易让人产生误解。两者结构都是“持有引用转发调用”但从设计意图到代码结构都有本质差别。只强调结构相似而忽略意图差异是面试答偏的主要原因。面试官问区别你要把“接口变不变”和“目的到底是为了什么”放在最前面。6. 常见误解与排查思路分享6.1 为什么有人总觉得IO流是适配器我在实际带人的时候经常遇到新人说BufferedReader是适配器理由是“它把字节流变成字符流”。这句话对了四分之一但又错得离谱。BufferedReader接收的是Reader类型不是InputStream。它构造器明确是BufferedReader(Reader in)而Reader本身已经是字符流了所以BufferedReader并没有做字节到字符的转换。真正做转换的是InputStreamReader——它构造器接收InputStream对外是Reader。一般写法是串联new BufferedReader(new InputStreamReader(new FileInputStream(...)))这个表达式里既有适配器转换字节到字符又有装饰器给字符流加缓冲。你把整条链说成适配器是把链上的两种角色揉在一起了。排查技巧看链上的每一个节点单独判断它改变了接口还是增强了功能不要用“最终效果”来倒推整个链条。这个思路在项目里分析多层包装类时特别管用。6.2 项目里什么时候用什么怎么判断业务代码里如果你要对接外部系统外部返回的数据结构和内部接口不一致别改内部接口先考虑用适配器统一入口。比如支付场景多渠道对接各家回调数据结构不同你可以定义一个统一的PaymentCallbackAdapter接口每家渠道一个实现这是适配器很典型的使用场景。如果你想给已有的核心对象增加通用能力比如加日志、加缓存、加权限校验但不想污染核心类装饰器是首选。举个例子你有一个ReportService接口现在要在调用前后打印耗时你可以写一个TimingReportServiceDecorator包装原实现而不是在ReportServiceImpl里加计时代码。这样将来想移除耗时统计直接换回原实现就行不用动业务代码。这种维护体验用适配器是做不到的。6.3 面对面试官追问如何保持稳定输出面试官问完区别后大概率会追问“适配器有哪几种” 回答类适配器和对象适配器。类适配器用继承对象适配器用组合实际开发推荐组合方式因为更灵活不受单继承限制。“装饰器会不会有性能损耗” 回答会每次包装都多一层方法调用栈包装层级过多可能影响性能。项目里如果只是为了加一两个能力别套五层六层适度就好。“装饰器和代理模式的区别是什么” 回答结构上相似但意图不同。装饰器强调给对象增强功能代理模式强调控制对对象的访问比如延迟初始化、远程代理、访问控制等。“你怎么判断一个未知类是不是装饰器” 回答先看构造方法是否接收相同接口/父类类型再看包装后类型是否不变再看是否重写了原来的方法并在调用前后做了增强三点全符合就是装饰器。这些追问的答案本质上依然是围绕“接口是否变化”和“意图是转换还是增强”展开。你只要咬住这两点答案怎么绕都不会跑偏。最后再分享一个我自己常用的记忆法适配器是“翻译官”把一种语言翻译成另一种语言装饰器是“升级包”在原有装备上不断叠加buff。翻译官不会让被翻译者变强升级包不会改变你的身份职业。面试的时候先想到这两个词再往具体代码上靠基本就不会翻车了。这个思路我推荐给团队新人后他们的正确率确实肉眼可见地提升了你可以试试。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

研发项目管理工具与模板:中小团队落地实践指南 2026/9/29 14:08:36

研发项目管理工具与模板:中小团队落地实践指南

简介:本资源是一套面向研发项目经理、技术负责人及研发流程优化从业者的「研发项目管理工具与模板」实战课件,聚焦解决跨部门协同低效、计划失控、需求蔓延、质量保障薄弱等典型研发管理痛点。课件以PPT形式系统梳理项目生命周期各阶段核心方法论&#x…

阅读更多 →
Pylint与Flake8如何分工搭配?Python代码检查实战指南 2026/9/29 14:08:36

Pylint与Flake8如何分工搭配?Python代码检查实战指南

接手过老项目的人都懂,代码本身能跑、测试能过,但一改起来就像拆雷。命名乱成一锅粥、一个函数里塞十几个参数、无用的 import 堆了满满一屏,想重构又怕碰坏哪个隐藏逻辑。这种时候,静态检查工具就是最后一道心理防线。Python 生态…

阅读更多 →
多回路温控模块如何重构控温范式:从单表堆砌到Modbus集成 2026/9/29 14:08:36

多回路温控模块如何重构控温范式:从单表堆砌到Modbus集成

1. 多温区控温的痛点与东崎模块的破局思路做过多温区设备的人都有一个共同感受:一台设备上如果有四路、八路甚至十六路加热区,用传统单表方案搭出来的电柜,打开柜门那一瞬间自己都不想看。每路温控器一个独立表头,加上固态继电器、…

阅读更多 →
VSCode 没了这些插件,感觉代码都不会写了:用 TaoToken 统一 Key 打通 AI 编程链路 2026/9/29 14:08:29

VSCode 没了这些插件,感觉代码都不会写了:用 TaoToken 统一 Key 打通 AI 编程链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
STULZ机房空调保养维护全攻略:从制冷循环到故障排查 2026/9/29 14:08:29

STULZ机房空调保养维护全攻略:从制冷循环到故障排查

简介:STULZ机房空调操作、保养与维护大全是一份面向数据中心运维人员和精密空调维护岗的实操性技术文档,适合从新手到老手在日常巡检、应急排障时对照查阅。内容围绕STULZ空调的全生命周期使用与管理展开:操作部分包括开关机、控制器菜单密码…

阅读更多 →
Paperclip:面向本地开发的AI工具链协同协议 2026/9/29 14:08:29

Paperclip:面向本地开发的AI工具链协同协议

1. 项目概述:Paperclip 不是回形针,而是一个被严重低估的 AI 工具链协同范式“Paperclip”这个词在中文技术圈里最近频繁跳出来,但绝大多数人第一反应还是办公桌抽屉里的那个银色小物件——这恰恰说明,它背后代表的技术理念还没被…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉