第14章 设计模式入门:单例、工厂、建造者与代理

第14章 设计模式入门:单例、工厂、建造者与代理

本系列是《Java 高级用法》篇,承接《Java 基础入门》21 章的内容,面向已学完基础篇、想进阶的小白读者。这一章是"设计模式"的入门课:不追求背全 23 种模式,而是把最常用的 4 类讲透------单例、工厂、建造者、代理,顺带认识观察者模式。看完你能看懂框架源码里的一大半套路,面试也能聊出干货。

背景介绍 :设计模式(Design Pattern)是前辈们总结的"在特定场景下反复被验证的解决方案",相当于编程界的"菜谱"。它不是新语法,而是代码组织方式的经验总结。基础篇写的都是"一个类干一件事"的小程序,但真实项目动辄几十万行,怎么让类与类之间协作得优雅、可维护、可扩展?设计模式回答的就是这个问题。

本章目标:学完你能手写五种单例并说清各自优劣;能解释"为什么用工厂代替到处 new";能用 Builder 解决"构造器参数爆炸";能看懂动态代理是怎么回事(Spring AOP 的根基);面试被问"单例怎么保证线程安全""JDK 动态代理的原理""观察者和策略模式的区别"都能接住。

一、什么是设计模式,为什么要学

设计模式是"在特定场景下反复被验证的解决方案"。23 种经典模式按用途分成三大类:创建型 (怎么创建对象:单例、工厂、建造者)、结构型 (怎么组合类与对象:代理、装饰器)、行为型(怎么分配职责、怎么协作:观察者、策略)。本章主角是创建型 3 个 + 结构型 1 个(代理)+ 行为型 1 个(观察者)。

设计模式解决的典型痛点:代码重复、难以修改、到处 new 导致耦合(耦合指类与类之间依赖太深,改一个要动一片)。先看一个反面教材:

java 复制代码
// 反面教材:创建"形状"的逻辑散落在各处,每处都要写一遍 if-else
public class BadCode {
    public static void main(String[] args) {
        String shape = "circle";
        if ("circle".equals(shape)) {
            System.out.println("画一个圆");      // 画图功能里
        } else if ("square".equals(shape)) {
            System.out.println("画一个方形");
        }
        if ("circle".equals(shape)) {
            System.out.println("面积=πr²");      // 面积功能里又写一遍
        } else if ("square".equals(shape)) {
            System.out.println("面积=边长²");
        }
        // 如果再加一个 triangle(三角形),上面两处 if-else 都要改------漏改一处就出 bug
    }
}

为什么要学设计模式? ① 面试必考(单例、工厂是高频题);② 看懂框架源码:Spring、MyBatis 内部大量使用工厂、代理、观察者等模式;③ 让代码更好改:需求变化时只改"该改的地方"(本章会反复强调这个)。但要记住:模式是工具不是目的,小项目硬套模式反而更难看,第六节扩展知识会专门讲"不要过度设计"。

二、单例模式:全局只有一个实例

单例模式(Singleton)保证一个类在整个程序里只有一个对象,比如线程池、数据库连接池、配置文件读取器都只需要一份。核心三点:构造方法私有(外部不能 new)、自己保存唯一实例、提供全局访问入口。类比:一个公司只能有一个 CEO,所有人都通过"董事长办公室"(getInstance)找他,不能自己另立门户。

1. 饿汉式:类加载时就创建

java 复制代码
public class EagerSingleton {
    // 类加载时就创建好,天生线程安全(线程安全=多线程同时访问不出错)
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {}  // 构造方法私有,外部无法 new
    public static EagerSingleton getInstance() { return INSTANCE; }
}

// 使用:怎么调用都是同一个对象
EagerSingleton a = EagerSingleton.getInstance();
EagerSingleton b = EagerSingleton.getInstance();
System.out.println(a == b); // 输出:true(同一个实例,== 比较的是引用地址)

为什么叫"饿汉"? 不管用不用,先把实例"吃"到嘴里。优点:简单、线程安全(JVM 保证类加载只发生一次,static final 字段初始化线程安全);缺点:类一加载就占内存,对象很重而没用它时就浪费了。适用:对象不重、程序启动就要用(如配置读取器)。

2. 懒汉式:用的时候才创建

java 复制代码
public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if (instance == null) {          // 用的时候才创建
            instance = new LazySingleton();
        }
        return instance;
    }
}

为什么这个写法有坑? 两个线程同时第一次调用,可能都看到 instance == null 各自 new 一个------单例失效(产生两个对象)。这正是多线程的经典陷阱。用代码演示一下:

java 复制代码
// 演示懒汉式线程不安全:100 个线程同时首次调用 getInstance
// 需要 import java.util.*
final java.util.Set<LazySingleton> instances =
    java.util.Collections.synchronizedSet(new java.util.HashSet<>());
