☰
适配器模式与装饰器模式的区别:从接口转换到功能增强的Java实战解析
2026/9/28 8:58:36 网站建设 项目流程

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 MVC

3.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继承自Reader,FileInputStream继承自InputStream,两个体系不同,所以是适配器。BufferedInputStream继承自InputStream,FileInputStream也继承自InputStream,一家人,装饰器。

3.2 Collections工具类的隐蔽操作

Collections.synchronizedList(new ArrayList<>())这个方法,无数人用过,但很少人意识到它是装饰器模式。它会返回一个SynchronizedList,这个内部类实现了List接口,同时内部持有传入的List引用,所有方法加锁后转发给内部List。接口没变,能力增强了(线程安全),这绝对是装饰器。

再看Collections.unmodifiableList(list),返回的UnmodifiableList同样实现List接口,内部持有原list,但把所有修改方法比如add、remove都抛异常。接口没变,行为变了或约束了,这属于装饰器的变体——它增强的是“防御性”或“限制性”,而不是“性能增强”。所以在面试里,Collections工具类的几个方法可以当作一个加分案例来展开,因为能同时体现“接口不变”和“行为增强/约束”两个特点。

3.3 从集合工具到适配器:Arrays.asList

Arrays.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。翻译官不会让被翻译者变强,升级包不会改变你的身份职业。面试的时候先想到这两个词,再往具体代码上靠,基本就不会翻车了。这个思路我推荐给团队新人后,他们的正确率确实肉眼可见地提升了,你可以试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询