Java设计模式

单例模式

单例模式(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 关键字修饰 instanceinstance = 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 推荐的方式。

常见应用场景

  1. 资源共享

    • 数据库连接池:避免频繁创建和销毁连接。
    • 线程池:复用线程,减少开销。
    • 日志记录器:保证日志写入的一致性。
  2. 配置管理

    • 读取配置文件(如 application.properties)的配置管理器,确保全系统读取的配置一致。
  3. 系统工具

    • Windows 的任务管理器、打印机后台处理服务(Spooler)。

代理模式

代理模式(Proxy Pattern)是 Java 开发中极其重要的一种结构型设计模式。它是 Spring AOP、RPC 框架、事务管理等核心技术的底层基石。

简单来说,代理模式的核心思想是:为某个对象提供一个代理(代用)对象,由代理对象控制对目标对象的访问。

角色和结构

  1. 抽象主题(Subject):定义真实对象和代理对象的共同接口。确保代理对象可以在任何需要真实对象的地方被使用(里氏替换原则)。
  2. 真实主题(RealSubject):真正执行业务逻辑的对象,是代理的最终调用目标。
  3. 代理对象(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-elseswitch 判断来创建不同的产品。

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 关键字重新实例化。

角色

  1. Prototype(抽象原型) :声明克隆自己的接口(在 Java 中通常是实现 Cloneable 接口)。
  2. ConcretePrototype(具体原型):实现克隆操作的具体类。
  3. 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 操作和反射,比直接 newclone() 慢;
  • 限制多 :所有关联类必须实现 Serializable,且无法拷贝 transient 字段。

比对

特性 浅拷贝 深拷贝
基本数据类型 复制值 复制值
引用数据类型 复制地址(指向同一块内存) 复制新对象(指向新内存)
修改新对象属性 会影响原对象 不会影响原对象
实现难度 简单(默认支持) 复杂(需手动处理)
性能开销 高(需要创建更多对象)

生产消费模式

生产消费模式(Producer-Consumer Pattern)是并发编程和系统架构中最经典、最基础的解耦模式之一。

它的核心思想非常直观:引入一个"中间层"(缓冲区)来平衡"生产者"和"消费者"的处理速度差异,从而解除两者之间的直接耦合。

角色

  1. 生产者 :负责生成数据或任务;当缓冲区满时,必须阻塞等待,不能无限生产导致内存溢出。
  2. 消费者 :负责处理数据或执行任务;当缓冲区空时,必须阻塞等待,不能空转浪费 CPU 资源。
  3. 缓冲区核心组件 。通常是一个线程安全的队列 (如 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();
    }
}

注意事项

  1. 队列溢出

    • 如果生产者速度远大于消费者,且队列有界(如 ArrayBlockingQueue),生产者会一直阻塞,甚至导致整个系统吞吐量下降(背压效应)。
    • 对策:合理设置队列大小,或者使用无界队列(需谨慎内存溢出)。
  2. 数据丢失

    • 如果消费者在处理过程中崩溃,队列中的消息可能会丢失(取决于队列是内存型还是持久化型)。
    • 对策:使用持久化的消息队列(如 Kafka)。
  3. 死锁风险

    • 如果在同步块中调用 puttake,可能会导致死锁。
    • 对策 :尽量使用 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);
    }
}

优点

优势 说明
符合开闭原则 增加一种新的支付方式(如"数字人民币"),只需新增一个类实现接口,不需要修改现有的 ShoppingCartClient 代码。
避免多重条件 彻底消除了难以维护的 if-else 嵌套,代码结构清晰。
算法复用 具体的策略类(如 WechatPay)可以在不同的上下文中复用。
解耦 上下文(购物车)不需要知道具体策略的实现细节,只通过接口交互。

应用场景

场景 说明
排序算法 Arrays.sort()Collections.sort()。你可以根据需要传入不同的 Comparator(策略),比如按年龄排序、按姓名排序。
压缩算法 压缩文件时,可以选择 ZipCompressorRarCompressor7zCompressor
电商促销 计算价格时,根据活动类型选择 NormalDiscount(日常9折)、Double11Discount(双11五折)或 VipDiscount(会员价)。
登录方式 用户登录时,可以选择 PasswordLogin(密码登录)、SmsLogin(短信验证码)或 WechatLogin(微信扫码)。
Spring 框架 Spring 的 Resource 接口(ClassPathResource, FileSystemResource)也是策略模式的一种体现,用于处理不同来源的资源加载。

总结

策略模式是"消除 if-else 地狱"的利器。

核心原则 :封装变化,多用组合,少用继承。
最佳实践 :当你的代码中出现大量的 if (type == A) ... else if (type == B) 时,请优先考虑使用策略模式。
一句话心法 :如果你有多种做法 去完成同一件事 ,并且需要随时切换 ,请用策略模式

  1. 提供一种凭据式的设计方案。
  2. 服务程序不等数据处理完成便立即返回客户端一个伪造的数据,实现了Future模式的客户端在拿到这个返回结果后,并不急于对其进行处理,而去调用了其他业务逻辑,充分利用了等待时间。

Future模式

Future模式,也称为异步编程模式,是一种并发设计模式

它的核心思想非常直观:当你发起一个耗时操作时,不立即等待结果,而是立刻拿到一个"凭证"或"占位符",然后继续做其他事情。等到真正需要结果时,再通过这个"凭证"去获取。

角色

1、**Future(未来对象/凭证):**这是模式的核心。它是一个接口或对象,代表了异步计算的结果。它提供了检查任务是否完成、等待任务完成和获取结果的方法。

2、**Task(任务):**实际执行耗时操作的逻辑。在Java中,通常是 CallableRunnable 接口的实现。

3、**Client(客户端):**任务的发起者。它提交任务,获得 Future 对象,然后可以继续执行其他操作。在需要结果时,调用 Futureget() 方法。

使用示例

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 的键非常安全 不变对象非常适合作为 HashMapHashSet 的键,因为它的 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)。
易于扩展 如果需要增加一种新饮料(如可可),只需继承抽象类并实现特定方法,无需修改现有代码,符合开闭原则。
维护简单 算法的通用逻辑集中在父类,修改流程只需改一处。
相关推荐
计算机学姐1 小时前
基于SpringBoot+Vue的家政服务预约系统【个性化推荐+数据可视化】
java·vue.js·spring boot·后端·mysql·信息可视化·java-ee
一只大袋鼠1 小时前
请求转发vs重定向、同源策略与跨域
java·javaweb·同源策略·请求转发·重定向
小胖java1 小时前
基于LDA主题模型与情感分析的航空客户满意度分析
java·spring boot·spring
左左右右左右摇晃1 小时前
Java并发——Lock锁
java·开发语言·笔记
森林里的程序猿猿2 小时前
导致内存泄漏的ThreadLocal详解
java·jvm·数据结构
Dream_sky分享2 小时前
Excel模板下载(Resources目录下)
java·spring boot·后端
西海天际蔚蓝2 小时前
线上环境接口访问转到本机的一套小工具
java·python
deviant-ART2 小时前
为什么 Java 编译器要求 catch 块显式 return 或 throw
java·开发语言
LJianK12 小时前
《Java 数据分组的四种姿势:从 for 循环到 Stream API》
java·linux·服务器