for (int i = 0; i < 100; i++) {
    new Thread(() -> instances.add(LazySingleton.getInstance())).start();
}
Thread.sleep(1000); // 等所有线程跑完(正式代码不要用 sleep 等线程,这里只是演示)
System.out.println("实例个数=" + instances.size());
// 输出:可能大于 1(例如 2、3......线程越多越容易触发;有时碰巧是 1,但"有时对"不等于"安全")

为什么可能得到多个实例? instance = new LazySingleton() 不是一步:判断 null、创建对象、赋值。线程 A 判断完 null 还没赋值,线程 B 也判断 null 为真,两个线程各 new 一个------后赋值的覆盖先赋值的,先创建的那个对象就"丢了",但确实产生了两个对象。

3. 双重检查锁定:既懒加载又线程安全

java 复制代码
public class DoubleCheckSingleton {
    // volatile:保证多线程下 instance 的可见性(一个线程改了,别的线程立刻能看到)
    private static volatile DoubleCheckSingleton instance;
    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {                                // 第一次检查:避免每次都加锁
            synchronized (DoubleCheckSingleton.class) {        // 加锁,只让一个线程进来
                if (instance == null) {                        // 第二次检查:防止重复创建
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

为什么需要 volatile? instance = new Xxx() 底层不是一步:分配内存、初始化、赋引用。没有 volatile,另一个线程可能看到"还没初始化完"的半成品对象(指令重排导致"赋引用"先于"初始化"执行)。volatile 是这道题的考点,面试常问"为什么双重检查要加 volatile"------标准答案:防止指令重排,保证"看到 instance 非 null 时,它一定初始化完成"。

执行流程拆解(两个线程同时首次调用):线程 A 进锁创建实例;线程 B 第一次检查发现 null,等在锁外;A 释放锁,B 进锁做第二次检查------发现已经不是 null,直接返回 A 创建的实例。第二次检查就是"防重复创建"的关键。

4. 静态内部类:推荐的生产写法

java 复制代码
public class HolderSingleton {
    private HolderSingleton() {}
    // 静态内部类:只有被用到时 JVM 才加载它(JVM 加载类天然线程安全)
    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }
    public static HolderSingleton getInstance() { return Holder.INSTANCE; }
}

为什么推荐? 兼得饿汉的线程安全和懒汉的懒加载:Holder 不被调用就不加载;JVM 保证类加载只发生一次,天然线程安全还不用写锁。面试常问:"静态内部类和双重检查有什么区别?"------双重检查要写 volatile + synchronized,稍不注意就写错;静态内部类把"线程安全"交给 JVM 类加载机制,代码最不容易出错。

5. 枚举单例:最简洁也最安全

java 复制代码
public enum EnumSingleton {
    INSTANCE;  // 枚举天然只有一个实例
    public void doSomething() { System.out.println("枚举单例干活"); }
}

EnumSingleton.INSTANCE.doSomething(); // 输出:枚举单例干活

// 枚举单例也能带字段、带方法,和普通类一样用
public enum ConfigSingleton {
    INSTANCE;
    private String appName = "我的应用";
    public String getAppName() { return appName; }
    public void setAppName(String name) { this.appName = name; }
}
System.out.println(ConfigSingleton.INSTANCE.getAppName()); // 输出:我的应用

为什么枚举能当单例? JVM 保证枚举实例全局唯一,且天然防反射、防序列化破坏(第五节的扩展知识会演示"反射怎么破坏普通单例、却拿枚举没办法")。

五种写法对比(面试常考):

写法 线程安全 懒加载 防反射/序列化 缺点
饿汉式 ❌(类加载就建) 可能浪费内存
懒汉式 多线程失效
双重检查 细节多,易写错
静态内部类 写法稍绕
枚举 类加载时 灵活性稍差

总结:面试答"你会用哪种"------静态内部类或枚举;要谈"为什么"------饿汉简单但可能浪费内存;懒汉省内存但需处理线程;双重检查性能好但细节多;静态内部类优雅;枚举最安全。

三、工厂模式:把"创建对象"集中管理

工厂模式的核心思想:不直接 new,而是让专门的"工厂"负责创建对象。调用方只关心"我要圆",不关心"圆怎么造"。类比:你去餐厅点"宫保鸡丁",不用自己买菜、切菜、炒菜------后厨(工厂)负责,你只报菜名。

1. 简单工厂

java 复制代码
interface Shape { void draw(); }

class Circle implements Shape {
    public void draw() { System.out.println("画圆"); }
}
class Square implements Shape {
    public void draw() { System.out.println("画方形"); }
}

// 简单工厂:一个方法根据参数决定创建哪个对象
class ShapeFactory {
    public static Shape create(String type) {
        if ("circle".equals(type)) return new Circle();
        else if ("square".equals(type)) return new Square();
        throw new IllegalArgumentException("未知形状: " + type);  // 参数不合法:快速失败,而不是返回 null
    }
}

Shape s = ShapeFactory.create("circle");  // 调用方不再写 new
s.draw(); // 输出:画圆

// 边界:未知类型 -> 抛异常
try {
    ShapeFactory.create("triangle");
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage()); // 输出:未知形状: triangle
}

为什么好? 第一节反面教材里"散落的 if-else"被收拢到工厂一处,新增形状只改工厂,调用方完全不用动。缺点是:工厂里 if-else 越加越长(职责膨胀)------加一种形状就要改工厂,违反"开闭原则"。

2. 工厂方法:把"创建"延迟到子类

工厂方法把"创建对象"抽象成接口方法,由不同子类工厂各自实现------新增形状时不改旧代码、只加新类,符合"开闭原则"(对扩展开放、对修改关闭)。

java 复制代码
interface ShapeFactory2 { Shape create(); }

class CircleFactory implements ShapeFactory2 {
    public Shape create() { return new Circle(); }   // 只负责造圆
}
class SquareFactory implements ShapeFactory2 {
    public Shape create() { return new Square(); }   // 只负责造方形
}

ShapeFactory2 factory = new CircleFactory();  // 想换形状就换工厂
Shape s = factory.create();
s.draw(); // 输出:画圆

// 新增三角形:只加两个类(Triangle + TriangleFactory),旧代码一行不改

简单工厂 vs 工厂方法 :简单工厂一个类集中判断,代码少但违反开闭原则;工厂方法每个形状一个工厂类,扩展不动旧代码但类变多。小项目用简单工厂,大项目用工厂方法。面试常问:"简单工厂算设计模式吗?"------不算在 23 种经典模式里,它是工厂方法/抽象工厂的简化版,但面试可以拿来对比着讲。

3. 抽象工厂:生产"一族"相关的产品

抽象工厂解决"一组产品要成套出现"的问题:比如一套 UI 皮肤(深色)包含按钮、复选框、输入框,它们必须风格统一,不能深色按钮配浅色复选框。

java 复制代码
// 一族产品:按钮 + 复选框
interface Button { void render(); }
interface Checkbox { void render(); }
class DarkButton implements Button { public void render() { System.out.println("深色按钮"); } }
class DarkCheckbox implements Checkbox { public void render() { System.out.println("深色复选框"); } }
class LightButton implements Button { public void render() { System.out.println("浅色按钮"); } }
class LightCheckbox implements Checkbox { public void render() { System.out.println("浅色复选框"); } }

// 抽象工厂:声明"生产一族产品"的接口
interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}
class DarkFactory implements UIFactory {
    public Button createButton() { return new DarkButton(); }
    public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
class LightFactory implements UIFactory {
    public Button createButton() { return new LightButton(); }
    public Checkbox createCheckbox() { return new LightCheckbox(); }
}

UIFactory f = new DarkFactory();  // 一键切换整套皮肤
f.createButton().render();        // 输出:深色按钮
f.createCheckbox().render();      // 输出:深色复选框

UIFactory f2 = new LightFactory();  // 换皮肤只换工厂,调用代码不用改
f2.createButton().render();         // 输出:浅色按钮

为什么叫"抽象"工厂? 它把"工厂"本身也抽象成接口,客户端面对的是 UIFactory 接口而不是具体工厂------换皮肤 = 换工厂实现。真实场景 :Spring 的 BeanFactory 就是工厂接口,具体创建逻辑由 XmlBeanFactoryAnnotationConfigApplicationContext 等实现类各自负责。

四、建造者模式:一步一步"搭"出对象

建造者模式(Builder)适用于构造参数很多、且有很多可选参数的对象:把"构造过程"拆成一步步的方法调用,最后 build() 一次性产出对象。类比:组装电脑------主板、CPU、内存、显卡一步步选,最后装机师傅(build)一次性装好。

1. 你早就用过它:StringBuilder

java 复制代码
// StringBuilder 就是 Builder 的经典例子:一步步 append,最后 toString
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");  // 链式调用:每个 append 返回 this
System.out.println(sb.toString()); // 输出:Hello World

// 链式调用的秘密:append 返回 this,所以能一直 .append().append()
StringBuilder sb2 = new StringBuilder().append("A").append("B").append("C");
System.out.println(sb2); // 输出:ABC

为什么 StringBuilder 是 Builder? 它没有一次性传入所有内容,而是分步拼接,最后统一产出 String------正是"一步步搭、最后 build"的思想。

2. 自己写一个 PersonBuilder

java 复制代码
public class Person {
    private final String name;   // 必填
    private final int age;       // 必填
    private final String phone;  // 选填
    private final String email;  // 选填

