面试官让我手写单例,我写了7种问他要哪种
2026/6/8 4:53:52 网站建设 项目流程

面试官让我手写单例,我写了7种问他要哪种

面试被问到设计模式,大部分人背概念——"单例保证全局唯一实例,工厂解耦创建逻辑,代理增强对象行为"。

面试官听完不会有任何感觉。因为这是标准答案,任何人都能背出来。

换个方式试试。

场景一:手写单例——写一个不算本事,写清楚为什么选这个才算

面试官:"写一个线程安全的单例。"

别上来就写 DCL(双重检查锁定)。先问一句:"您期望的是启动时初始化还是延迟初始化?"

这比手写代码更有说服力,因为你在考虑业务场景,不是背模板。

| 写法 | 初始化时机 | 线程安全 | 适用场景 | |------|-----------|---------|---------| | 饿汉式 | 类加载时 | JVM保证 | 创建成本低、一定会用到的对象 | | 静态内部类 | 首次访问时 | JVM保证 | 延迟加载但不想自己处理同步 | | DCL | 首次访问时 | volatile+synchronized | 延迟加载且构造中有复杂逻辑 | | 枚举 | 类加载时 | JVM保证 | 防反射、防序列化破坏 |

DCL 写法:

```java public class Singleton { private static volatile Singleton instance;

private Singleton() { if (instance != null) { throw new RuntimeException("反射攻击已被拦截"); } } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }

} ```

这里三个关键点:

  1. volatile防止指令重排——没有它,instance指向的内存可能还没初始化完就被另一个线程拿到
  2. 二次检查——多个线程同时过外层 if,第一个拿到锁的创建完,后面的进到内层 if 发现不为 null 直接退出
  3. 构造里防反射——getDeclaredConstructor().setAccessible(true)能绕过去,加个判断挡住

但面试官真正想听的可能是:"如果你的系统里单例是用 Spring 管理的,不需要自己写 DCL,Spring 的DefaultSingletonBeanRegistryConcurrentHashMap保证线程安全,并且在 Bean 销毁时处理生命周期。自己写的单例容易漏掉序列化破坏和反射破坏的问题。"

场景二:模板方法——不是"定义算法骨架",而是"别让我改核心流程"

面试官:"说说你在项目里怎么用模板方法的。"

别背书。说一个具体场景:消息推送。

你要对接短信、邮件、App 推送、微信模板消息。每种渠道的发送逻辑不一样,但流程都一样:校验内容→限流检查→发送→记录日志。

```java public abstract class MessageSender { public final SendResult send(Message msg) { validate(msg); // 子类覆写 if (!checkRateLimit(msg)) { // 基类统一实现 return SendResult.RATE_LIMITED; } SendResult result = doSend(msg); // 子类实现 logResult(msg, result); // 基类统一日志 return result; }

protected abstract void validate(Message msg); protected abstract SendResult doSend(Message msg); private boolean checkRateLimit(Message msg) { // 统一的限流逻辑 } private void logResult(Message msg, SendResult result) { // 统一的日志记录 }

} ```

关键不是"骨架和细节分离",而是让新增渠道的成本降到最低。后来老板说加个"钉钉消息推送",你只需要:

```java public class DingTalkSender extends MessageSender { @Override protected void validate(Message msg) { if (msg.getContent().length() > 5000) { throw new IllegalArgumentException("钉钉消息不能超过5000字符"); } }

@Override protected SendResult doSend(Message msg) { return dingTalkClient.send(msg); }

} ```

限流、日志这些公共逻辑完全不用管。这才是模板方法值钱的地方——不是代码少写几行,是新增场景时不出 bug。

场景三:策略+工厂——if-else 多到让你自己都烦的时候

面试官:"怎么消除过多的 if-else?"

"用策略模式"只是答对了一半。另一半是:你怎么把策略注册到工厂里?

三种方式:

方式一:手动注册

java Map<String, PaymentStrategy> strategyMap = new HashMap<>(); strategyMap.put("ALIPAY", new AlipayStrategy()); strategyMap.put("WECHAT", new WechatStrategy());

最简单,但每次加策略要改工厂代码,违反开闭原则。

方式二:Spring 自动注入

```java @Component public class PaymentFactory { private final Map strategyMap;

public PaymentFactory(List<PaymentStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toMap( s -> s.getClass().getAnnotation(PaymentType.class).value(), Function.identity() )); } public PaymentStrategy get(String type) { return strategyMap.get(type); }

} ```

加策略只需要写新类加注解,工厂不碰。这是大部分 Spring 项目的标准写法。

方式三:枚举策略

```java public enum PaymentMethod { ALIPAY { @Override public PayResult pay(Order order) { return alipayClient.execute(order); } }, WECHAT { @Override public PayResult pay(Order order) { return wechatClient.invoke(order); } };

public abstract PayResult pay(Order order);

} ```

适合策略数量固定且不频繁变动的场景。代码最紧凑,但扩展性最差——加策略要改枚举本身。

问到这一步,你可以再加一句:"实际项目里我用的是方式二,因为支付渠道是持续接入的。但如果只有两三种且永远不会再加,枚举就够了。设计模式没有银弹,看场景选。"

场景四:观察者——别让下单接口扛住所有后续逻辑

面试官:"下单完成后要发短信、减库存、送积分,你怎么设计?"

新手写法:在createOrder()方法末尾串行调用发短信、减库存、送积分。结果是下单接口响应时间越来越长。

Spring 事件机制:

```java // 发布事件 @Transactional public void createOrder(Order order) { orderDao.insert(order); eventPublisher.publishEvent(new OrderCreatedEvent(order)); }

// 监听者各自独立 @EventListener @Async // 异步执行,不阻塞下单 public void sendSms(OrderCreatedEvent event) { ... }

@EventListener @Transactional(propagation = Propagation.REQUIRES_NEW) public void deductInventory(OrderCreatedEvent event) { ... }

@EventListener @Async public void addPoints(OrderCreatedEvent event) { ... } ```

但要补充一个踩坑点:@EventListener默认是同步执行的。不加@Async的话,三个监听器会串行阻塞下单线程。以及事务边界——减库存失败不应该回滚整个下单,所以用REQUIRES_NEW独立事务。

这些都是代码跑了才知道的事,不是看图能看出来的。

面试真正考验的不是你会多少,是你踩过多少坑

设计模式这东西,过了入门阶段,比的不是谁背得全,是谁真的在项目里用过、遇到问题后重新理解了这些模式。

面试的时候,与其列一堆类图术语,不如讲一个你用策略模式重构 if-else 的真实案例。面试官听到具体代码,比听到概念要安心得多。

最近在玩一个小项目,用卡皮巴拉漫画讲设计模式,叫「爪爪代码冒险记」。里面每个模式都配了答题关,答对了才能进下一关。我写这些面试经验的时候也在想,要是有人面试前刷一遍题,至少聊起这些模式来不会只停留在"定义算法骨架"这种背诵层面。搜「爪爪代码冒险记」就能找到。

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

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

立即咨询