Lesson 47 · Java 高级特性

面试必会设计模式:单例、工厂、策略、观察者、模板方法

中级·⭐ 必问·#设计模式·#面试

第 1 站

开场:面试官到底想听什么

"请说说你熟悉的设计模式"——这道题考的不是定义,而是你有没有在真实项目中用过。

面试官真正在意的是三件事:

  1. 识别能力——你能在 JDK / Spring 源码里找到模式的影子;
  2. 决策能力——你知道什么时候该用、什么时候不该用;
  3. 表达精度——用最短的语言把结构、角色、优缺点讲清楚。

本文挑选面试中出现频率最高的 5 种模式,每种都会给出:核心结构 → 代码实现 → JDK/Spring 中的真实应用。背完这篇,设计模式题基本不会翻车。

面试核心

模式 ≠ 背定义。面试官要的是"我在源码里见过它"+"我知道为什么这么设计"。

第 2 站

单例模式 (Singleton)

保证一个类只有一个实例,并提供全局访问点。面试必问"5 种写法"和"Spring 单例与 GoF 单例的区别"。

单例模式 5 种写法对比 饿汉式 线程安全 懒汉式 需 synchronized DCL 双重检查锁定 静态内部类 延迟+安全 枚举 防反射+序列化 推荐:静态内部类(通用)/ 枚举(需防反射)
图 1 单例模式 5 种实现方式速览

1. 饿汉式

类加载时初始化,天然线程安全
public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

2. 懒汉式(synchronized)

简单但性能差,每次获取都加锁
public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}
    public static synchronized LazySingleton getInstance() {
        if (instance == null) instance = new LazySingleton();
        return instance;
    }
}

3. 双重检查锁定(DCL)

volatile + 双重 null 检查,面试高频写法
public class DCLSingleton {
    private static volatile DCLSingleton instance;  // volatile 禁止指令重排
    private DCLSingleton() {}
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null)
                    instance = new DCLSingleton();
            }
        }
        return instance;
    }
}
面试追问

"为什么 volatile 不能省?"—— new 操作分三步:分配内存 → 初始化对象 → 引用赋值。没有 volatile,第 2、3 步可能重排,导致其他线程拿到半初始化对象。

4. 静态内部类

利用类加载机制保证线程安全,延迟加载
public class StaticInnerSingleton {
    private StaticInnerSingleton() {}
    private static class Holder {
        static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }
    public static StaticInnerSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

5. 枚举

Effective Java 推荐,天然防反射和反序列化攻击
public enum EnumSingleton {
    INSTANCE;
    public void doSomething() { /* 业务逻辑 */ }
}

Spring 的 singleton scope 和 GoF 单例是一回事吗?

不是。GoF 单例是 JVM 层面保证一个类只有一个对象;Spring singleton scope 是 IoC 容器层面,保证同一个 BeanDefinition 只创建一个 Bean 实例。同一个类注册两个不同 id 的 Bean,会得到两个不同的对象。

对比项GoF 单例Spring Singleton Scope
作用范围JVM 级别ApplicationContext 级别
实现方式private 构造 + static 字段ConcurrentHashMap 缓存
同类多实例不可能注册不同 Bean name 即可
线程安全需要自己保证Spring 只保证唯一性,不保证状态安全
面试答案模板

"单例有 5 种写法——饿汉、懒汉、DCL、静态内部类、枚举。生产环境推荐静态内部类或枚举。Spring 的 singleton scope 是容器级别的单例,跟 GoF 单例不是一个概念。Spring 内部用 ConcurrentHashMap 缓存 Bean,DefaultSingletonBeanRegistry.getSingleton() 就是它的实现。"

第 3 站

工厂模式 (Factory)

工厂模式有三个层次:简单工厂 → 工厂方法 → 抽象工厂,面试中经常要求"用 JDK 或 Spring 的例子说明区别"。

工厂模式三阶段演进 简单工厂 一个工厂,switch 决定 违反 OCP 工厂方法 每个产品一个工厂类 符合 OCP 抽象工厂 产品族,多产品等级 JDBC / GUI 工具包 JDK: Calendar.getInstance() / Spring: BeanFactory / Logger.getLogger()
图 2 工厂模式的三个层次

简单工厂

一个工厂类根据参数创建不同产品
public class PaymentFactory {
    public static Payment create(String type) {
        switch (type) {
            case "alipay":  return new AlipayPayment();
            case "wechat":  return new WechatPayment();
            default: throw new IllegalArgumentException(type);
        }
    }
}

JDK 真实案例:java.util.Calendar.getInstance()——根据 Locale 和 TimeZone 返回不同的 Calendar 子类(GregorianCalendarBuddhistCalendar 等),是经典的简单工厂。

工厂方法

为每个产品定义独立的工厂类,符合开闭原则。新增产品只需新增工厂,不用改已有代码。

JDK 真实案例:

  • java.util.logging.Logger.getLogger(name)——工厂方法,子类可以覆盖创建逻辑
  • java.net.URLStreamHandlerFactory——接口 + 工厂方法,URL 根据协议找到对应 Handler

抽象工厂

创建一系列相关对象(产品族)。JDBC 就是经典案例:ConnectionStatementResultSet 都由对应数据库厂商的工厂统一创建。

Spring BeanFactory 的工厂方法模式
// Spring IoC 容器本身就是最大的工厂
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = ctx.getBean(UserService.class);

// BeanFactory 接口定义了工厂方法的抽象
public interface BeanFactory {
    Object getBean(String name);       // 按名称获取
    <T> T getBean(Class<T> type);    // 按类型获取
}
面试追问

"Spring 的 FactoryBean 和 BeanFactory 有什么区别?"—— BeanFactory 是 IoC 容器本身,管理所有 Bean 的生命周期;FactoryBean 是一个特殊 Bean,通过 getObject() 方法自定义创建逻辑,常用于代理对象(如 MyBatis 的 Mapper 接口就是通过 FactoryBean 创建的)。

面试答案模板

"工厂模式分三层——简单工厂用 switch 分发,违反 OCP;工厂方法为每个产品定义独立工厂;抽象工厂创建产品族。JDK 的 Calendar.getInstance() 是简单工厂,Logger.getLogger() 是工厂方法,JDBC 是抽象工厂。Spring 的 BeanFactory 是工厂方法模式的极致应用,整个 IoC 容器就是一个大工厂。"

第 4 站

策略模式 (Strategy)

定义一系列算法,把它们封装起来并使它们可互换。消除大量的 if-else / switch,是重构面试中最常被提到的模式。

策略模式结构 Context 持有策略引用 uses Strategy 接口/抽象类 策略 A 策略 B 策略 C JDK / Spring Comparator ThreadPoolExecutor RejectedExecution HandlerMapping
图 3 策略模式结构与 JDK/Spring 应用
JDK Comparator —— 最经典的策略模式
// Comparator 就是策略接口,不同排序规则就是不同策略实现
List<Student> students = getStudents();

// 策略 A:按成绩排序
students.sort(Comparator.comparing(Student::getScore).reversed());

// 策略 B:按年龄排序
students.sort(Comparator.comparingInt(Student::getAge));

// Collections.sort(list, comparator) 中,Context 是 sort 方法
// comparator 参数就是可替换的策略
ThreadPoolExecutor 的拒绝策略
// RejectedExecutionHandler 是策略接口
new ThreadPoolExecutor(coreSize, maxSize, keepAlive, TimeUnit.SECONDS,
    queue,
    new ThreadPoolExecutor.CallerRunsPolicy()  // 可换成 4 种内置策略
);
// 内置策略:AbortPolicy | CallerRunsPolicy | DiscardPolicy | DiscardOldestPolicy
Spring HandlerMapping —— MVC 请求分发的策略模式
// Spring MVC 中 DispatcherServlet 持有多个 HandlerMapping 实现
// 每个 HandlerMapping 就是一种"找到 Handler"的策略:
//   RequestMappingHandlerMapping  → @RequestMapping 注解
//   BeanNameUrlHandlerMapping    → Bean 名称以 / 开头
//   SimpleUrlHandlerMapping      → 显式 URL → Handler 映射
// DispatcherServlet 遍历所有 HandlerMapping,第一个匹配的就处理请求
面试追问

"策略模式怎么消除 if-else?"——用 Map<String, Strategy> 或者 Spring 的 @Autowired List<Strategy> 注入所有实现,通过标识匹配。配合 Spring 使用时,策略 Bean 自动注册,新增策略只需加一个类,完全符合 OCP。

策略模式 = 接口 + N 个实现 + Context 持有引用 → 运行时切换算法
面试答案模板

"策略模式定义一系列可互换的算法。JDK 的 Comparator 是最经典的例子,Collections.sort() 是 Context,不同 Comparator 实现就是不同策略。线程池的 RejectedExecutionHandler 也是策略模式。Spring MVC 的 HandlerMapping 用策略模式实现请求路由,DispatcherServlet 遍历所有 HandlerMapping 找到匹配的 Handler。"

第 5 站

观察者模式 (Observer)

定义对象间一对多的依赖关系:当一个对象状态改变时,所有依赖者都会收到通知。也叫发布-订阅模式。

观察者模式流程 Subject 状态变更 notify Observer A Observer B Observer C 真实应用 JDK: Observer / Observable Spring: ApplicationEvent Guava: EventBus JDK Observer 已废弃(Java 9+),推荐 Flow API / Spring Event
图 4 观察者模式与主流框架应用

JDK 内置(已废弃)→ Spring 事件机制

JDK 的 java.util.ObserverObservable 在 Java 9 被标记为 @Deprecated,原因是 Observable 的方法不是线程安全的,且它继承方式限制了灵活性。现代 Java 推荐 Spring 事件机制。

Spring ApplicationEvent 完整示例
// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
    private final String orderId;
    public OrderCreatedEvent(Object source, String orderId) {
        super(source);
        this.orderId = orderId;
    }
    public String getOrderId() { return orderId; }
}