    private Person(Builder b) {  // 构造私有,只能通过 Builder 创建
        this.name = b.name; this.age = b.age;
        this.phone = b.phone; this.email = b.email;
    }

    // 静态内部类 Builder:字段与 Person 一一对应
    public static class Builder {
        private final String name;   // 必填字段放构造器(不填根本编译不过)
        private final int age;
        private String phone = "无";  // 选填字段给默认值
        private String email = "无";
        public Builder(String name, int age) { this.name = name; this.age = age; }
        public Builder phone(String phone) { this.phone = phone; return this; }  // 返回 this 才能链式
        public Builder email(String email) { this.email = email; return this; }
        public Person build() {
            // 在 build() 里做参数校验:不满足就抛异常,而不是造出"半残"对象
            if (name == null || name.isEmpty()) {
                throw new IllegalArgumentException("姓名不能为空");
            }
            return new Person(this);
        }
    }

    public String toString() { return name + ", " + age + ", " + phone + ", " + email; }
}

Person p = new Person.Builder("小明", 20).phone("13800000000").build();
System.out.println(p); // 输出:小明, 20, 13800000000, 无(email 没设,用默认值"无")

// 边界:必填校验在 build 时触发
try {
    new Person.Builder("", 20).build();
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage()); // 输出:姓名不能为空
}

// 边界:链式顺序随意,可读性好
Person p2 = new Person.Builder("小红", 18).email("hong@test.com").phone("13900000000").build();
System.out.println(p2); // 输出:小红, 18, 13900000000, hong@test.com

为什么用 Builder 而不是构造器? 参数一多,直接写构造器就要写一大堆重载组合(构造器重载爆炸):2 个必填 + 2 个选填就有 2² 种组合要写重载,更别说 10 个字段。Builder 让你"想设哪个就调用哪个方法",可读性好,还能在 build() 里做参数校验。何时别用:只有 2、3 个参数时用 Builder 反而啰嗦,直接构造器最合适。

五、代理模式:找个"经纪人"替你办事

代理模式(Proxy):不直接访问目标对象,而是通过一个"代理"间接访问。代理可以在调用前后附加额外逻辑------日志、权限校验、性能统计------目标类完全不用改。类比:明星不直接接商演,经纪人(代理)负责谈价格、签合同(附加逻辑),明星只管上台(核心业务)。

1. 静态代理:手动写一个代理类

java 复制代码
interface UserService { void addUser(String name); }

// 目标类:只关心业务
class UserServiceImpl implements UserService {
    public void addUser(String name) { System.out.println("添加用户: " + name); }
}

// 静态代理:和目标实现同一个接口,包一层
class UserServiceProxy implements UserService {
    private final UserService target;
    public UserServiceProxy(UserService target) { this.target = target; }

