为什么设计模式是面试的核心?
在Java高级开发及架构师岗位的面试中,Java设计模式几乎是绕不开的话题。面试官不仅考察你是否知道某种模式的名称,更关注你是否理解其背后的设计思想,以及如何在实际项目中灵活运用。设计模式不仅是代码组织的艺术,更是解决复杂业务逻辑、降低系统耦合度、提高代码可维护性的关键手段。
许多开发者虽然在日常工作中使用了某种模式,却未能将其理论化。本页面旨在通过梳理高频考点,结合源码级代码示例,帮助你建立完整的设计模式知识体系。我们将重点探讨单例模式、工厂模式、观察者模式、策略模式和代理模式等在实际面试中出现频率最高的模式,并深入分析其优缺点及适用场景。
单例模式 (Singleton Pattern):面试的“敲门砖”
单例模式是Java面试中最基础也最常考的模式之一。它的核心目标是确保一个类只有一个实例,并提供一个全局访问点。虽然概念简单,但在多线程环境下的实现却大有学问。
1. 饿汉式 vs 懒汉式
饿汉式(Eager Initialization)在类加载时就创建实例,线程安全但可能浪费资源;懒汉式(Lazy Initialization)在第一次使用时创建实例,节省资源但需处理线程安全问题。
2. 双重检查锁定 (DCL)
这是面试中的高频追问点。通过 synchronized 关键字和 volatile 关键字的结合,实现既线程安全又高效的懒汉式单例。
public class Singleton {
// volatile 禁止指令重排,确保线程安全
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 枚举单例
Effective Java 作者推荐的方式。枚举类型天然线程安全,且能防止反序列化破坏单例,是防御性编程的最佳实践。
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
// 业务逻辑
}
}
✅ 优点
- 内存中只有一个实例,减少内存开销。
- 避免对资源的多重占用。
- 配置全局唯一,便于管理。
❌ 缺点
- 扩展困难,不支持继承。
- 职责过重,可能违背单一职责原则。
- 在多线程环境下需要小心处理同步。
工厂模式 (Factory Pattern):解耦的艺术
工厂模式定义了创建对象的接口,让子类决定实例化哪一个类。它将对象的创建与使用分离,极大地提高了系统的灵活性和可扩展性。在Spring框架中,BeanFactory 就是工厂模式的典型应用。
简单工厂模式 (Simple Factory)
由一个工厂类根据传入的参数,决定创建出哪一个产品类的实例。它不属于GoF 23种设计模式之一,但非常实用。
public class ProductFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ProductA();
} else if ("B".equals(type)) {
return new ProductB();
}
return null;
}
}
适用场景:工厂类负责创建的对象比较少,客户端只需要传入工厂类的参数,对于如何创建对象不关心。
工厂方法模式 (Factory Method)
定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法模式让类的实例化推迟到子类。
public interface ProductFactory {
Product createProduct();
}
public class ConcreteFactoryA implements ProductFactory {
@Override
public Product createProduct() {
return new ProductA();
}
}
优点:符合开闭原则,增加新产品无需修改现有代码,只需增加新的工厂类。
抽象工厂模式 (Abstract Factory)
为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。适用于有多个产品系列的系统。
经典案例:Java中的 javax.xml.parsers.DocumentBuilderFactory 就是抽象工厂的应用。
观察者模式 (Observer Pattern):事件驱动的核心
观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。在Java中,java.util.Observable 类(虽已废弃)和事件监听机制(如Swing、Spring Event)都是其典型应用。
核心组件
- Subject (主题): 维护观察者列表,提供添加、删除和通知观察者的方法。
- Observer (观察者): 定义更新接口,当主题状态改变时调用。
- ConcreteSubject (具体主题): 存储具体状态,状态改变时通知所有观察者。
- ConcreteObserver (具体观察者): 实现更新接口,处理来自主题的通知。
Java中的实际应用
在Spring框架中,ApplicationEventPublisher 和 @EventListener 注解极大地简化了观察者模式的实现。这种发布-订阅机制广泛应用于解耦业务逻辑,例如:用户注册成功后发送欢迎邮件、记录日志等。
| 特性 | 观察者模式 | 发布-订阅模式 |
|---|---|---|
| 依赖关系 | 观察者直接依赖主题 | 通过消息中间件解耦 |
| 触发机制 | 同步调用 | 异步消息传递 |
| 典型场景 | GUI事件处理、Spring Event | RabbitMQ、Kafka |
策略模式 (Strategy Pattern):算法的灵活切换
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。它完美体现了“开闭原则”和“依赖倒置原则”。
代码示例:支付策略
假设我们需要支持支付宝、微信支付和银联支付。使用策略模式可以避免大量的 if-else 判断。
// 策略接口
public interface PayStrategy {
void pay(double amount);
}
// 具体策略
public class AlipayStrategy implements PayStrategy {
@Override
public void pay(double amount) {
System.out.println("支付宝支付: " + amount);
}
}
// 上下文
public class PayContext {
private PayStrategy strategy;
public PayContext(PayStrategy strategy) {
this.strategy = strategy;
}
public void executePay(double amount) {
strategy.pay(amount);
}
}
面试陷阱:策略模式 vs 状态模式
许多开发者容易混淆这两种模式。策略模式侧重于算法的替换,客户端主动选择策略;而状态模式侧重于对象内部状态的改变导致行为的改变,状态转换通常由对象自身管理。在面试中,清晰地阐述这一区别是加分项。
代理模式 (Proxy Pattern):AOP的基石
代理模式为其他对象提供一种代理以控制对这个对象的访问。在Java中,代理模式主要分为静态代理和动态代理。Spring AOP 的核心就是动态代理。
1. 静态代理
代理类和目标类在编译期就确定好了。需要为每个目标类编写一个代理类,代码冗余度高。
2. 动态代理 (JDK & CGLIB)
在运行期动态生成代理类。JDK动态代理基于接口,要求目标类实现接口;CGLIB基于继承,通过字节码技术生成目标类的子类。Spring默认使用JDK动态代理,如果目标类没有接口,则自动切换为CGLIB。
// JDK动态代理示例
public class ProxyHandler implements InvocationHandler {
private Object target;
public ProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method");
Object result = method.invoke(target, args);
System.out.println("After method");
return result;
}
}
// 使用
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new ProxyHandler(target)
);
Java设计模式面试高频Q&A
以下是网友们在面试中最常遇到的问题,以及深度解答:
枚举单例不仅线程安全,而且能防止反射和反序列化攻击。反射可以通过 setAccessible(true) 破坏普通单例,反序列化可以生成新实例,而枚举类型天然禁止这些操作。
工厂方法模式针对的是一个产品等级结构(如:只生产手机),而抽象工厂模式针对的是多个产品等级结构(如:同时生产手机和电脑)。抽象工厂是工厂方法的升级。
可以将每个策略实现类注册为Spring Bean,然后通过 Map<String, Strategy> 注入到上下文中,根据key动态获取策略实现,避免大量的if-else。
代理模式主要控制对对象的访问(增强权限、延迟加载等),不改变对象的核心功能;装饰器模式旨在增强对象的功能,通常通过包装对象来增加新的行为。