// 2. 发布者——注入 ApplicationEventPublisher
@Service
public class OrderService {
    @Autowired private ApplicationEventPublisher publisher;

    public void createOrder(String orderId) {
        // ... 业务逻辑
        publisher.publishEvent(new OrderCreatedEvent(this, orderId));
    }
}

// 3. 监听者
@Component
public class InventoryListener {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // 扣减库存
        log.info("扣减库存: {}", event.getOrderId());
    }
}

// 4. 异步监听(新线程执行)
@Component
public class NotifyListener {
    @Async
    @EventListener
    public void sendNotification(OrderCreatedEvent event) {
        // 发送通知,不影响主流程
    }
}
Guava EventBus 示例
EventBus bus = new EventBus("order-bus");

// 注册监听者
bus.register(new Object() {
    @Subscribe
    public void onOrder(OrderCreatedEvent event) {
        // 处理事件
    }
});

// 发布事件
bus.post(new OrderCreatedEvent("ORD-001"));

Spring 事件默认是同步还是异步?

默认同步。监听者和发布者在同一个线程执行。要异步需要加 @Async 注解并启用 @EnableAsync。同步事件适合事务内操作(监听者共享事务),异步事件适合发通知、写日志等不影响主流程的操作。

面试答案模板

"观察者模式定义一对多依赖,Subject 状态变化时通知所有 Observer。JDK 的 Observer 在 Java 9 废弃了,现在主流用 Spring 的 ApplicationEvent 机制:定义事件类继承 ApplicationEvent,用 ApplicationEventPublisher 发布,用 @EventListener 监听。加 @Async 可以异步处理。Guava 的 EventBus 是轻量替代方案。"

第 6 站

模板方法模式 (Template Method)

在父类中定义算法骨架,将某些步骤延迟到子类实现。子类可以在不改变算法结构的情况下重新定义某些步骤。

模板方法:Spring JdbcTemplate 执行流程 1. 获取连接 DataSourceUtils 2. 创建语句 PreparedStatement 3. 执行回调 4. 处理结果 ResultSetExtractor RowMapper (用户自定义部分) 步骤 1~3, 5~6 是固定模板 步骤 4 是用户扩展点 模板方法应用 JdbcTemplate RestTemplate HttpServlet AbstractList JUnit setUp/tearDown Spring Bean 生命周期
图 5 模板方法模式与 JdbcTemplate 执行流程

JDK 经典案例:HttpServlet