    public void addUser(String name) {
        System.out.println("[日志] 开始添加用户");  // 附加逻辑
        target.addUser(name);                       // 调用真实业务
        System.out.println("[日志] 添加完成");      // 附加逻辑
    }
}

UserService proxy = new UserServiceProxy(new UserServiceImpl());
proxy.addUser("小明");
// 输出:[日志] 开始添加用户 / 添加用户: 小明 / [日志] 添加完成

为什么用代理? 日志直接写进业务类会每个方法重复;代理把"日志"和"业务"分开------业务类保持纯净,附加逻辑集中到代理。静态代理缺点:每个接口都要手写代理类,接口多了很累。

2. JDK 动态代理:运行时自动生成代理(回顾第 11 章反射)

第 11 章学过反射,动态代理正是反射的典型应用:不用手写代理类,程序运行时自动"造"一个。

java 复制代码
import java.lang.reflect.*;

UserService target = new UserServiceImpl();
// 三个参数:类加载器、接口数组、调用处理器(InvocationHandler)
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    new Class[]{UserService.class},
    (p, method, args) -> {
        System.out.println("[权限] 校验通过");       // 附加逻辑:权限
        Object r = method.invoke(target, args);       // 反射调用真实方法
        System.out.println("[日志] 调用完成: " + method.getName());
        return r;
    });

proxy.addUser("小红");
// 输出:[权限] 校验通过 / 添加用户: 小红 / [日志] 调用完成: addUser

动态代理的原理Proxy.newProxyInstance运行时生成 一个实现 UserService 接口的代理类(一个临时的 .class 字节码),代理类所有方法调用都会转发给 InvocationHandler 的 invoke 方法------我们在 invoke 里统一加逻辑,加什么、加多少都由我们说了算。一个 InvocationHandler 能代理任意接口,这就是"动态"二字的威力。

