Java 热门面试题200道之设计模式(11题)
(1)谈谈你了解的最常见的几种设计模式,说说他们的应用场景
1. 单例模式(Singleton Pattern)
- 介绍:确保一个类只有一个实例,并提供一个全局访问点。
- 应用场景:
- 配置管理:系统中需要一个全局配置对象来存储设置,如数据库连接信息等。
- 日志记录:创建一个全局日志记录器,统一管理日志输出。
2. 工厂模式(Factory Pattern)
- 介绍:定义一个创建对象的接口,但由子类决定实例化哪一个类。
- 应用场景:
- 对象创建复杂:当创建对象需要大量重复代码时,封装在工厂中简化创建逻辑。
- 系统扩展:当系统需要扩展新对象时,只需添加新工厂,无需修改原有代码。
- 需要控制资源:如数据库连接池,控制同时创建的连接数。
3. 观察者模式(Observer Pattern)
- 介绍:定义对象间的一对多依赖关系,当一个对象状态变化时,所有依赖对象自动更新。
- 应用场景:
- 事件驱动系统:GUI程序中,按钮点击事件可被多个监听器观察处理。
- 数据绑定:前端框架中,数据模型变化自动更新视图。
- 消息订阅:系统中部分对象需订阅另一对象消息,如新闻订阅、邮件通知等。
4. 策略模式(Strategy Pattern)
- 介绍:定义一系列算法,将每个算法封装起来,使它们可以互换。
- 应用场景:
- 算法切换:系统需动态切换算法,如计算运费时根据订单重量选择不同运费计算策略。
- 解耦算法:使算法与使用它代码独立,便于维护扩展,如排序策略选择。
- 策略配置:根据配置文件或用户输入选择策略,如不同促销策略配置。
5. 适配器模式(Adapter Pattern)
- 介绍:将一个类的接口转换成客户希望的另一个接口。
- 应用场景:
- 系统集成:将旧系统接口转换为新系统所需接口,使新旧系统兼容。
- 类库复用:使因接口不兼容而不能一起使用的类合作,如将不同数据库接口统一。
- 框架开发:提供固定接口,通过适配器模式兼容不同组件。
6. 装饰器模式(Decorator Pattern)
- 介绍:动态地给对象增加额外职责,比子类化更灵活。
- 应用场景:
- 功能扩展:在不修改原对象前提下,动态添加功能,如Java IO流操作中对输入输出流功能增强。
- 灵活组合:多种装饰可组合使用,实现功能灵活组合,如咖啡店按不同配料组合咖啡。
- 避免代码冗余:替代过多子类,减少冗余代码。
7. 代理模式(Proxy Pattern)
- 介绍:为其他对象提供一种代理以控制对这个对象的访问。
- 应用场景:
- 远程代理:透明化远程对象访问,本地代码无需关心对象在远程机器。
- 虚拟代理:对重资源对象提供临时占位符,按需加载。
- 保护代理:控制不同权限用户对对象访问,如管理员和普通用户。
8. 模板方法模式(Template Method Pattern)
- 介绍:定义一个操作中的算法骨架,将一些步骤延迟到子类中。
- 应用场景:
- 框架搭建:框架提供通用流程,允许子类覆盖具体步骤,如Web框架统一处理HTTP请求流程。
- 代码复用:相同代码封装在模板方法,具体子类复用,减少重复代码。
- 流程控制:固定流程顺序,让子类控制部分步骤,如产品制作流程。
9. 命令模式(Command Pattern)
- 介绍:将请求封装为对象,使可用不同的请求对客户进行参数化。
- 应用场景:
- 命令队列:将多个命令放入队列依次执行,如批处理任务。
- 日志恢复:记录命令执行,便于恢复撤销,如Word中撤销命令。
- 远程控制:将命令对象传输到远程执行,如远程服务器管理。
(2)什么是策略模式?一般用在什么场景?
策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,让算法独立于使用它的客户端(调用方)而变化。
很多情况下,我们代码里有大量的ifelse、switch等,可以通过使用策略模式,避免大量条件语句的使用,实现算法的分离和独立变化。
它的主要目的是为了解耦多个策略,并方便调用方在针对不同场景灵活切换不同的策略。
策略模式的特点:
- 算法封装:将算法的实现与使用算法的代码分离,通过封装提高代码的灵活性和可扩展性。
- 动态替换:可以在运行时选择和替换算法。
- 遵循开闭原则:新增策略无需修改现有代码。
一般用在什么场景?
- 多种算法可互换:需要动态选择算法,例如排序算法的选择。
- 避免条件语句:通过策略模式替代代码中大量的if-else或switch语句。
- 与上下文独立:客户端不需要知道具体的算法实现细节,只需依赖抽象策略。
典型场景:
- 支付系统:支持多种支付方式(如微信、支付宝、信用卡)。
- 数据压缩:提供不同的压缩算法。
- 日志策略:根据日志级别动态选择记录策略。
(3)什么是责任链模式?一般用在什么场景?
责任链模式是一种行为型设计模式,将多个对象连接成一条链,并且沿着这条链传递请求,让多个对象都有机会处理这个请求,请求会顺着链传递,直到某个对象处理它为止。
它主要避免了请求发送者和接受者之间的耦合,增强了系统的灵活性和可扩展性。
责任链模式的特点:
- 解耦请求发送者与接收者:请求发送者无需知道请求是由哪个处理器处理的。
- 动态组合处理器:可以灵活地动态改变链的结构。
- 请求沿链传递:请求可以由链中的一个或多个处理器处理。
一般用在什么场景?
- 请求需要多个处理器:例如日志记录的不同级别处理。
- 动态指定处理流程:请求的处理方式不固定,依赖于运行时的链条结构。
- 消除条件分支:用责任链代替代码中的if-else或switch-case语句。
典型场景:
- 事件处理系统(如GUI的事件冒泡机制)。
- 审批流程(如多级审批)。
- 日志系统(如不同级别的日志记录)。
在很多场景都能看到责任链模式,比如Spring拦截器的Chain也是责任链模式。
(4)什么是模板方法模式?一般用在什么场景?
模板方法模式是一种行为型设计模式,它在一个抽象类中定义了一个算法(业务逻辑)的骨架,具体步骤的实现由子类提供,它通过将算法的不变部分放在抽象类中,可变部分放在子类中,达到代码复用和扩展的目的。
- 复用:所有子类可以直接复用父类提供的模板方法,即上面提到的不变的部分。
- 扩展:子类可以通过模板定义的一些扩展点就行不同的定制化实现。
模板方法模式的特点:
- 算法骨架:在基类中定义一个算法的固定执行步骤(模板方法),具体实现步骤交给子类完成。
- 复用代码:子类复用基类中定义的通用逻辑,仅需实现特定步骤。
- 遵循开闭原则:基类的骨架逻辑对扩展开放,对修改关闭。
一般用在什么场景?
- 定义算法骨架:有一个固定的流程,但某些步骤需要根据具体情况自定义。
- 复用公共逻辑:多个子类共享相同的算法结构,仅需重写特定步骤。
- 控制执行顺序:需要对子类执行方法的顺序进行控制时。
典型场景:
- 数据处理流程(如读取数据、处理数据、输出结果)。
- Web请求处理(如解析请求、处理逻辑、返回响应)。
在Java中有很多应用场景,例如JdbcTemplate就是使用了模板方法来处理数据库的操作。
再比如HttpServlet类的service方法也用了模板方法,doGet、doPost等方法都是需要子类实现的。
(5)什么是观察者模式?一般用在什么场景?
观察者模式(发布订阅模式)是一种行为型设计模式,用于定义对象之间的一种一对多的依赖关系,使得一个对象状态发生变化时,所有依赖它的对象都会收到通知并自动更新。
它的目的就是将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。
观察者模式的特点:
- 松耦合:观察者和被观察者之间是松耦合的,便于扩展和维护。
- 动态订阅:可以动态添加或移除观察者,灵活性高。
- 单向通信:被观察者通知观察者,观察者不能反向修改被观察者的状态。
一般用在什么场景?
- 事件驱动系统:在用户操作界面中,通过监听事件(如按钮点击)触发响应。
- 系统间通信:系统中某个模块发生变化时,需要通知多个依赖模块。
- 分布式系统:数据更新时通知所有订阅者,例如推送通知、实时数据同步。
典型场景:
- GUI事件处理系统(如按钮点击、窗口关闭事件)。
- 数据模型与视图同步更新(如MVC架构中的数据绑定)。
- 股票价格更新通知订阅者。
(6)什么是代理模式?一般用在什么场景?
代理模式(Proxy Pattern)是一种结构型设计模式,在不改变原始对象的前提下,通过引入一个代理对象来控制对原始对象的访问,实现额外的功能。例如控制权限、延迟加载、缓存等。
这个模式在我们业务中太常见了,例如动态代理就是代理模式,Spring AOP就是动态代理,RPC框架也是使用了动态代理才使得调用远程方法和本地方法一样。
所以,统一报错、监控、限流、鉴权等等,需要跟业务解耦的功能,我们基本上都是使用代理类进行统一处理的。
代理模式的特点:
- 间接访问:客户端通过代理访问实际对象,代理对象负责对实际对象的控制。
- 功能增强:代理对象可以在访问实际对象之前或之后添加额外的功能。
- 解耦性:客户端不直接与实际对象交互,通过代理对象可以透明地扩展实际对象的功能。
一般用在什么场景?
- 访问控制:对实际对象的访问需要进行权限验证时。
- 性能优化:通过代理实现缓存或延迟加载以提高系统性能。
- 远程访问(rpc调用):客户端需要访问远程对象时,通过代理封装远程调用的细节。
- 日志记录:在方法调用时添加日志记录功能。
典型场景:
- 远程代理:为远程对象提供本地代表。
- 权限代理:控制对对象的访问权限。
- 智能引用:在访问对象时增加一些附加行为,如引用计数、日志记录等。
(7)请描述简单工厂模式的工作原理。
简单工厂模式(Simple Factory Pattern)不属于GoF23种经典设计模式之一,但是在实际开发中非常常见,它的作用是利用一个工厂作为入口,将多个对象的创建和使用分离。它根据传入的参数来创建并返回不同类型的对象实例。
来看下具体的代码实现就很清晰了:
// 产品接口
public interface Product {
void use();
}
// 具体产品A
public class ConcreteProductA implements Product {
@Override
public void use() {
System.out.println("Using ConcreteProductA");
}
}
// 具体产品B
public class ConcreteProductB implements Product {
@Override
public void use() {
System.out.println("Using ConcreteProductB");
}
}
// 简单工厂类
public class SimpleFactory {
public static Product createProduct(String type) {
switch (type) {
case "A":
return new ConcreteProductA();
case "B":
return new ConcreteProductB();
default:
throw new IllegalArgumentException("Unknown product type");
}
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Product productA = SimpleFactory.createProduct("A");
productA.use(); // Output: Using ConcreteProductA
Product productB = SimpleFactory.createProduct("B");
productB.use(); // Output: Using ConcreteProductB
}
}
优点:
- 将对象的创建集中在工厂类中,便于管理和维护
- 屏蔽了底层的创建细节,对使用者来说更方便
缺点:
- 如果一个工厂类集中所有产品的创建逻辑,那么职责过重,不符合单一职责原则
- 新增一个产品类型,工厂类都需要被修改,不符合开闭原则
(8)工厂模式和抽象工厂模式有什么区别?
工厂模式关注的是创建单一类型对象,定义一个抽象方法,由子类实现具体对象的实例化。
抽象工厂模式关注的是创建一组相关对象,提供一个接口来创建一组相关的或互相依赖的对象,而无需指定它们的具体类。
(9)什么是设计模式?请简述其作用。
设计模式其实是在软件开发过程中经过经验积累和验证总结得出的一套通用代码设计方案,是踩在巨人的肩膀上总结的设计成果。
如果熟悉了设计模式,当遇到类似的场景,我们可以快速地参考设计模式实现代码。不仅可以加速我们的编码速度,也提升了代码的可扩展性、可重用性与可维护性!
设计模式的作用是什么?
- 加速开发: 当遇到类似的问题时,开发者可以快速参考已有的设计模式来构建代码,从而加快开发速度。
- 提升代码质量:
- 可扩展性:设计模式使得代码更容易在未来添加新功能而不会影响现有功能。
- 可重用性:通过应用设计模式,可以将通用的设计思想提取出来,方便在不同的项目或模块中复用。
- 可维护性:遵循设计模式的代码结构更清晰,更容易理解、修改和维护。
- 促进沟通:设计模式提供了一套通用的术语和概念,使得团队成员之间能够更有效地沟通和交流关于代码设计的思想。
(10)单例模式有哪几种实现?如何保证线程安全?
单例模式的常见实现包括饿汉式、懒汉式、静态内部类和枚举。饿汉式和枚举天然线程安全。懒汉式需要在特定方法中添加同步机制,如同步方法或双重检查锁定(DCL)来保证线程安全。
1. 饿汉式 (Hungry Singleton)
- 实现方式:在类加载时就创建单例实例,通常使用静态常量或静态代码块。
- 线程安全:天生线程安全,因为实例创建发生在类加载过程中,不存在并发问题。
- 优点:简单,线程安全。
- 缺点:如果单例在初始化时较重,或未被使用,会造成资源浪费。
2. 懒汉式 (Lazy Singleton)
- 实现方式:实例在首次被需要时才创建,即在 getInstance() 方法中按需实例化。
- 线程安全:
- 非线程安全:直接的懒汉式实现没有加锁,在多线程环境下可能创建多个实例。
- 线程安全版:
- 同步方法:将 getInstance() 方法加上 synchronized 关键字,保证了线程安全,但会牺牲性能,因为每次调用都需要加锁。
- 双重检查锁定 (DCL):在 getInstance() 方法中,先检查实例是否已存在,不存在再加锁创建。这种方式性能较好,但需要注意对内存可见性的处理,否则仍可能存在问题。
- 优点:节约资源,实现了懒加载。
- 缺点:线程安全版本实现相对复杂,可能影响性能。
3. 静态内部类 (Static Inner Class)
- 实现方式:利用 Java 虚拟机的类加载机制,将单例实例的创建放在一个私有的静态内部类中。
- 线程安全:单例实例的创建由 JVM 在第一次被访问时完成,保证了线程安全。
- 优点:实现了懒加载,并且线程安全,占用内存比饿汉式少。
4. 枚举 (Enum Singleton)
- 实现方式:使用 Java 的枚举类型来定义单例。
- 线程安全:枚举类型由 JVM 从根本上保证线程安全。
- 优点:代码简洁,线程安全,并且能有效防止反序列化破解,是最完美的设计方式之一。
(11)Netty 采用了哪些设计模式?
-
责任链模式
概念:责任链模式(Chain of Responsibility)用于将请求沿着处理链传播,每个对象都有机会处理请求或将其传递给下一个对象。
Netty中的应用:Netty 的 ChannelPipeline 和 ChannelHandler 正是责任链模式的经典实现。ChannelPipeline 是一组互相连接的 ChannelHandler 对象,每个 ChannelHandler 执行对数据流的处理。
实现案例:
- 在Netty中,ChannelPipeline提供了一组按顺序工作的ChannelHandler,可分为入站(inbound)和出站(outbound)。
- 当接收到数据时,它会沿入站处理链传播,各种入站ChannelHandler依次处理该数据(如解码、业务逻辑处理等)。
- 当发送数据时,它会沿出站处理链传播,各个出站ChannelHandler依次处理该数据(如编码、压缩等)。
代码示例:
ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new DecoderHandler()); pipeline.addLast(new BusinessLogicHandler()); pipeline.addLast(new EncoderHandler());每个处理器都会处理关系的部分,并将其余的事件交给链内的下一个处理器。
-
观察者模式
概念:观察者(Observer)定义了对象之间的一对多依赖关系,当目标对象状态发生改变时,其观察者会收到通知并自动更新。
Netty中的应用:Netty的事件驱动模型通过观察者模式实现。当Selector检测到待定事件(如read或write准备完成)后会通过对应的Channel。Channel会触发事件并将任务提交到合适的处理器执行。
实现案例:
- ChannelFuture是Netty中观察者模式的典型应用。例如,当你向服务器发送数据时,可以通过ChannelFuture注册监听器,来监控数据发送是否完成。
- 当操作完成时,监听器会被通知从而执行用户定义的回调逻辑。
代码示例:
ChannelFuture future = channel.writeAndFlush(message); future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { System.out.println("Write successful!"); } else { System.out.println("Write failed: " + future.cause()); } } });ChannelFutureListener是典型的观察者,当ChannelFuture的状态变化时会受到通知。
-
工厂模式
概念:工厂模式用于创建对象的实例,屏蔽了对象创建的复杂性。
Netty中的应用:Netty使用工厂模式隐藏了创建复杂对象的细节,常见的是EventLoopGroup和Bootstrap等组件。
实现案例:
- EventLoopGroup是执行事件循环的关键组件,Netty提供了多种实现(如NIO的NioEventLoopGroup和epoll的EpollEventLoopGroup),用户可以通过抽象工厂模式指定自己需要的实现。
- Bootstrap和ServerBootstrap也是工厂模式的经典应用,它们用于构造客户端和服务端配置。
代码示例:
EventLoopGroup group = new NioEventLoopGroup(); Bootstrap bootsrtap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>()) { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MyHandler()); } });用户只需要调用 Bootstrap 的方法即可完成工厂的配置,隐藏了复杂的配置逻辑。
-
适配器模式
概念:适配器模式(Adapter)用来将一个类的接口转换为另一个接口,以实现接口之间的兼容。
Netty中的应用:Netty的ChannelHandlerAdapter和ChannelInBoundHandlerAdapter是典型的适配器模式应用,它们简化了ChannelHandler的实现。
实现案例:- Netty的ChannelHandler提供了很多接口方法,但用户可能只需要实现一小部分逻辑。在这种情况下,用户无需全部实现素有方法,可以继承ChannelInboundHandlerAdapter或ChannelOutboundHandlerAdapter来简化代码。
代码示例:
public class MyHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("Message received: " + msg); ctx.fireChannelRead(msg); } } -
策略模式
概念:策略模式将一组算法封装起来,使得它们可以互换,同时将算法的选择独立于使用这些算法的客户。
Netty中的应用:Netty在其EventLoopGroup和处理IO的任务分配中采用了策略模式。通过抽象的 EventLoop,Netty 支持多种不同的多路复用机制(如 NIO、epoll 等)。
实现案例:- EventLoopGroup支持多种实现,并根据运行环境动态选择策略,例如在Linux平台优先选择epoll。
- Netty的序列化与解码器也使用了策略模式,不同的序列化方式可以互相替换(如Protobuf、JSON等)。
代码示例:
EventLoopGroup group = new EpoollEventLoopGroup();// Linux 平台下的高性能实现 Boostrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(EpollSocketChannel.class);用户可以灵活替换实现以适配特定需求。
-
原型模式
概念:原型模式 (Prototype)通过克隆的方式创建对象,而不是直接实例化。

Netty中的应用:Netty的缓冲区分配(ByteBufAllocator)中使用了原型模式。为了减少内存分配和回收的开销,Netty提供了池化的缓冲区,通过克隆和回收来重复利用缓冲区。
实现案例:
- PooledByteBufAllocator负责提供缓冲区,其内部维护了一系列固定大小的内存块,用于内存分配和回收。
- 使用原型模式减少了频繁的内存分配成本。
-
单例模式
概念:单例模式(Singleton)保证一个类只存在一个实例,并提供全局访问点。

Netty中的应用:Netty中某些共享的组件采用单例模式,例如Unpooled类和一些内部工具类。实现案例:
- Unpooled是非池化缓冲区的工厂类,它使用单例模式提供缓冲区操作的统一入口。
代码示例:
ByteBuf buf = Unpooled.buffer(256); -
模板方法模式
概念:模板方法模式允许在积累定义操作的框架,而将具体实现延迟到子类。

Netty中的应用:Netty的很多组件都提供了模板方法模式的实现,例如ChannelInitializer用于设置ChannelPipeline。实现案例:
- 用户通过继承ChannelInitializer定义自己的逻辑,而底层框架负责调用和执行。
代码示例:
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MyHandler()); } }
更多推荐


所有评论(0)