Lesson 47 · Java 高级特性
面试必会设计模式:单例、工厂、策略、观察者、模板方法
开场:面试官到底想听什么
"请说说你熟悉的设计模式"——这道题考的不是定义,而是你有没有在真实项目中用过。
面试官真正在意的是三件事:
- 识别能力——你能在 JDK / Spring 源码里找到模式的影子;
- 决策能力——你知道什么时候该用、什么时候不该用;
- 表达精度——用最短的语言把结构、角色、优缺点讲清楚。
本文挑选面试中出现频率最高的 5 种模式,每种都会给出:核心结构 → 代码实现 → JDK/Spring 中的真实应用。背完这篇,设计模式题基本不会翻车。
模式 ≠ 背定义。面试官要的是"我在源码里见过它"+"我知道为什么这么设计"。
单例模式 (Singleton)
保证一个类只有一个实例,并提供全局访问点。面试必问"5 种写法"和"Spring 单例与 GoF 单例的区别"。
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)
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. 枚举
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() 就是它的实现。"
工厂模式 (Factory)
工厂模式有三个层次:简单工厂 → 工厂方法 → 抽象工厂,面试中经常要求"用 JDK 或 Spring 的例子说明区别"。
简单工厂
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 子类(GregorianCalendar、BuddhistCalendar 等),是经典的简单工厂。
工厂方法
为每个产品定义独立的工厂类,符合开闭原则。新增产品只需新增工厂,不用改已有代码。
JDK 真实案例:
java.util.logging.Logger.getLogger(name)——工厂方法,子类可以覆盖创建逻辑java.net.URLStreamHandlerFactory——接口 + 工厂方法,URL 根据协议找到对应 Handler
抽象工厂
创建一系列相关对象(产品族)。JDBC 就是经典案例:Connection、Statement、ResultSet 都由对应数据库厂商的工厂统一创建。
// 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 容器就是一个大工厂。"
策略模式 (Strategy)
定义一系列算法,把它们封装起来并使它们可互换。消除大量的 if-else / switch,是重构面试中最常被提到的模式。
// 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 参数就是可替换的策略
// RejectedExecutionHandler 是策略接口 new ThreadPoolExecutor(coreSize, maxSize, keepAlive, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.CallerRunsPolicy() // 可换成 4 种内置策略 ); // 内置策略:AbortPolicy | CallerRunsPolicy | DiscardPolicy | DiscardOldestPolicy
// 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。
"策略模式定义一系列可互换的算法。JDK 的 Comparator 是最经典的例子,Collections.sort() 是 Context,不同 Comparator 实现就是不同策略。线程池的 RejectedExecutionHandler 也是策略模式。Spring MVC 的 HandlerMapping 用策略模式实现请求路由,DispatcherServlet 遍历所有 HandlerMapping 找到匹配的 Handler。"
观察者模式 (Observer)
定义对象间一对多的依赖关系:当一个对象状态改变时,所有依赖者都会收到通知。也叫发布-订阅模式。
JDK 内置(已废弃)→ Spring 事件机制
JDK 的 java.util.Observer 和 Observable 在 Java 9 被标记为 @Deprecated,原因是 Observable 的方法不是线程安全的,且它继承方式限制了灵活性。现代 Java 推荐 Spring 事件机制。
// 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) { // 发送通知,不影响主流程 } }
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 是轻量替代方案。"
模板方法模式 (Template Method)
在父类中定义算法骨架,将某些步骤延迟到子类实现。子类可以在不改变算法结构的情况下重新定义某些步骤。
JDK 经典案例:HttpServlet
// 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 内部 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.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() 也是模板方法。它和策略模式的区别:模板方法用继承固定骨架,策略模式用组合在运行时切换。"
总结:速查表与面试锦囊
| 模式 | 一句话 | 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 生命周期中的模板方法? |
面试回答三板斧
- 说结构:用一句话概括模式的角色和关系("策略 = 接口 + N 实现 + Context")
- 举例子:至少给一个 JDK 或 Spring 的真实案例("JDK 的 Comparator 就是策略模式")
- 讲细节:补充实现细节或源码分析("DCL 必须用 volatile 防止指令重排")
面试中不需要把 5 种模式全讲一遍。面试官说"挑一个你熟悉的",就选一个讲透:结构 → 代码 → 框架应用 → 优缺点。如果面试官问"还有别的吗",再简短补充第二个。深度比广度重要。