3. 动态代理的限制与 CGLIB

JDK 动态代理有个硬限制:目标必须有接口------它生成的代理类只能"实现接口",不能"继承类"。

java 复制代码
// 没有接口的类:JDK 动态代理无能为力
class PlainService {  // 注意:没有实现任何接口
    public void doWork() { System.out.println("干活"); }
}
// Proxy.newProxyInstance(loader, new Class[]{PlainService.class}, ...)
// -> 抛 IllegalArgumentException: PlainService is not an interface

怎么办? 用 CGLIB(第三方库):它在运行时生成目标类的子类 ,通过继承实现代理,不需要接口。Spring 的 AOP 就是"有接口用 JDK 动态代理,没接口用 CGLIB"。面试常问:"JDK 动态代理和 CGLIB 的区别?"------JDK 代理要求接口、基于接口实现;CGLIB 基于继承、不要求接口、不能代理 final 类。

静态代理 vs 动态代理:静态代理"写死",一个接口一个类;动态代理靠反射在运行时生成,一个 InvocationHandler 能代理任意接口。Spring 的 AOP(面向切面编程)底层就是这个机制。

六、观察者模式:一喊多应(概念)

观察者模式(Observer):一个对象(被观察者)状态变化时,自动通知所有"关注它"的对象(观察者)。典型例子:按钮的点击监听器------你点了按钮,所有注册的监听器都会被回调。类比:主播开播(事件发生),所有关注他的粉丝(观察者)都收到通知。

java 复制代码
import java.util.ArrayList;
import java.util.List;

interface Listener { void onEvent(String event); }  // 观察者接口:监听器都实现它

// 被观察者:维护监听器列表,事件发生时挨个通知
class Button {
    private final List<Listener> listeners = new ArrayList<>();
    public void addListener(Listener l) { listeners.add(l); }  // 注册监听
    public void click() {
        System.out.println("按钮被点击");
        for (Listener l : listeners) l.onEvent("click");       // 通知所有观察者
    }
}

Button button = new Button();
button.addListener(e -> System.out.println("监听器A: 记录日志"));
button.addListener(e -> System.out.println("监听器B: 弹出提示"));
button.click();
// 输出:按钮被点击 / 监听器A: 记录日志 / 监听器B: 弹出提示

为什么用观察者? Button 不需要知道"谁在听",只管喊一嗓子;新增监听器不用改 Button 代码。GUI 里的 addMouseListener、Spring 的事件监听都是这个思想。适用场景:一个变化要触发多个动作,且动作列表会变。

推模型 vs 拉模型 :上面的例子是"推"------被观察者主动把事件内容推给观察者;"拉"是被观察者只通知"我变了",观察者自己来取数据。推模型简单直接(本章用推),拉模型解耦更彻底。注意线程安全 :真实项目里监听器列表常用 CopyOnWriteArrayList 而不是 ArrayList,防止"遍历通知的同时有人注册监听器"导致并发修改异常(易错点有专门条目)。

扩展知识

1. 设计模式的六大原则(SOLID)

模式背后是一组原则。SOLID 五个缩写 + 迪米特法则:单一职责(一个类只干一件事)、开闭原则(对扩展开放、对修改关闭)、里氏替换(子类能替换父类)、接口隔离(接口别太胖)、依赖倒置(依赖抽象不依赖具体)、迪米特法则(少和陌生人说话)。面试被问"为什么这么设计"时往原则上靠,比背模式名更显功底。开闭原则用代码看最直观:

java 复制代码
// 违反开闭原则:加一种支付方式就要改"支付处理"类
class BadPaymentProcessor {
    void pay(String type) {
        if ("alipay".equals(type))      { System.out.println("支付宝支付"); }
        else if ("wechat".equals(type)) { System.out.println("微信支付"); }
        // 加"银行卡支付"?又来这里加一个 else if------对修改开放了
    }
}

// 遵守开闭原则:定义接口,新增支付方式 = 新增一个类,旧代码一行不改
interface Pay { void pay(); }
class Alipay implements Pay { public void pay() { System.out.println("支付宝支付"); } }
class WechatPay implements Pay { public void pay() { System.out.println("微信支付"); } }
// 新增银行卡支付:再写一个 BankCardPay implements Pay 即可------这就是"对扩展开放、对修改关闭"

2. 单例的破坏与防御:反射、序列化

普通单例(饿汉/懒汉/双重检查/静态内部类)有一个致命弱点:反射可以绕过私有构造方法 ,强行再 new 一个实例;反序列化也会创建新实例。枚举单例天生免疫这两招。

java 复制代码
import java.lang.reflect.Constructor;