HttpServlet.service() 是模板方法
// HttpServlet.service() 定义了算法骨架
protected void service(HttpServletRequest req, HttpServletResponse resp) {
    String method = req.getMethod();
    if (method.equals("GET"))  doGet(req, resp);      // ← 子类实现
    if (method.equals("POST")) doPost(req, resp);     // ← 子类实现
    // ... PUT, DELETE, etc.
}
// doGet() / doPost() 默认返回 405,子类覆盖即可

Spring JdbcTemplate

JdbcTemplate.query() 封装了 JDBC 全部样板代码
// JdbcTemplate 内部 execute() 的简化逻辑(模板方法)
public <T> T execute(ConnectionCallback<T> action) {
    Connection con = DataSourceUtils.getConnection(dataSource);  // 步骤 1
    try {
        return action.doInConnection(con);  // 步骤 2:回调(用户代码)
    }
    catch (SQLException ex) {
        // 步骤 3:异常转换
        throw getExceptionTranslator().translate(ex);
    }
    finally {
        DataSourceUtils.releaseConnection(con, dataSource);  // 步骤 4
    }
}

// 用户只需关心业务逻辑
List<User> users = jdbcTemplate.query(
    "SELECT * FROM user WHERE age > ?",
    new Object[]{18},
    new BeanPropertyRowMapper<>(User.class)
);

Spring RestTemplate

RestTemplate 同样是模板方法
// RestTemplate.execute() 封装了 HTTP 调用全流程
// 1. 创建 ClientHttpRequest(选择底层 HttpClient)
// 2. 设置请求头、请求体(HttpMessageConverter)
// 3. 执行请求
// 4. ResponseExtractor 处理响应(用户自定义)
// 5. 关闭连接、错误处理

// 用户只需关注结果处理
User user = restTemplate.getForObject(
    "http://api/users/{id}", User.class, 123
);

模板方法和策略模式有什么区别?

模板方法用继承,子类重写父类的某些步骤,算法骨架在父类中固定;策略模式用组合,通过持有不同的策略对象在运行时切换算法。模板方法是"白箱复用"(子类知道父类结构),策略模式是"黑箱复用"(只依赖接口)。

面试追问

"Spring Bean 生命周期中哪些是模板方法?"—— AbstractApplicationContext.refresh() 就是模板方法。它定义了 12 步标准流程(prepareRefresh → obtainFreshBeanFactory → invokeBeanFactoryPostProcessors → ... → finishRefresh),每个子类(如 AnnotationConfigApplicationContext)只需实现个别步骤。

面试答案模板

"模板方法在父类定义算法骨架,子类实现具体步骤。HttpServlet.service() 是经典 JDK 案例。Spring 的 JdbcTemplate 封装了获取连接、创建语句、异常转换、关闭连接的全部样板代码,用户只需提供 SQL 和结果映射。RestTemplate、AbstractApplicationContext.refresh() 也是模板方法。它和策略模式的区别:模板方法用继承固定骨架,策略模式用组合在运行时切换。"

第 7 站

总结:速查表与面试锦囊

模式 一句话 JDK 实例 Spring 实例 面试高频追问
单例 全局唯一实例 Runtime DefaultSingletonBeanRegistry DCL 为何要 volatile?Spring 单例 vs GoF 单例?
工厂 封装对象创建 Calendar.getInstance() BeanFactory / FactoryBean 简单工厂 vs 工厂方法?FactoryBean 用途?
策略 算法可替换 Comparator HandlerMapping 如何消除 if-else?策略模式 vs 状态模式?
观察者 一对多通知 Observer(已废弃) ApplicationEvent + @EventListener 同步 vs 异步?事件传播机制?
模板方法 算法骨架+子类扩展 HttpServlet JdbcTemplate / RestTemplate 模板方法 vs 策略模式?Bean 生命周期中的模板方法?

面试回答三板斧

  1. 说结构:用一句话概括模式的角色和关系("策略 = 接口 + N 实现 + Context")
  2. 举例子:至少给一个 JDK 或 Spring 的真实案例("JDK 的 Comparator 就是策略模式")
  3. 讲细节:补充实现细节或源码分析("DCL 必须用 volatile 防止指令重排")
最后提醒

面试中不需要把 5 种模式全讲一遍。面试官说"挑一个你熟悉的",就选一个讲透:结构 → 代码 → 框架应用 → 优缺点。如果面试官问"还有别的吗",再简短补充第二个。深度比广度重要。