单例模式
单例模式(Singleton Pattern)是软件开发中最简单、也是最常用的设计模式之一,属于创建型模式。
它的核心目标非常明确:确保一个类在整个系统中只有一个实例,并提供一个全局访问点。
实现方式
1. 饿汉式
类加载时就创建实例,"未运行,先创建"。
java
public class Singleton {
// 类加载时直接初始化
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
优点:
- 线程安全:由 JVM 类加载机制保证,无需同步控制。
- 实现简单。
缺点:
- 不支持延迟加载:即使程序中从未使用该实例,它也会占用内存。如果实例初始化非常消耗资源,可能会导致启动变慢。
2. 懒汉式(线程不安全)
第一次调用 getInstance() 时才创建实例,"用到再创建"。
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 多线程下可能创建多个实例
}
return instance;
}
}public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 多线程下可能创建多个实例
}
return instance;
}
}
缺点 :致命缺陷。在多线程并发环境下,多个线程可能同时通过 if (instance == null) 判断,导致创建多个实例,违反单例原则。
3. 懒汉式(线程安全 - 同步方法)
在方法上加锁,强制串行化。
java
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
缺点:性能低下。锁的粒度过大,每次获取实例都要排队等待,即使实例已经创建好了,也会造成不必要的同步开销。
4. 双重检查锁 ------ 推荐
结合了懒加载和高性能,通过两次检查减少同步开销。
java
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;
}
}
关键点 :必须使用 volatile 关键字修饰 instance。instance = new Singleton() 这行代码在 JVM 中分为三步:1.分配内存;2.初始化对象;3.将引用指向内存。由于指令重排序 ,JVM 可能执行顺序为 1->3->2。如果没有 volatile,线程 A 执行了 3 但还没执行 2,线程 B 判断 instance != null 直接取走了一个尚未初始化完成 的对象,导致程序崩溃。volatile 禁止了指令重排序,确保了可见性和有序性。
5. 静态内部类 ------ 推荐
利用 JVM 类加载机制实现延迟加载和线程安全。
java
public class Singleton {
private Singleton() {}
// 静态内部类,只有在被调用时才会被加载
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
原理 :外部类加载时,内部类 Holder 不会被加载。只有当调用 getInstance() 时,JVM 才会加载 Holder 类并初始化 INSTANCE。这既实现了延迟加载 ,又利用了 JVM 机制保证了线程安全。
6. 枚举单例 ------ 最安全
利用 Java 枚举特性,代码最简洁,且天然防御攻击。
java
public enum Singleton {
INSTANCE;
public void businessMethod() {
// 业务逻辑
}
}
优点:绝对安全。由 JVM 底层保证单例,天然防止反射攻击和序列化破坏。这是《Effective Java》作者 Joshua Bloch 推荐的方式。
常见应用场景
-
资源共享:
- 数据库连接池:避免频繁创建和销毁连接。
- 线程池:复用线程,减少开销。
- 日志记录器:保证日志写入的一致性。
-
配置管理:
- 读取配置文件(如
application.properties)的配置管理器,确保全系统读取的配置一致。
- 读取配置文件(如
-
系统工具:
- Windows 的任务管理器、打印机后台处理服务(Spooler)。
代理模式
代理模式(Proxy Pattern)是 Java 开发中极其重要的一种结构型设计模式。它是 Spring AOP、RPC 框架、事务管理等核心技术的底层基石。
简单来说,代理模式的核心思想是:为某个对象提供一个代理(代用)对象,由代理对象控制对目标对象的访问。
角色和结构
- 抽象主题(Subject):定义真实对象和代理对象的共同接口。确保代理对象可以在任何需要真实对象的地方被使用(里氏替换原则)。
- 真实主题(RealSubject):真正执行业务逻辑的对象,是代理的最终调用目标。
- 代理对象(Proxy):实现与真实对象相同的接口。持有真实对象的引用。在调用真实对象的方法前后,添加附加逻辑(如日志、权限校验、延迟加载等)。
使用示例
1. 静态代理
由程序员手动编写代理类,在编译期就确定了代理类与目标类的关系。
- 优点:代码直观,易于理解。
- 缺点:如果接口方法很多,代理类会变得非常臃肿;且一旦接口变更,代理类也要修改(违反开闭原则)。
java
// 1. 抽象主题
interface UserService {
void addUser(String name);
}
// 2. 真实主题
class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("【核心业务】正在添加用户: " + name);
}
}
// 3. 静态代理类
class UserServiceProxy implements UserService {
private UserService target; // 持有真实对象引用
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String name) {
System.out.println("【代理前置】权限校验通过"); // 附加功能
target.addUser(name); // 调用真实对象
System.out.println("【代理后置】记录日志"); // 附加功能
}
}
2. 动态代理
代理类在运行时动态生成,不需要手动编写代理类的代码。这是 Spring AOP 的核心实现方式。主要分为两种:
JDK 动态代理
- 原理:基于反射机制。
- 限制:目标类必须实现接口。
- 核心类:java.lang.reflect.Proxy 和 InvocationHandler。
GLIB 动态代理
- 原理:基于继承(使用 ASM 字节码生成库)。
- 特点:生成目标类的子类,通过重写方法来拦截调用。
- 限制:目标类不能是 final 的(因为无法继承)。
java
import java.lang.reflect.*;
// InvocationHandler 是核心,所有动态代理的方法调用都会汇聚到这里
class DebugInvocationHandler implements InvocationHandler {
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【动态代理】方法 " + method.getName() + " 开始执行");
// 反射调用真实对象的方法
Object result = method.invoke(target, args);
System.out.println("【动态代理】方法执行结束");
return result;
}
}
// 客户端调用
public class Client {
public static void main(String[] args) {
UserService realService = new UserServiceImpl();
// 动态生成代理对象
UserService proxyService = (UserService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(),
new DebugInvocationHandler(realService)
);
proxyService.addUser("张三");
}
}
对比
| 维度 | 代理模式 | 装饰器模式 | 适配器模式 |
|---|---|---|---|
| 核心意图 | 控制访问 | 增强功能 | 接口转换 |
| 解决的问题 | 客户端不能或不想直接访问目标对象(如权限、远程、延迟)。 | 需要动态地给对象添加职责,且不想使用继承。 | 两个现有类的接口不兼容,无法协同工作。 |
| 接口关系 | 代理类与真实类实现相同的接口。 | 装饰器与被装饰对象实现相同的接口。 | 适配器实现目标接口,但内部持有的是源接口对象。 |
| 对原对象的影响 | 不改变原对象的功能,只是控制访问时机或流程。 | 在原有功能基础上叠加新功能(1+1>2)。 | 改变了客户端看到的接口形态,使其符合预期。 |
| 典型场景 | Spring AOP、RPC远程调用、图片懒加载、权限校验。 | Java IO流 (BufferedInputStream)、咖啡加料、窗口加滚动条。 |
电源转接头、旧系统接口迁移、第三方库集成。 |
| 生命周期 | 代理对象通常由框架或系统管理,生命周期与真实对象绑定。 | 装饰器是在运行时动态组合的,可以层层嵌套。 | 适配器通常是一次性转换,用于解决兼容性问题。 |
使用场景
在不修改原有代码的前提下,对对象的访问进行控制或增强
| 你的需求 | 推荐场景 | 常用技术/框架 |
|---|---|---|
| 我要调用别人的服务 | 远程代理 | Dubbo, Feign, gRPC |
| 对象太大/太慢,不想马上加载 | 虚拟代理 | Hibernate Lazy, 图片懒加载 |
| 我要控制谁能用这个方法 | 保护代理 | Spring Security, 权限拦截器 |
| 我要加日志、事务、监控 | 智能引用/增强代理 | Spring AOP, AspectJ |
| 我想省去重复计算的麻烦 | 缓存代理 | Spring Cache, Redis 代理 |
工厂模式
工厂模式(Factory Pattern)是软件开发中最经典、使用最频繁的创建型设计模式。
它的核心思想非常朴素:将对象的"创建过程"与"使用过程"分离
使用示例
1. 简单工厂 ------ "一个工厂包打天下"
只有一个工厂类,内部通过 if-else 或 switch 判断来创建不同的产品。
java
public class SimpleCarFactory {
// 根据类型参数生产汽车
public static Car createCar(String type) {
if ("benz".equals(type)) {
return new Benz();
} else if ("bmw".equals(type)) {
return new Bmw();
} else {
throw new IllegalArgumentException("没有这种车");
}
}
}
优点 :调用方简单,不需要知道具体类名,只需要传参。
缺点 :违反开闭原则 。如果你想增加一种"奥迪"车,必须修改工厂类的代码(修改 if-else),这在大型系统中是危险的。
2. 工厂方法 ------ "专厂专用"
定义一个工厂接口,让具体的工厂子类去决定实例化哪个产品类。一个产品对应一个工厂。
java
// 工厂接口
interface CarFactory {
Car createCar();
}
// 奔驰工厂
class BenzFactory implements CarFactory {
public Car createCar() { return new Benz(); }
}
// 宝马工厂
class BmwFactory implements CarFactory {
public Car createCar() { return new Bmw(); }
}
优点 :符合开闭原则。如果要增加"奥迪",只需要新建一个 AudiFactory 和 Audi 类,不需要修改原有的任何代码。
缺点:类爆炸。每增加一个产品,就要增加一个工厂类,系统复杂度变高。
3. 抽象工厂 ------ "生产产品族"
一个工厂不仅能生产"奔驰车",还能生产"奔驰引擎"、"奔驰轮胎"。它保证客户端创建出来的是一套兼容的产品。
java
interface CarFactory {
Car createCar();
Engine createEngine();
}
// 奔驰工厂生产全套奔驰配件
class BenzFactory implements CarFactory {
public Car createCar() { return new BenzCar(); }
public Engine createEngine() { return new BenzEngine(); }
}
优点 :保证产品的一致性(你不会把宝马的引擎装到奔驰车上)。
缺点 :难以扩展新产品等级结构。如果你想增加"轮胎"这个产品,所有的工厂接口和实现类都要修改,非常麻烦。
| 模式类型 | 核心特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 简单工厂 | 一个工厂类,内部判断逻辑 | 结构简单,调用方便 | 违反开闭原则(改代码) | 产品种类少,且逻辑简单 |
| 工厂方法 | 一个产品对应一个工厂 | 符合开闭原则(扩展不修改) | 类数量多,系统复杂 | 产品种类多,且经常需要扩展 |
| 抽象工厂 | 一个工厂生产一族产品 | 保证产品族的一致性 | 难以扩展新的产品等级 | 需要创建一系列相关依赖的对象(如 UI 换肤) |
使用场景
场景一:对象创建过程复杂(封装复杂性)
场景二:需要根据条件动态决定创建哪个对象(解耦与灵活性)
场景三:系统需要遵循"开闭原则"(易于扩展)
场景四:需要创建一系列相关或依赖的对象(产品族)
场景五:统一管理对象的生命周期(单例与池化)
克隆模式
克隆模式(Prototype Pattern),又称原型模式,是一种创建型设计模式。
它的核心思想非常直观:通过复制一个现有的对象(原型)来创建新对象,而不是通过 new 关键字重新实例化。
角色
- Prototype(抽象原型) :声明克隆自己的接口(在 Java 中通常是实现
Cloneable接口)。 - ConcretePrototype(具体原型):实现克隆操作的具体类。
- Client(客户):让原型克隆自己,从而创建新对象。
如果一个对象的初始化非常耗时(比如读取大文件、连接数据库、复杂的计算),直接克隆内存中已经存在的对象,速度会快得多。
当对象的属性非常复杂,或者在运行时动态决定创建哪种类型的对象时,克隆比
new更灵活。
浅拷贝
定义 :只复制对象本身的基本数据类型字段(如 int, String 等),但对于引用类型 (如数组、对象),只复制了内存地址 。
后果:新旧对象共享同一个引用对象。如果你修改了新对象里的引用属性,原对象也会跟着变("牵一发而动全身")。
使用示例:
java
class User implements Cloneable {
private String name;
private int age;
private Address address; // 引用类型
// 构造方法、Getter、Setter 省略...
@Override
protected Object clone() throws CloneNotSupportedException {
// 默认调用 super.clone() 是浅拷贝
return super.clone();
}
}
class Address {
private String city;
// 构造、Getter、Setter...
}
深拷贝
定义 :不仅复制对象本身,还递归地复制对象中包含的所有引用类型 属性。
后果:新旧对象完全隔离,互不影响。
使用示例:
java
class User implements Cloneable {
private String name;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
// 1. 先克隆自身
User user = (User) super.clone();
// 2. 再手动克隆引用对象(Address 也要实现 Cloneable)
user.setAddress((Address) this.address.clone());
return user;
}
}
对象序列化示例:
java
import java.io.*;
public class SerializationUtils {
/**
* 利用序列化实现深拷贝
* @param object 需要拷贝的对象
* @return 拷贝后的新对象
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T object) {
if (object == null) return null;
try (
// 1. 序列化:将对象写入字节数组输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)
) {
oos.writeObject(object);
oos.flush();
// 2. 反序列化:从字节数组输入流读取对象
try (
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais)
) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
优点:
- 简单通用:无需手动递归拷贝每个字段;
- 安全:彻底隔离对象,自动处理循环引用。
缺点:
- 性能开销 :涉及 IO 操作和反射,比直接
new或clone()慢; - 限制多 :所有关联类必须实现
Serializable,且无法拷贝transient字段。
比对
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 基本数据类型 | 复制值 | 复制值 |
| 引用数据类型 | 复制地址(指向同一块内存) | 复制新对象(指向新内存) |
| 修改新对象属性 | 会影响原对象 | 不会影响原对象 |
| 实现难度 | 简单(默认支持) | 复杂(需手动处理) |
| 性能开销 | 低 | 高(需要创建更多对象) |
生产消费模式
生产消费模式(Producer-Consumer Pattern)是并发编程和系统架构中最经典、最基础的解耦模式之一。
它的核心思想非常直观:引入一个"中间层"(缓冲区)来平衡"生产者"和"消费者"的处理速度差异,从而解除两者之间的直接耦合。
角色
- 生产者 :负责生成数据或任务;当缓冲区满时,必须阻塞等待,不能无限生产导致内存溢出。
- 消费者 :负责处理数据或执行任务;当缓冲区空时,必须阻塞等待,不能空转浪费 CPU 资源。
- 缓冲区 :核心组件 。通常是一个线程安全的队列 (如
BlockingQueue);它起到了削峰填谷的作用:在请求高峰期暂存任务,在低峰期慢慢处理。
功能
| 价值点 | 说明 |
|---|---|
| 解耦 | 生产者不需要知道谁是消费者,也不需要知道消费者有多少个。只要把数据扔进队列,它的任务就完成了。 |
| 支持并发 | 生产者和消费者可以异步运行。生产者可以持续快速生产,而消费者可以按照自己的节奏处理,互不阻塞。 |
| 流量削峰 | 当突发流量(如秒杀活动)到来时,请求先堆积在缓冲区(队列)中,消费者按照系统能承受的最大速度慢慢处理,防止系统崩溃。 |
使用示例
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerDemo {
// 1. 创建缓冲区(容量为 10)
private static final BlockingQueue<Task> queue = new LinkedBlockingQueue<>(10);
// 2. 生产者线程
static class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 20; i++) {
Task task = new Task("任务-" + i);
// put 方法:如果队列满了,会自动阻塞等待,直到有空位
queue.put(task);
System.out.println("生产者生产了: " + task.getName());
Thread.sleep(200); // 模拟生产耗时
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 3. 消费者线程
static class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
// take 方法:如果队列空了,会自动阻塞等待,直到有新数据
Task task = queue.take();
System.out.println("消费者处理了: " + task.getName());
Thread.sleep(500); // 模拟消费耗时(比生产慢)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 4. 启动测试
public static void main(String[] args) {
// 启动 1 个生产者
new Thread(new Producer()).start();
// 启动 3 个消费者(多线程消费)
new Thread(new Consumer()).start();
new Thread(new Consumer()).start();
new Thread(new Consumer()).start();
}
}
注意事项
-
队列溢出:
- 如果生产者速度远大于消费者,且队列有界(如
ArrayBlockingQueue),生产者会一直阻塞,甚至导致整个系统吞吐量下降(背压效应)。 - 对策:合理设置队列大小,或者使用无界队列(需谨慎内存溢出)。
- 如果生产者速度远大于消费者,且队列有界(如
-
数据丢失:
- 如果消费者在处理过程中崩溃,队列中的消息可能会丢失(取决于队列是内存型还是持久化型)。
- 对策:使用持久化的消息队列(如 Kafka)。
-
死锁风险:
- 如果在同步块中调用
put或take,可能会导致死锁。 - 对策 :尽量使用
BlockingQueue这种封装好的并发工具,避免手动使用synchronized+wait/notify。
- 如果在同步块中调用
适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,
它的核心目标非常直观:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以协同工作。
角色
目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
实现方式
1. 类适配器(继承实现)
通过多重继承(Java 中是继承类 + 实现接口)来实现。适配器继承适配者,并实现目标接口。
java
// 1. 目标接口
interface MediaPlayer {
void play(String fileName);
}
// 2. 适配者(已有的类,接口不兼容)
class Mp4Player {
public void playMp4(String fileName) {
System.out.println("播放 MP4: " + fileName);
}
}
// 3. 类适配器
class Mp4Adapter extends Mp4Player implements MediaPlayer {
@Override
public void play(String fileName) {
// 调用父类(适配者)的具体方法
playMp4(fileName);
}
}
- 优点:代码简单,因为直接继承了适配者,无需持有对象引用。
- 缺点 :局限性大。Java 不支持多重继承(只能继承一个类),如果适配者已经是某个类的子类,你就无法使用类适配器了。此外,这也导致适配器与适配者耦合度极高。
2. 对象适配器(组合实现)
通过组合(关联关系)来实现。适配器实现目标接口,并在内部持有一个适配者的实例。
java
// 1. 目标接口
interface USB {
void connect();
}
// 2. 适配者(Type-C 接口)
class TypeCDevice {
public void transfer() {
System.out.println("Type-C 传输数据");
}
}
// 3. 对象适配器
class USBAdapter implements USB {
private TypeCDevice typeCDevice; // 持有适配者引用
public USBAdapter(TypeCDevice typeCDevice) {
this.typeCDevice = typeCDevice;
}
@Override
public void connect() {
// 委托给适配者执行
typeCDevice.transfer();
}
}
-
优点:
- 灵活:不破坏单继承规则,可以适配任何类及其子类。
- 低耦合:适配器与适配者是关联关系,而非继承关系,更符合"合成复用原则"。
-
缺点:需要多创建一个对象,稍微增加了一点点内存开销(通常可忽略)。
适配器模式 vs 装饰器模式 vs 代理模式
| 模式 | 核心意图 | 接口关系 | 形象比喻 |
|---|---|---|---|
| 适配器模式 | 接口转换 | 目标接口与适配者接口不同 | 翻译官:把中文翻译成英文,让对方听懂。 |
| 装饰器模式 | 功能增强 | 装饰器与被装饰对象接口相同 | 化妆师:给你化妆,让你更漂亮,但你还是你。 |
| 代理模式 | 访问控制 | 代理类与真实对象接口相同 | 经纪人:控制谁能见你,什么时候见你。 |
装饰模式
装饰模式(Decorator Pattern),又称装饰者模式,是一种结构型设计模式。
它的核心思想是:动态地给一个对象添加一些额外的职责,而不改变其原有结构。 相比于通过继承来扩展功能,装饰模式提供了更灵活、更轻量级的替代方案。
角色
抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
使用示例
java
// 抽象构件:定义咖啡的通用接口
public interface Coffee {
String getDescription(); // 获取描述
double getPrice(); // 获取价格
}
// 具体构件:美式咖啡,这是我们的"原始对象"
public class Americano implements Coffee {
@Override
public String getDescription() {
return "美式咖啡";
}
@Override
public double getPrice() {
return 20.0;
}
}
// 抽象装饰器:实现Coffee接口,并持有一个Coffee的引用
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee; // 核心:组合,持有被装饰的对象
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription(); // 默认委托给被装饰对象
}
@Override
public double getPrice() {
return decoratedCoffee.getPrice(); // 默认委托给被装饰对象
}
}
// 具体装饰器:牛奶
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + 牛奶"; // 在原有描述上增加
}
@Override
public double getPrice() {
return super.getPrice() + 5.0; // 在原有价格上增加
}
}
// 具体装饰器:摩卡
public class MochaDecorator extends CoffeeDecorator {
public MochaDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + 摩卡";
}
@Override
public double getPrice() {
return super.getPrice() + 8.0;
}
}
// 客户端使用(动态组合)
public class CoffeeShop {
public static void main(String[] args) {
// 1. 点一杯基础的美式咖啡
Coffee myCoffee = new Americano();
System.out.println(myCoffee.getDescription() + " ¥" + myCoffee.getPrice());
// 2. 动态地给它加上牛奶
myCoffee = new MilkDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " ¥" + myCoffee.getPrice());
// 3. 再继续加上摩卡
myCoffee = new MochaDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " ¥" + myCoffee.getPrice());
}
}
优点
| 优势 | 说明 |
|---|---|
| 比继承更灵活 | 可以在运行时动态地添加或撤销功能,而继承是在编译时静态决定的。 |
| 避免"类爆炸" | 如果使用继承,为每种功能组合(美式+牛奶、美式+摩卡、美式+牛奶+摩卡...)都创建一个子类,会导致类的数量急剧增加。装饰模式通过组合完美规避了这个问题。 |
| 符合开闭原则 | 对扩展开放,对修改关闭。要增加一种新配料(如"糖浆"),只需新增一个 SyrupDecorator 类,无需修改任何现有代码。 |
| 符合单一职责原则 | 每个具体装饰器类只负责一项功能的增强,职责清晰。 |
策略模式
策略模式(Strategy Pattern)是一种行为型设计模式。
它的核心思想非常直观:定义一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。
角色
1、**抽象策略(Strategy):**这是一个接口或抽象类,定义了所有具体策略必须实现的通用方法(比如 execute() 或 pay())。它规定了"做什么",但不规定"怎么做"。
2、**具体策略(ConcreteStrategy):**实现了抽象策略接口的具体类。每一个具体策略类都封装了一种具体的算法或行为(比如"微信支付算法"、"支付宝支付算法")。
3、**上下文(Context):**持有一个抽象策略的引用。它负责调用具体策略的方法,但不关心具体是哪个策略在执行。客户端可以通过它来动态切换策略。
使用示例
java
// 抽象策略:定义支付的通用接口
public interface PaymentStrategy {
void pay(double amount);
}
// 具体策略A:微信支付
public class WechatPay implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("使用【微信支付】付款:" + amount + " 元");
// 这里会调用微信的SDK接口
}
}
// 具体策略B:支付宝支付
public class Alipay implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("使用【支付宝支付】付款:" + amount + " 元");
// 这里会调用支付宝的SDK接口
}
}
// 具体策略C:银联支付
public class UnionPay implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("使用【银联支付】付款:" + amount + " 元");
}
}
// 上下文:持有支付策略的引用
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
// 动态注入/切换策略
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// 执行支付
public void checkout(double amount) {
if (paymentStrategy == null) {
System.out.println("请选择支付方式!");
return;
}
System.out.println("订单结算中...");
paymentStrategy.pay(amount); // 委托给具体策略
}
}
// 客户端调用
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
double amount = 1000.0;
// 场景1:用户选择了微信支付
cart.setPaymentStrategy(new WechatPay());
cart.checkout(amount);
// 场景2:用户改变主意,选择了支付宝
cart.setPaymentStrategy(new Alipay());
cart.checkout(amount);
}
}
优点
| 优势 | 说明 |
|---|---|
| 符合开闭原则 | 增加一种新的支付方式(如"数字人民币"),只需新增一个类实现接口,不需要修改现有的 ShoppingCart 或 Client 代码。 |
| 避免多重条件 | 彻底消除了难以维护的 if-else 嵌套,代码结构清晰。 |
| 算法复用 | 具体的策略类(如 WechatPay)可以在不同的上下文中复用。 |
| 解耦 | 上下文(购物车)不需要知道具体策略的实现细节,只通过接口交互。 |
应用场景
| 场景 | 说明 |
|---|---|
| 排序算法 | Arrays.sort() 或 Collections.sort()。你可以根据需要传入不同的 Comparator(策略),比如按年龄排序、按姓名排序。 |
| 压缩算法 | 压缩文件时,可以选择 ZipCompressor、RarCompressor 或 7zCompressor。 |
| 电商促销 | 计算价格时,根据活动类型选择 NormalDiscount(日常9折)、Double11Discount(双11五折)或 VipDiscount(会员价)。 |
| 登录方式 | 用户登录时,可以选择 PasswordLogin(密码登录)、SmsLogin(短信验证码)或 WechatLogin(微信扫码)。 |
| Spring 框架 | Spring 的 Resource 接口(ClassPathResource, FileSystemResource)也是策略模式的一种体现,用于处理不同来源的资源加载。 |
总结
策略模式是"消除 if-else 地狱"的利器。
核心原则 :封装变化,多用组合,少用继承。
最佳实践 :当你的代码中出现大量的 if (type == A) ... else if (type == B) 时,请优先考虑使用策略模式。
一句话心法 :如果你有多种做法 去完成同一件事 ,并且需要随时切换 ,请用策略模式。
- 提供一种凭据式的设计方案。
- 服务程序不等数据处理完成便立即返回客户端一个伪造的数据,实现了Future模式的客户端在拿到这个返回结果后,并不急于对其进行处理,而去调用了其他业务逻辑,充分利用了等待时间。
Future模式
Future模式,也称为异步编程模式,是一种并发设计模式。
它的核心思想非常直观:当你发起一个耗时操作时,不立即等待结果,而是立刻拿到一个"凭证"或"占位符",然后继续做其他事情。等到真正需要结果时,再通过这个"凭证"去获取。
角色
1、**Future(未来对象/凭证):**这是模式的核心。它是一个接口或对象,代表了异步计算的结果。它提供了检查任务是否完成、等待任务完成和获取结果的方法。
2、**Task(任务):**实际执行耗时操作的逻辑。在Java中,通常是 Callable 或 Runnable 接口的实现。
3、**Client(客户端):**任务的发起者。它提交任务,获得 Future 对象,然后可以继续执行其他操作。在需要结果时,调用 Future 的 get() 方法。
使用示例
java
import java.util.concurrent.*;
// 1. 定义一个耗时的任务 (Task)
class DataQueryTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("[" + Thread.currentThread().getName() + "] 开始查询数据...");
Thread.sleep(2000); // 模拟耗时操作
System.out.println("[" + Thread.currentThread().getName() + "] 数据查询完成!");
return "查询结果:用户ID=1001, 用户名=张三";
}
}
// 2. 客户端使用Future模式
public class FutureDemo {
public static void main(String[] args) throws Exception {
// 创建任务
Callable<String> task = new DataQueryTask();
// 将任务包装成FutureTask
FutureTask<String> futureTask = new FutureTask<>(task);
// 启动一个线程来执行任务
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("[" + Thread.currentThread().getName() + "] 任务已提交,继续做其他事情...");
// 主线程在这里可以做其他不依赖查询结果的工作
doOtherWork();
System.out.println("[" + Thread.currentThread().getName() + "] 现在需要结果了,准备获取...");
// 调用get()方法获取结果,如果任务没完成,这里会阻塞
String result = futureTask.get();
System.out.println("[" + Thread.currentThread().getName() + "] 获取到结果: " + result);
}
// 模拟其他工作
private static void doOtherWork() throws InterruptedException {
for (int i = 0; i < 3; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] 正在处理其他业务... " + (i+1));
Thread.sleep(500);
}
}
}
优点
| 优势 | 说明 |
|---|---|
| 提高系统响应速度 | 将耗时的操作异步化,主线程(如Web请求线程)可以立即返回,快速响应用户,提升用户体验。 |
| 充分利用CPU资源 | 在等待I/O(如数据库查询、网络请求)完成时,CPU可以去处理其他任务,避免了线程的空转等待。 |
| 解耦任务提交与结果获取 | 任务的发起者不需要关心任务是如何执行的,只需要在需要时获取结果即可。 |
应用场景
| 场景 | 说明 |
|---|---|
| Web服务异步调用 | 在处理HTTP请求时,将数据库查询、第三方API调用等耗时操作放入 Future 中,立即返回一个"处理中"的响应,避免请求超时。 |
| 并行执行独立任务 | 一个页面需要展示用户信息、订单列表和消息通知。这三个查询互不依赖,可以用三个 Future 并行执行,最后等待所有结果返回,大大缩短总耗时。 |
| 定时任务 | 提交一个定时任务,Future 对象可以用来检查任务状态或取消任务。 |
| RPC框架 | Dubbo、gRPC等远程调用框架,其客户端调用远程服务的方法时,返回的就是一个 Future 对象,代表未来才会返回的远程结果。 |
总结
Future模式是现代高并发编程的基石。
核心原则 :异步调用,用"凭证"换取"结果"。
最佳实践 :在Java中,优先使用 ExecutorService 提交 Callable 任务来获取 Future。对于复杂场景,强烈推荐使用 CompletableFuture。一句话心法 :当你有一个耗时操作 ,并且不希望当前线程傻等 时,请使用Future模式。
不变模式
不变模式(Immutable Pattern),又称不可变模式,是一种设计模式 ,其核心思想是:一个对象一旦被创建,其内部状态就永远无法被修改。
使用示例
1、**类本身必须是 final 的:**防止被继承。如果类可以被继承,子类就有可能通过重写方法来破坏其不可变性。
2、所有字段都必须是 private final 的
private:防止外部直接访问和修改。final:保证字段在对象创建后只能被赋值一次。
3、**不提供任何可以修改状态的方法(如 setter):**对象的状态只能在构造函数中被初始化,之后便无法更改。
4、处理可变对象时要格外小心(防御性拷贝)
- 如果一个字段引用了可变对象(如
Date、数组、集合),在构造函数中不能直接使用这个引用,而应该创建一个副本(深拷贝)。 - 同样,在提供访问这个字段的
getter方法时,也不能直接返回引用,而应该返回其副本。这是为了防止外部通过引用来间接修改内部状态。
java
import java.util.Arrays;
// 1. 类声明为 final,禁止被继承
public final class ImmutableUser {
// 2. 所有字段都声明为 private final
private final String name;
private final int age;
// 这是一个可变对象(数组),需要特殊处理
private final String[] hobbies;
// 3. 通过构造函数初始化所有状态
public ImmutableUser(String name, int age, String[] hobbies) {
this.name = name;
this.age = age;
// 4. 防御性拷贝:在构造时创建数组副本
this.hobbies = Arrays.copyOf(hobbies, hobbies.length);
}
// 5. 只提供 getter 方法,没有 setter
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 6. 防御性拷贝:返回数组副本,而不是原始引用
public String[] getHobbies() {
return Arrays.copyOf(hobbies, hobbies.length);
}
// 7. 如果需要"修改"状态,必须返回一个新对象
public ImmutableUser withAge(int newAge) {
return new ImmutableUser(this.name, newAge, this.hobbies);
}
}
优点
| 优势 | 说明 |
|---|---|
| 线程安全 | 这是不变模式最核心的优势。因为对象的状态不可变,所以它可以被多个线程同时访问和共享,而完全不需要任何同步机制(如锁),从根本上杜绝了线程安全问题。 |
| 易于理解和维护 | 你不需要关心一个对象在生命周期中会被谁修改、如何修改。一旦拿到一个不变对象,你就可以完全信任它的状态,这极大地简化了程序的逻辑。 |
| 天然支持缓存和共享 | 不变对象可以被安全地缓存和复用。Java 的字符串常量池就是一个绝佳的例子,相同的字符串字面量会指向内存中的同一个对象,节省了大量空间。 |
| 作为 Map 的键非常安全 | 不变对象非常适合作为 HashMap 或 HashSet 的键,因为它的 hashCode 在创建时就确定了,并且永远不会改变,保证了哈希集合的稳定性和正确性。 |
应用场景
| 场景 | 说明 |
|---|---|
Java 的 String 类 |
这是不变模式最经典的应用。它保证了字符串在作为类名、路径、网络地址或 Map 键时的安全性和稳定性。 |
| 基本数据类型的包装类 | Integer, Long, Boolean 等所有包装类都是不可变的,这保证了它们作为值对象的纯粹性。 |
| 高并发环境下的共享数据 | 在多线程环境中,任何需要被共享的配置信息、常量或值对象,都应该设计成不可变的,以避免复杂的锁竞争。 |
| 领域驱动设计(DDD)中的值对象 | 在 DDD 中,值对象(如地址、金钱)的核心特征就是不可变性,它们通过属性来定义,而不是通过ID。 |
总结
不变模式是构建健壮、安全、易维护系统的基石,尤其是在并发场景下。
核心原则 :对象创建后,状态永不改变。
最佳实践 :优先将值对象、常量、配置信息设计为不可变类。
一句话心法 :如果你希望一个对象可以被安全地共享 且无需担心被意外修改 ,请使用不变模式。
模版模式
模板方法模式(Template Method Pattern)是一种行为型设计模式。
它的核心思想非常直观:在一个抽象类中定义一个算法的骨架(流程),将某些步骤的具体实现延迟到子类中完成。
角色
1、抽象类(AbstractClass)
- 模板方法 :定义算法的骨架,按顺序调用其他方法。通常被声明为
final,防止子类修改流程。 - 抽象方法:定义算法中可变的步骤,由子类实现。
- 钩子方法:可选的步骤,提供默认实现(通常为空),子类可以选择性地覆盖它来改变逻辑。
2、具体类(ConcreteClass)
- 实现抽象类中定义的抽象方法,完成算法中特定步骤的具体逻辑。
使用示例
java
// 抽象类:饮料制作模板
abstract class CaffeineBeverage {
// 1. 模板方法:定义算法骨架,final 防止子类修改
public final void prepareRecipe() {
boilWater(); // 步骤1:烧水(公共步骤)
brew(); // 步骤2:冲泡(抽象步骤)
pourInCup(); // 步骤3:倒入杯中(公共步骤)
addCondiments(); // 步骤4:添加调料(抽象步骤)
if (customerWantsCondiments()) { // 步骤5:钩子方法控制
addCondiments();
}
}
// 公共步骤:所有饮料都一样
void boilWater() {
System.out.println("烧开水...");
}
// 公共步骤
void pourInCup() {
System.out.println("把水倒入杯中...");
}
// 抽象步骤:子类必须实现
abstract void brew();
// 抽象步骤:子类必须实现
abstract void addCondiments();
// 钩子方法:子类可以选择性覆盖,默认返回 true
boolean customerWantsCondiments() {
return true;
}
}
// 具体类:咖啡
class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("用沸水冲泡咖啡粉");
}
@Override
void addCondiments() {
System.out.println("加入糖和牛奶");
}
// 覆盖钩子方法,询问用户是否需要调料
@Override
boolean customerWantsCondiments() {
System.out.println("请问需要加糖和牛奶吗?(y/n)");
// 假设用户输入了 'y'
return true;
}
}
// 具体类:茶
class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("用沸水浸泡茶叶");
}
@Override
void addCondiments() {
System.out.println("加入柠檬");
}
}
// 客户端调用
public class Shop {
public static void main(String[] args) {
System.out.println("=== 制作咖啡 ===");
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
System.out.println("\n=== 制作茶 ===");
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
}
}
优点
| 优势 | 说明 |
|---|---|
| 代码复用 | 将公共的逻辑(如烧水、倒入杯中)提取到父类中,避免代码重复。 |
| 反向控制 | 父类控制算法流程,子类只负责填充细节。这被称为"好莱坞原则"(Don't call us, we'll call you)。 |
| 易于扩展 | 如果需要增加一种新饮料(如可可),只需继承抽象类并实现特定方法,无需修改现有代码,符合开闭原则。 |
| 维护简单 | 算法的通用逻辑集中在父类,修改流程只需改一处。 |