// 反射破坏双重检查单例:绕过私有构造,造出第二个实例
DoubleCheckSingleton s1 = DoubleCheckSingleton.getInstance();
Constructor<DoubleCheckSingleton> c = DoubleCheckSingleton.class.getDeclaredConstructor();
c.setAccessible(true);                       // 私有构造也能调
DoubleCheckSingleton s2 = c.newInstance();   // 需要在外层方法声明 throws Exception
System.out.println(s1 == s2); // 输出:false(两个不同实例!单例被破坏)
java 复制代码
// 枚举单例免疫反射:反射创建枚举实例直接抛异常
Constructor<EnumSingleton> ec = EnumSingleton.class.getDeclaredConstructors()[0];
ec.setAccessible(true);
try {
    ec.newInstance("x", 0);  // 枚举构造器带 (String, int) 两个隐藏参数
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage());
    // 输出:Cannot reflectively create enum objects(JVM 直接拒绝)
}

// 非枚举单例的序列化防御:加 readResolve 方法,反序列化时返回同一个实例
// protected Object readResolve() { return INSTANCE; }  // 加到单例类里即可

面试常问:"单例怎么防反射/序列化破坏?"------答:用枚举,或者加 readResolve + 在构造器里做"已存在实例就抛异常"的检查。

3. 装饰器模式与代理模式的区别(别混淆)

两者都是"包一层",但目的完全不同:代理控制"访问权" (能不能调、调用前后加逻辑);装饰器增强"功能"(加能力)。装饰器可以层层包裹,比如咖啡加牛奶、再加糖:

java 复制代码
interface Coffee {
    double cost();
    String desc();
}
class PlainCoffee implements Coffee {
    public double cost() { return 8; }
    public String desc() { return "原味咖啡"; }
}
// 装饰器:包一层,加料(持有"被装饰者",自己也是 Coffee)
class MilkDecorator implements Coffee {
    private final Coffee coffee;
    public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
    public double cost() { return coffee.cost() + 2; }
    public String desc() { return coffee.desc() + "+牛奶"; }
}
class SugarDecorator implements Coffee {
    private final Coffee coffee;
    public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
    public double cost() { return coffee.cost() + 1; }
    public String desc() { return coffee.desc() + "+糖"; }
}

// 层层包裹:原味 -> 加牛奶 -> 再加糖
Coffee c = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
System.out.println(c.desc() + " " + c.cost()); // 输出:原味咖啡+牛奶+糖 11.0

记忆口诀 :代理是"管门的"(管你能不能进),装饰器是"加料的"(给你加能力);Java IO 的 BufferedInputStream(new FileInputStream(...)) 就是装饰器经典案例。

4. 观察者模式进阶:事件三件套(EventObject / EventListener)

JDK 的事件体系(AWT/Swing、Spring 事件)由三件套组成:事件对象 (携带信息)、事件监听器接口 (观察者)、事件源(被观察者,负责广播)。模仿 JDK 风格写一遍:

java 复制代码
import java.util.EventObject;
import java.util.EventListener;
import java.util.ArrayList;
import java.util.List;

// 1) 事件对象:携带事件信息(继承 EventObject,source 是"谁发的")
class OrderEvent extends EventObject {
    private final String orderId;
    public OrderEvent(Object source, String orderId) {
        super(source);
        this.orderId = orderId;
    }
    public String getOrderId() { return orderId; }
}

// 2) 监听器接口:继承 EventListener 只是标记"我是监听器"
interface OrderListener extends EventListener {
    void onOrderCreated(OrderEvent e);
}

// 3) 事件源:创建订单后广播给所有监听器
class OrderService {
    private final List<OrderListener> listeners = new ArrayList<>();
    public void addListener(OrderListener l) { listeners.add(l); }
    public void createOrder(String orderId) {
        System.out.println("创建订单: " + orderId);
        OrderEvent event = new OrderEvent(this, orderId);
        for (OrderListener l : listeners) l.onOrderCreated(event);  // 广播
    }
}

OrderService service = new OrderService();
service.addListener(e -> System.out.println("[短信] 下单成功: " + e.getOrderId()));
service.addListener(e -> System.out.println("[日志] 记录订单: " + e.getOrderId()));
service.createOrder("ORD-1001");
// 输出:创建订单: ORD-1001 / [短信] 下单成功: ORD-1001 / [日志] 记录订单: ORD-1001

为什么要学这套? Spring 的 ApplicationEvent / ApplicationListener、Swing 的 ActionEvent / ActionListener 全是这个套路。看懂三件套,读框架事件源码就有了地图。

5. 策略模式:把"算法"也变成可替换的

策略模式(Strategy):把一组"可互换的算法"抽成接口,运行时自由切换------和观察者一样属于行为型模式。最典型的例子是促销折扣:不同用户不同折扣规则,收银台"插什么策略按什么算"。

java 复制代码
// 策略接口:一种折扣规则 = 一个策略
interface DiscountStrategy {
    double calc(double price);
}
class NoDiscount implements DiscountStrategy {
    public double calc(double price) { return price; }
}
class TenPercentOff implements DiscountStrategy {
    public double calc(double price) { return price * 0.9; }   // 9 折
}
class FullReduce implements DiscountStrategy {
    public double calc(double price) { return price >= 100 ? price - 20 : price; }  // 满 100 减 20
}

// 收银台:持有策略,可随时换
class Cashier {
    private DiscountStrategy strategy;
    public Cashier(DiscountStrategy strategy) { this.strategy = strategy; }
    public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; }  // 运行时换策略
    public double checkout(double price) { return strategy.calc(price); }
}

Cashier cashier = new Cashier(new NoDiscount());
System.out.println(cashier.checkout(120));  // 输出:120.0(无优惠)
cashier.setStrategy(new FullReduce());
System.out.println(cashier.checkout(120));  // 输出:100.0(满 100 减 20)
cashier.setStrategy(new TenPercentOff());
System.out.println(cashier.checkout(120));  // 输出:108.0(9 折)

// 新增"会员 8 折":加一个 VipDiscount 类即可,Cashier 一行不用改(又是开闭原则)

为什么用策略模式? 不用策略,折扣逻辑就是一个巨大的 if-else 或者 switch(满减、打折、无优惠、叠加......越加越长);用了策略,每种规则一个类,新增规则不动旧代码。策略模式 vs 状态模式:策略是"主动换算法",状态是"状态变了行为自动变"------面试答出这层区别就加分。

6. 不要过度设计:模式是工具,不是 KPI

模式是工具不是目的,小项目硬套 5 种模式只会更难读。判断标准:这个模式是否解决了当下的真实问题------只有一个形状时工厂就是多余的。先写简单代码,出现重复或扩展痛点时再引入模式。

java 复制代码
// 只有一个实现类时,工厂就是多余的
class PdfReport {
    public void generate() { System.out.println("生成 PDF 报告"); }
}

// 简单直接(推荐):
PdfReport report = new PdfReport();
report.generate(); // 输出:生成 PDF 报告

// 过度设计(只有一个实现类,别这么写):
// 接口 + PdfReportFactory + 工厂方法,三层包装就为了 new 一个对象
// PdfReport r = PdfReportFactory.create();  // 没有任何收益,只有阅读成本

什么时候该上模式? ① 出现了重复代码(工厂/策略能消重);② 需求明确会变(开闭原则才有意义);③ 团队里大家都懂模式(否则沟通成本大于收益)。记住:先能跑,再优雅------过度设计比没有设计更糟。

易错点

错误 :懒汉单例不加锁,多线程下创建多个实例(单例失效)。

正确:用双重检查锁定 + volatile,或用静态内部类、枚举------三者都兼顾线程安全。面试手写推荐静态内部类,最不容易写错。

错误 :忘记把构造方法设为 private,外部随便 new,单例变"多例"。

正确:单例构造方法必须 private,只能通过 getInstance() 拿实例。

错误 :以为枚举不能当单例、或者"枚举单例没业务能力"。

正确:枚举实例天然全局唯一、防反射防序列化,还能写方法、带字段,是最安全的单例写法之一。

错误 :工厂类里堆所有创建逻辑,if-else 越来越长(职责膨胀)。

正确:按开闭原则拆分,新增类型时新增工厂/类(工厂方法/抽象工厂),而不是继续往一个工厂加分支。

错误 :写了 Builder 却忘了调用 build(),得到 null;或漏了必填字段校验。

正确:build() 才真正组装对象;必填字段放 Builder 构造器(不填编译不过),业务校验放 build() 里抛异常。

错误 :把代理模式和装饰器模式(Decorator)混为一谈。

正确:代理控制"访问权"(能不能调、调用前后加逻辑);装饰器增强"功能"(加能力)。代理由调用方间接使用,装饰器可层层包裹(咖啡加牛奶加糖)。

错误 :以为 JDK 动态代理能代理任何类。它只能代理有接口 的类。

正确:没有接口的类要用 CGLIB(基于继承生成子类);Spring AOP 的策略就是"有接口用 JDK 动态代理,没接口用 CGLIB"。

错误 :观察者的监听器列表用普通 ArrayList,遍历通知的同时有人注册监听器,抛 ConcurrentModificationException。

正确 :用 CopyOnWriteArrayList(写时复制,遍历时改也安全)或加锁。

错误 :以为"单例 = 线程安全"。单例只保证"一个实例",实例里的共享可变字段被多线程改照样出问题。

正确:单例 + 无状态(或不可变字段)才安全;有可变状态要加锁或用原子类。

错误 :以为饿汉/懒汉/双重检查/静态内部类单例"绝对安全"------反射、反序列化都能再造一个实例。

正确:要绝对安全用枚举单例;或加 readResolve + 构造器防反射检查。

JDK 8 / 11 / 17 版本调用方式

设计模式是代码组织思想,与 JDK 版本无关:本章所有模式在 JDK 8、JDK 11、JDK 17 下写法完全一致、都能运行。枚举单例、静态内部类、volatile、synchronized、反射动态代理,三个版本全部支持。版本差异只在一处:JDK 17 新增的 record 可以简化 Builder 里的数据类。

写法 JDK 8 JDK 11 JDK 17
五种单例写法(饿汉/懒汉/双重检查/静态内部类/枚举) 全部可用 全部可用 全部可用
工厂(简单工厂/工厂方法/抽象工厂)、Builder(普通类)、静态代理、观察者 全部可用 全部可用 全部可用
JDK 动态代理(java.lang.reflect.Proxy + lambda) 可用 可用 可用
record(简化 Builder 中的数据类) 新增,需 JDK 17 才能运行
java 复制代码
// JDK 17 及以上:record 一键生成不可变数据类,替代手写 getter/toString
public record Address(String city, String street) {}

Address a = new Address("北京", "中关村");
System.out.println(a.city() + " " + a.street()); // 输出:北京 中关村(需 JDK 17 才能运行)
// record 自动生成:构造器、city()/street() 访问器、equals/hashCode/toString------不用手写

JDK 17 的一个注意点 :JDK 17 默认强封装(strong encapsulation),反射访问 JDK 内部类 会抛 InaccessibleObjectException(需加 --add-opens 才放行);但本章反射示例访问的是我们自己写的类,三个版本下都正常,不受影响。

迁移建议:三个版本下设计模式写法零差异,无需迁移。升级 JDK 17 后可以用 record 少写样板代码(比如 Person 的姓名/年龄数据用 record 承载),但模式本身的套路不变。

小结

  1. 设计模式是"特定场景下被验证的解决方案",不是语法,是代码组织经验;23 种模式分创建型、结构型、行为型三大类。
  2. 单例保证全局唯一:饿汉简单但可能浪费内存、懒汉有线程坑、双重检查要 volatile、静态内部类最优雅、枚举最安全(还防反射/序列化)。
  3. 工厂把"创建对象"集中管理:简单工厂一个类搞定但会膨胀;工厂方法扩展时不动旧代码;抽象工厂生产"一族"产品(成套皮肤)。
  4. Builder 适合参数多的对象,StringBuilder 就是现成例子;自己写 Builder 要记得最后 build(),必填字段放构造器、业务校验放 build()。
  5. 代理在调用前后附加逻辑:静态代理手写、动态代理靠反射运行时生成(Spring AOP 的根基);JDK 动态代理只能代理有接口的类,没接口用 CGLIB。
  6. 观察者"一喊多应",事件监听器就是它的应用;监听器列表注意线程安全(CopyOnWriteArrayList)。
  7. 扩展知识:SOLID 六大原则、单例的破坏与防御(反射/序列化/枚举免疫)、装饰器 vs 代理(管门的 vs 加料的)、事件三件套、策略模式(算法可替换)、不要过度设计。
  8. 模式是工具不是 KPI:先写简单代码,出现重复或扩展痛点时再引入模式;模式要服务于真实问题,别为模式而模式。

下一篇:第15章 日期时间与常用工具类

相关推荐
坐吃山猪22 分钟前
JDK 8 到 JDK 21 新特性知识要点
java·开发语言·windows
坐吃山猪24 分钟前
【多线程】FutureTask多线程底层实现
java·开发语言·数据库
weixin1997010801628 分钟前
[特殊字符]《从0到1:闲鱼开放平台授权登录 + AccessToken 刷新 + 聚石塔部署完整链路》(附Python源码)
java·数据库·python
Freak嵌入式1 小时前
RP2040 PIO 编程模型与状态机原理:从硬件架构到工作逻辑全解析
java·大数据·开发语言·单片机·嵌入式硬件·硬件架构
kakawzw1 小时前
Netty源码笔记
java·服务器·后端
zhuodedao1 小时前
Spring AI + MCP 文件工具未调用问题复盘:为什么初始化成功却没有写入文件?
java·debug·agent·springai·mcp
Java小白笔记1 小时前
Java中大数据实时归集与指标汇总方案
java·大数据·开发语言
随遇而安zx1 小时前
【地基篇】---Java 8 JVM 知识大纲
java·开发语言·jvm
雾隐隐o1 小时前
Docker 镜像归档与容器管理
java·docker·eureka