设计模式的分类
创建型模式:通过隐藏对象创建的细节,避免直接使用 new
关键字实例化对象,从而使程序在判断和创建对象时更具灵活性。常见的模式包括:
- 工厂模式
- 抽象工厂模式
- 单例模式
- 建造者模式
- 原型模式
结构型模式:通过类和接口之间的继承与引用,帮助组织复杂的对象结构。常见的模式有:
- 适配器模式
- 桥接模式
- 过滤器模式
- 组合模式
- 装饰器模式
- 外观模式
- 享元模式
- 代理模式
行为型模式:通过类之间不同的交互方式,帮助实现对象之间的行为协作。常见的模式包括:
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 策略模式
- 模板方法模式
- 访问者模式
软件设计原则有哪些?
设计原则名称 | 简单定义 |
---|---|
开闭原则(OCP) | 对扩展开放,对修改关闭 |
单一职责原则(SRP) | 一个类只负责一个功能领域中的相应职责 |
里氏替换原则(LSP) | 所有引用基类的地方必须能透明地使用其子类的对象 |
依赖倒置原则(DIP) | 依赖于抽象,不能依赖于具体实现 |
接口隔离原则(ISP) | 类之间的依赖关系应该建立在最小的接口上 |
合成/聚合复用原则(C/ARP) | 尽量使用合成/聚合,而不是通过继承达到复用的目的 |
迪米特法则(LKP) | 一个软件实体应当尽可能少的与其他实体发生相互作用 |
⼯⼚模式
说⼀说简单⼯⼚模式
简单⼯⼚模式指由⼀个⼯⼚对象来创建实例,客户端不需要关注创建逻 辑,只需提供传⼊⼯⼚的参数。
适⽤于⼯⼚类负责创建对象较少的情况,缺点是如果要增加新产品,就需 要修改⼯⼚类的判断逻辑,违背开闭原则,且产品多的话会使⼯⼚类⽐较 复杂。
Calendar 抽象类的 getInstance ⽅法,调⽤ createCalendar ⽅法根据不同 的地区参数创建不同的⽇历对象。 Spring 中的 BeanFactory 使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来 获得 Bean 对象。
简单工厂模式示例代码:
java">// 产品接口
interface Product {void operation();
}// 具体产品A
class ConcreteProductA implements Product {@Overridepublic void operation() {System.out.println("Operation of ConcreteProductA");}
}// 具体产品B
class ConcreteProductB implements Product {@Overridepublic void operation() {System.out.println("Operation of ConcreteProductB");}
}// 简单工厂
class Factory {public Product createProduct(String productType) {if ("A".equalsIgnoreCase(productType)) {return new ConcreteProductA();} else if ("B".equalsIgnoreCase(productType)) {return new ConcreteProductB();} else {throw new IllegalArgumentException("Unknown product type");}}
}// 客户端代码
public class Main {public static void main(String[] args) {Factory factory = new Factory();Product productA = factory.createProduct("A");productA.operation(); // Output: Operation of ConcreteProductAProduct productB = factory.createProduct("B");productB.operation(); // Output: Operation of ConcreteProductB}
}
解释:
Product
是一个接口,定义了operation()
方法。ConcreteProductA
和ConcreteProductB
分别是实现了Product
接口的具体产品类。Factory
类包含一个createProduct()
方法,根据传入的字符串参数来决定返回哪种具体的产品对象。- 在
Main
类中的客户端代码,调用工厂方法createProduct()
来创建产品并调用其operation()
方法。
⼯⼚⽅法模式了解吗?
和简单⼯⼚模式中⼯⼚负责⽣产所有产品相⽐,⼯⼚⽅法模式将⽣成具体 产品的任务分发给具体的产品⼯⼚。
也就是定义⼀个抽象⼯⼚,其定义了产品的⽣产接⼝,但不负责具体的产 品,将⽣产任务交给不同的派⽣类⼯⼚。这样不⽤通过指定类型来创建对 象了。
工厂方法模式示例代码:
java">// 产品接口
interface Product {void operation();
}// 具体产品A
class ConcreteProductA implements Product {@Overridepublic void operation() {System.out.println("Operation of ConcreteProductA");}
}// 具体产品B
class ConcreteProductB implements Product {@Overridepublic void operation() {System.out.println("Operation of ConcreteProductB");}
}// 工厂接口
abstract class Creator {// 工厂方法,子类需要实现此方法来创建具体产品public abstract Product createProduct();
}// 具体工厂A
class ConcreteCreatorA extends Creator {@Overridepublic Product createProduct() {return new ConcreteProductA();}
}// 具体工厂B
class ConcreteCreatorB extends Creator {@Overridepublic Product createProduct() {return new ConcreteProductB();}
}// 客户端代码
public class Main {public static void main(String[] args) {// 使用工厂A创建产品ACreator creatorA = new ConcreteCreatorA();Product productA = creatorA.createProduct();productA.operation(); // Output: Operation of ConcreteProductA// 使用工厂B创建产品BCreator creatorB = new ConcreteCreatorB();Product productB = creatorB.createProduct();productB.operation(); // Output: Operation of ConcreteProductB}
}
解释:
-
Product
是产品接口,定义了产品的operation()
方法。 -
ConcreteProductA
和ConcreteProductB
是实现了Product
接口的具体产品类。 -
Creator
是抽象工厂类,定义了一个抽象的工厂方法createProduct()
,具体的产品创建工作由子类实现。 -
ConcreteCreatorA
和ConcreteCreatorB
是具体工厂类,分别实现了createProduct()
方法来创建ConcreteProductA
和ConcreteProductB
产品。 -
在
Main
类中,客户端通过使用不同的具体工厂(ConcreteCreatorA
和ConcreteCreatorB
)来创建不同的产品,并调用其operation()
方法。
工厂方法模式与简单工厂模式的区别:
-
在 简单工厂模式 中,工厂类负责创建所有产品,客户端依赖于工厂来决定具体的产品类型。
-
在 工厂方法模式 中,工厂是抽象的,产品的创建由子类来决定,每个具体工厂类负责创建特定的产品。
抽象⼯⼚模式了解吗?
简单⼯⼚模式和⼯⼚⽅法模式不管⼯⼚怎么拆分抽象,都只是针对⼀类产 品,如果要⽣成另⼀种产品,就⽐较难办了! 抽象⼯⼚模式通过在 AbstarctFactory 中增加创建产品的接⼝,并在具体⼦ ⼯⼚中实现新加产品的创建,当然前提是⼦⼯⼚⽀持⽣产该产品。否则继 承的这个接⼝可以什么也不⼲。
从上⾯类图结构中可以清楚的看到如何在⼯⼚⽅法模式中通过增加新产品 接⼝来实现产品的增加的。
抽象方法模式(Abstract Factory Pattern)是另一种创建型设计模式,它提供了一个接口,用于创建一系列相关或相互依赖的对象,而不需要指定它们的具体类。在该模式中,每个具体工厂都负责创建一组相关的产品对象。
抽象工厂模式的示例代码:
假设我们有一个 UI 框架,其中可以有不同的主题(如 Windows 主题和 Mac 主题),每种主题下有不同的按钮和文本框(即产品族)。我们可以使用抽象工厂模式来创建每个主题的按钮和文本框。
java">// 按钮接口
interface Button {void render();void onClick();
}// 文本框接口
interface TextBox {void render();void onType();
}// Windows 系统的按钮
class WindowsButton implements Button {@Overridepublic void render() {System.out.println("Rendering Windows Button");}@Overridepublic void onClick() {System.out.println("Windows Button Clicked");}
}// Windows 系统的文本框
class WindowsTextBox implements TextBox {@Overridepublic void render() {System.out.println("Rendering Windows TextBox");}@Overridepublic void onType() {System.out.println("Typing in Windows TextBox");}
}// Mac 系统的按钮
class MacButton implements Button {@Overridepublic void render() {System.out.println("Rendering Mac Button");}@Overridepublic void onClick() {System.out.println("Mac Button Clicked");}
}// Mac 系统的文本框
class MacTextBox implements TextBox {@Overridepublic void render() {System.out.println("Rendering Mac TextBox");}@Overridepublic void onType() {System.out.println("Typing in Mac TextBox");}
}// 抽象工厂接口
interface GUIFactory {Button createButton();TextBox createTextBox();
}// Windows 系统的工厂
class WindowsFactory implements GUIFactory {@Overridepublic Button createButton() {return new WindowsButton();}@Overridepublic TextBox createTextBox() {return new WindowsTextBox();}
}// Mac 系统的工厂
class MacFactory implements GUIFactory {@Overridepublic Button createButton() {return new MacButton();}@Overridepublic TextBox createTextBox() {return new MacTextBox();}
}// 客户端代码
public class Main {public static void main(String[] args) {// 创建 Windows 系统的 UI 工厂GUIFactory factory = new WindowsFactory();Button windowsButton = factory.createButton();TextBox windowsTextBox = factory.createTextBox();windowsButton.render();windowsButton.onClick();windowsTextBox.render();windowsTextBox.onType();System.out.println();// 创建 Mac 系统的 UI 工厂factory = new MacFactory();Button macButton = factory.createButton();TextBox macTextBox = factory.createTextBox();macButton.render();macButton.onClick();macTextBox.render();macTextBox.onType();}
}
解释:
-
产品接口:
-
Button
和TextBox
分别是按钮和文本框的接口,定义了相应的操作方法。
-
-
具体产品:
-
WindowsButton
,WindowsTextBox
,MacButton
, 和MacTextBox
是具体的产品类,分别实现了Button
和TextBox
接口,用于不同操作系统的 UI 元素。
-
-
抽象工厂:
-
GUIFactory
是一个抽象工厂接口,定义了createButton()
和createTextBox()
方法,负责创建相关的产品。
-
-
具体工厂:
-
WindowsFactory
和MacFactory
分别是具体的工厂类,实现了GUIFactory
接口,负责创建特定操作系统的按钮和文本框。
-
-
客户端代码:
-
在
Main
类的客户端代码中,我们根据不同的操作系统创建相应的工厂,并通过工厂创建产品(按钮和文本框)。每个工厂创建的一系列产品是兼容的,并且与操作系统相关。
-
抽象工厂模式的优点:
-
一致性:抽象工厂模式确保了每个产品族(如按钮、文本框等)之间的兼容性,避免了客户端在不同产品之间的错误组合。
-
可扩展性:添加新产品族(例如 Linux 系统)时,只需要创建新的工厂和具体产品类,不需要修改已有的客户端代码。
与工厂方法模式的区别:
-
工厂方法模式 是用于创建单个产品的工厂方法,而 抽象工厂模式 是用来创建一组相关或依赖的产品族。
-
工厂方法模式可以有多个工厂,每个工厂负责创建一个类型的产品,而抽象工厂模式每个工厂负责创建一整套相关的产品(例如按钮、文本框等)。
对比总结:
特性 | 简单工厂模式 | 工厂方法模式 | 抽象工厂模式 |
---|---|---|---|
创建的产品类型 | 一个工厂可以创建多个不同类型的产品 | 每个工厂创建单一类型的产品 | 每个工厂创建一系列相关产品 |
扩展性 | 扩展较困难,新增产品需要修改工厂类 | 通过继承和多态扩展产品类型 | 通过增加新的工厂扩展产品族 |
复杂度 | 简单,只需要一个工厂类 | 需要多个工厂类和子类 | 较为复杂,涉及多个工厂和产品类 |
适用场景 | 产品类型较少,且变化不频繁 | 产品类型较多,且可能需要扩展 | 需要创建多个相关或依赖的产品 |
代码维护性 | 产品种类增加时,工厂类会变得庞大 | 易于维护,但需要多个工厂类 | 维护成本较高,但易于扩展产品族 |
单例模式
什么是单例模式?单例模式的特点是什么?
单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例, 构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例,对外提供⼀ 个静态公有⽅法获取实例。 优点是内存中只有⼀个实例,减少了开销,尤其是频繁创建和销毁实例的 情况下并且可以避免对资源的多重占⽤。缺点是没有抽象层,难以扩展, 与单⼀职责原则冲突。
单例模式的常⻅写法有哪些?
饿汉式单例模式(Eager Singleton)
定义:
饿汉式单例模式是在类加载时就立即创建实例,确保该类只有一个实例存在,并且该实例在整个程序运行过程中是唯一的。类加载时就创建对象,通常是通过 静态常量 或 静态代码块 来实现。
优点:
-
线程安全:饿汉式单例模式保证了线程安全,因为类加载时实例已经被创建,并且类加载是线程安全的。
-
执行效率较高:因为不需要加锁,所以创建实例时的性能优于其他单例实现(例如,懒汉式单例模式)。
缺点:
-
不是懒加载:类加载时就初始化对象,导致不管是否使用该实例,都会占用内存空间。
-
可能会浪费内存:如果实例从未被使用,类加载时就会创建该实例,造成不必要的内存浪费。
如何保证线程安全:
饿汉式单例模式通过 类加载机制 保证了线程安全。类加载器 在加载类时会确保类的静态成员(例如单例实例)在内存中只有一份,而且类加载本身是线程安全的。因此,多个线程并发访问时,饿汉式单例模式可以避免出现多个实例。
潜在问题:
-
如果类被 不同的类加载器 加载,就可能创建多个不同的实例。这是因为每个类加载器会有自己独立的类加载机制,因此会导致不同的实例。
饿汉式单例模式示例代码:
java">public class EagerSingleton {// 1. 类加载时就创建唯一实例private static final EagerSingleton instance = new EagerSingleton();// 2. 构造器私有化,避免外部直接实例化private EagerSingleton() {// 防止反射破坏单例if (instance != null) {throw new IllegalStateException("Instance already created");}}// 3. 提供公共的获取实例方法public static EagerSingleton getInstance() {return instance;}// 示例方法public void showMessage() {System.out.println("Hello from Eager Singleton!");}public static void main(String[] args) {// 获取唯一的实例EagerSingleton singleton = EagerSingleton.getInstance();singleton.showMessage(); // 输出:Hello from Eager Singleton!}
}
解释:
-
静态实例:
private static final EagerSingleton instance = new EagerSingleton();
-
这个静态实例在类加载时就会被初始化,因此是饿汉式单例模式。
-
-
私有构造器:
private EagerSingleton()
-
防止外部直接创建实例,确保只能通过
getInstance()
方法来获取单例。 -
通过
if (instance != null)
语句防止通过反射破坏单例模式。
-
-
静态获取实例的方法:
public static EagerSingleton getInstance()
-
返回静态实例
instance
,因为实例是类加载时就创建的,所以直接返回即可。
-
使⽤反射破坏单例,代码如下:
java">import java.lang.reflect.Constructor;public class EagerSingleton {// 1. 类加载时就创建唯一实例private static final EagerSingleton instance = new EagerSingleton();// 2. 构造器私有化,避免外部直接实例化private EagerSingleton() {// 防止反射破坏单例if (instance != null) {throw new IllegalStateException("Instance already created");}}// 3. 提供公共的获取实例方法public static EagerSingleton getInstance() {return instance;}// 示例方法public void showMessage() {System.out.println("Hello from Eager Singleton!");}public static void main(String[] args) {try {// 获取饿汉式单例的类类型Class<?> clazz = Class.forName("EagerSingleton");// 获取构造器,设置可以访问私有构造器Constructor<?> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 使用反射破坏单例,创建新的实例EagerSingleton newInstance = (EagerSingleton) constructor.newInstance();// 输出两个实例是否相同EagerSingleton singleton = EagerSingleton.getInstance();System.out.println(singleton == newInstance); // 输出 false} catch (Exception e) {e.printStackTrace();}}
}
解释:
-
反射获取构造器:
-
使用
clazz.getDeclaredConstructor()
获取类的构造器。 -
使用
constructor.setAccessible(true)
将私有构造器设置为可访问。
-
-
创建新实例:
-
EagerSingleton newInstance = (EagerSingleton) constructor.newInstance();
通过反射绕过私有构造器,创建了一个新的实例。
-
-
验证是否创建了不同的实例:
-
System.out.println(singleton == newInstance);
输出false
,证明反射创建了一个新的对象,破坏了单例模式。
-
结果:
-
通过反射,能够直接调用私有构造器并创建新的实例,从而打破了单例模式。
-
上述代码输出
false
,证明反射创建了一个新的实例。
解决方案:
为了防止反射破坏单例,可以在构造器中增加判断逻辑,确保如果实例已经创建,抛出异常。例如在构造器中加入 if (instance != null)
来防止多次创建。这样,如果反射尝试创建新实例,会抛出 IllegalStateException
。
懒汉式单例模式(Lazy Singleton)
定义:
懒汉式单例模式指的是在需要使用单例对象时,才会创建该对象实例。换句话说,只有当实例第一次被调用时,才会进行初始化。这样可以实现 懒加载(lazy loading),即在程序启动时不立即创建对象,而是延迟到使用时再创建。
优点:
-
懒加载:只有在真正需要使用单例实例时,才会进行实例化,节省了内存空间,避免了不必要的对象创建。
缺点:
-
线程不安全:在多线程环境下,多个线程同时调用
getInstance()
方法时,可能会导致多个实例的创建,无法保证单例性。因此,在多线程场景下是 不安全 的。
线程不安全的原因:
懒汉式单例模式在第一次调用时创建实例,如果多个线程同时进入 getInstance()
方法并发现实例为空,就会同时创建多个实例,导致违反单例模式的原则,造成 线程不安全。
懒汉式单例模式示例代码(线程不安全)
java">public class LazySingleton {// 1. 声明一个私有静态实例,不进行初始化private static LazySingleton instance;// 2. 构造器私有化,防止外部直接创建实例private LazySingleton() {}// 3. 提供公共的静态方法,获取单例对象public static LazySingleton getInstance() {if (instance == null) { // 第一次访问时才创建实例instance = new LazySingleton();}return instance;}// 示例方法public void showMessage() {System.out.println("Hello from Lazy Singleton!");}public static void main(String[] args) {// 获取唯一的实例LazySingleton singleton = LazySingleton.getInstance();singleton.showMessage(); // 输出:Hello from Lazy Singleton!}
}
解释:
-
通过
getInstance()
方法在第一次使用时创建实例,这是懒汉式的 懒加载。 -
但是,线程不安全,多个线程同时调用
getInstance()
方法时,可能会导致多个实例的创建,无法保证单例模式。
多线程破坏懒汉式单例的示例代码
在多线程环境中,多个线程同时调用 getInstance()
方法时,会同时进入 if (instance == null)
检查,并且都通过创建新实例,导致破坏单例模式。
java">public class MultiThreadLazySingleton {// 1. 声明一个私有静态实例,不进行初始化private static MultiThreadLazySingleton instance;// 2. 构造器私有化,防止外部直接创建实例private MultiThreadLazySingleton() {}// 3. 提供公共的静态方法,获取单例对象public static MultiThreadLazySingleton getInstance() {if (instance == null) { // 第一次访问时才创建实例instance = new MultiThreadLazySingleton();}return instance;}// 示例方法public void showMessage() {System.out.println("Hello from MultiThreadLazySingleton!");}public static void main(String[] args) {// 模拟多线程环境Runnable task = () -> {MultiThreadLazySingleton singleton = MultiThreadLazySingleton.getInstance();System.out.println(Thread.currentThread().getName() + " : " + singleton);};// 创建多个线程Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);// 启动线程thread1.start();thread2.start();}
}
输出结果(可能的):
java">Thread-0 : MultiThreadLazySingleton@15db9742
Thread-1 : MultiThreadLazySingleton@6d06d69c
解释:
-
在上面的代码中,两个线程
Thread-0
和Thread-1
同时调用getInstance()
方法,没有加锁,因此可能会在同一时刻创建两个不同的实例。 -
输出的两个实例的哈希码不同,证明不同的线程创建了不同的实例,从而破坏了单例模式。
问题所在:
-
线程不安全:多个线程并发执行时,可能同时进入
getInstance()
方法并发现instance
为null
,于是分别创建了各自的实例。
懒汉式单例模式(线程安全)
定义:
懒汉式单例模式在第一次需要使用时才会创建实例,即采用 懒加载。为了确保在多线程环境下的安全性,需要使用 synchronized
来加锁,避免多个线程同时创建多个实例。
优点:
-
懒加载:只有在首次访问时,才会创建实例,节省了内存空间。
-
线程安全:使用
synchronized
关键字加锁,确保多线程环境中只有一个实例被创建,避免多个线程同时创建不同实例的情况。
缺点:
-
性能开销大:由于每次调用
getInstance()
时都需要加锁,会显著影响性能,尤其是在多线程高并发的场景中。
懒汉式单例模式(线程安全)的代码示例
java">public class Singleton {// 1. 声明一个私有静态实例,不进行初始化private static Singleton instance;// 2. 构造器私有化,防止外部直接创建实例private Singleton() {}// 3. 提供公共的静态方法,获取单例对象public static synchronized Singleton getInstance() {if (instance == null) { // 第一次访问时才创建实例instance = new Singleton();}return instance;}// 示例方法public void showMessage() {System.out.println("Hello from Singleton!");}public static void main(String[] args) {// 模拟多线程环境for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println("多线程创建的单例:" + Singleton.getInstance());}).start();}}
}
解释:
-
getInstance()
方法加锁:为了确保线程安全,getInstance()
方法使用了synchronized
关键字。这确保了在多线程环境下,每次调用该方法时,只有一个线程可以进入并创建实例。 -
多线程测试:在
main
方法中,启动了 3 个线程,每个线程都会尝试获取单例实例并打印出来。由于加了synchronized
锁,所有线程共享同一个单例实例,输出的哈希码应该是相同的。
输出结果(期望):
java">多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
双重检查锁(DCL,Double-Checked Locking)是一种优化懒汉式单例模式线程安全性的方法,目的是减少同步带来的性能开销。
双重检查锁定(DCL)实现代码
java">public class Singleton {// 1. 声明一个私有静态实例,但不进行初始化private static volatile Singleton instance;// 2. 构造器私有化,防止外部直接创建实例private Singleton() {}// 3. 提供公共的静态方法,获取单例对象public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) { // 加锁if (instance == null) { // 第二次检查instance = new Singleton();}}}return instance;}// 示例方法public void showMessage() {System.out.println("Hello from Singleton!");}public static void main(String[] args) {// 模拟多线程环境for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println("多线程创建的单例:" + Singleton.getInstance());}).start();}}
}
解释:
-
instance
声明为volatile
:-
volatile
关键字确保实例在多线程环境中正确地被共享,避免出现实例在内存中的可见性问题。 -
使用
volatile
可以防止 JVM 对实例的创建过程进行指令重排,保证实例的创建过程是 原子性 和 有序 的。
-
-
双重检查锁定:
-
第一次检查:在没有锁的情况下,检查
instance
是否为null
,如果是null
,则进入同步块进行实例化。 -
第二次检查:在进入同步块后,重新检查
instance
是否为null
,这样可以避免多个线程重复创建实例。 -
加锁范围缩小:只对实例化过程加锁,避免了每次调用时都加锁,从而提高了性能。
-
-
性能优化:
-
双重检查锁定的关键是,在第一次检查时,避免了无谓的加锁操作,从而减少了锁的竞争,提高了性能。
-
输出结果(期望):
java">多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
多线程创建的单例:com.example.spring.demo.single.Singleton@1a2b3c4d
双重检查锁定(DCL)分析与原因
1. 双重检查的必要性:
-
第一次检查:为了避免每次调用
getInstance()
时都进入同步块进行加锁。如果实例已经存在,直接返回实例,从而避免了不必要的性能开销。 -
第二次检查:当线程进入同步块后,为了防止多个线程在同步块内创建多个实例,进行第二次检查,确保
instance
仍然是null
。只有在第二次检查为null
时,才会创建实例。
2. volatile
关键字的作用:
-
可见性:
volatile
确保一个线程对instance
变量的修改对于其他线程是可见的。即,当一个线程修改instance
时,其他线程能够立刻看到这个变化,避免了出现旧值。 -
禁止指令重排序:由于 JVM 和 CPU 的优化,通常会对程序中的指令进行重排序,以提高性能。然而,
new
关键字初始化对象时的多个步骤(例如分配内存、调用构造方法、引用赋值)是不能被乱序的,否则会导致多个线程在某些情况下获取未初始化的对象。volatile
禁止了这种乱序,确保了对象的初始化过程在多线程环境中的顺序性。
3. 为什么使用 volatile
:
这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯
的步骤:
1. 在堆内存开辟内存空间
2. 调⽤构造⽅法,初始化对象
3. 引⽤变量指向堆内存空间
对应字节码指令如下:
为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重 排序,从源码到最终执⾏指令会经历如下流程: 源码编译器优化重排序指令级并⾏重排序内存系统重排序最终执⾏指令序 列 所以经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3 2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存 空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进 ⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使 ⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。
当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令 的前后添加内存屏障来禁⽌指令重排序,就可以避免这个问题,⽽且对 volatile 修饰的变量的修改对其他任何线程都是可⻅的。
静态内部类实现单例模式(推荐)
除了双重检查锁定之外,使用 静态内部类 是一种更简洁且高效的单例模式实现方法。
实现代码:
java">public class Singleton {// 1. 私有化构造器,防止外部创建实例private Singleton() {}// 2. 静态内部类,JVM保证它的懒加载和线程安全private static class SingletonHelper {// 静态初始化器创建单例实例private static final Singleton INSTANCE = new Singleton();}// 3. 提供公共的静态方法,获取单例对象public static Singleton getInstance() {return SingletonHelper.INSTANCE;}// 示例方法public void showMessage() {System.out.println("Hello from Singleton!");}public static void main(String[] args) {// 模拟多线程环境for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println("多线程创建的单例:" + Singleton.getInstance());}).start();}}
}
解释:
-
静态内部类实现:
-
延迟加载:
SingletonHelper
类只有在调用getInstance()
方法时才会被加载,因此实例会被延迟创建。 -
线程安全:
SingletonHelper
是 静态内部类,它是由 JVM 在类加载时保证线程安全地初始化的,无需加锁。 -
性能优势:相比于使用
synchronized
或双重检查锁定,这种方式不需要加锁,也避免了性能开销。
-
优点:
-
线程安全:JVM保证了类的加载和初始化的线程安全性。
-
懒加载:实例只有在第一次调用
getInstance()
时才会创建。 -
性能优越:无需使用
synchronized
,也不需要通过volatile
来保证实例的正确性,性能较高。
java 类初始化的 5 种情况
-
使用
new
、getstatic
、putstatic
、invokestatic
字节码指令时:-
当遇到这四条字节码指令时,JVM 会触发类的初始化。这些字节码指令对应着以下几种常见的 Java 代码:
-
new
:通过new
关键字实例化对象时,类会被初始化。 -
getstatic
:访问类的静态字段时(除final
字段外),会触发类的初始化。 -
putstatic
:给类的静态字段赋值时(除final
字段外),会触发类的初始化。 -
invokestatic
:调用类的静态方法时,类会被初始化。
-
-
特别地,
final
修饰的静态字段会在编译时进行常量替换,所以不会触发类初始化。
-
-
通过
java.lang.reflect
包的反射方法调用时:-
当使用反射机制(例如
Class.forName()
)来加载类时,JVM 会触发类的初始化。 -
通过反射访问类的构造方法、静态字段、静态方法等,也会触发类的初始化。
-
-
初始化一个类时,如果发现其父类还没有初始化,则需要先触发父类的初始化:
-
如果一个类的初始化依赖于其父类(如父类的静态字段或方法),那么在初始化当前类之前,JVM 会先初始化父类。
-
-
当虚拟机启动时,用户指定要执行的主类时:
-
在启动 Java 程序时,JVM 会加载并初始化包含
main()
方法的主类,以便开始执行程序。
-
-
在 JDK 1.7 动态语言支持时:
-
在 JDK 1.7 引入的动态语言支持中,如果一个方法句柄(
MethodHandle
)的解析结果是REF_getStatic
、REF_putStatic
或REF_invokeStatic
,则会先触发该方法句柄所对应类的初始化。
-
主动引用 vs 被动引用
-
主动引用:
-
按照 《虚拟机规范》,类的初始化必须通过以下五种情况之一来触发,这些情况被称为 类的主动引用:
-
使用
new
、getstatic
、putstatic
、invokestatic
指令。 -
通过反射机制调用类时。
-
初始化类时,发现父类未初始化,需先初始化父类。
-
程序启动时初始化主类(包含
main()
方法的类)。 -
JDK 1.7 的动态语言支持时,方法句柄解析时会触发类初始化。
-
-
-
被动引用:
-
被动引用指的是类在没有通过上述五种情况之一来主动引用时,不会进行初始化。换句话说,类的初始化会被推迟。
-
静态内部类就是一个典型的被动引用的例子。它的初始化只有在真正访问静态字段时才会发生。
-
静态内部类如何实现线程安全的单例
在静态内部类的单例模式中,类的初始化是 被动的,只有在第一次访问时才会发生。具体来说:
-
延迟加载(懒加载):静态内部类
InnerClass
中的静态字段INSTANCE
只有在调用getInstance()
方法时才会被初始化。这确保了实例化操作是 懒加载 的,不会在类加载时就创建实例。 -
线程安全:
INSTANCE
的创建是线程安全的,因为:-
JVM 会保证静态内部类的初始化是线程安全的。即使多个线程同时访问
getInstance()
方法,只有一个线程会成功执行InnerClass.INSTANCE
的初始化。 -
在类的初始化过程中(例如
<clinit>()
方法执行时),JVM 会自动加锁,保证 同一时刻只有一个线程 能够执行类的初始化,其他线程会等待,直到初始化完成。
-
-
单例的唯一性:由于类的静态成员
INSTANCE
在类的初始化时被创建,而类的初始化只有在第一次访问时发生,所以INSTANCE
在类的生命周期内是唯一的。这保证了单例的 唯一性。 -
避免锁的开销:与传统的线程安全单例模式(如
synchronized
锁)不同,静态内部类实现的单例模式避免了每次访问getInstance()
时都加锁的性能开销。它通过 类加载机制 来实现线程安全和延迟加载,且只有第一次访问时才会进行同步和对象创建。
JVM 对类初始化的控制
-
同步性:在多线程环境中,如果多个线程同时访问静态内部类
SingletonHelper
,只有一个线程会执行类的初始化。JVM 会确保 类的初始化是同步的,避免出现多个线程同时创建实例的情况。 -
阻塞与唤醒:当一个线程正在执行类的初始化时,其他线程会被阻塞。初始化完成后,其他线程会唤醒并直接返回已经创建的实例。
总结
-
静态内部类实现单例模式:
-
保证线程安全。
-
延迟加载实例(懒加载)。
-
保证单例的唯一性。
-
避免了同步锁带来的性能开销。
-
利用了 JVM 的类加载机制,确保只有在首次访问时才会创建实例。
-
枚举单例
代码实现如下:
java">public enum Singleton {INSTANCE;public void doSomething() {System.out.println("Doing something...");}
}
优点总结:
-
简单:枚举单例的代码简洁易懂,避免了传统单例模式中涉及的锁和同步问题。
-
高效:不需要加锁,每次访问实例时都可以直接获取,不涉及额外的同步开销。
-
线程安全:Java 枚举类型在 JVM 中有内建的机制来确保线程安全,枚举实例只有在类加载时被创建,并且创建过程是线程安全的。
-
避免通过反射破坏单例:通过枚举的特性,反射无法破坏枚举单例,因为 Java 枚举会强制保证枚举实例的唯一性。
获取枚举单例实例的方式:
java">public class Test {public static void main(String[] args) {// 获取枚举单例实例Singleton singleton = Singleton.INSTANCE;// 调用实例方法singleton.doSomething();// 验证实例是否相同Singleton anotherSingleton = Singleton.INSTANCE;System.out.println(singleton == anotherSingleton); // 输出 true,证明是同一实例}
}
使⽤下⾯的命令反编译枚举类
java">javap Singleton.class
得到如下内容
java">Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton
extends
java.lang.Enum<com.spring.demo.singleton.Singleton> {public static final
com.spring.demo.singleton.Singleton INSTANCE;public static com.spring.demo.singleton.Singleton[]
values();public static com.spring.demo.singleton.Singleton
valueOf(java.lang.String);public void doSomething(java.lang.String);static {};
}
从枚举的反编译结果可以看到,INSTANCE 被 static final 修饰,所以可以 通过类名直接调⽤,并且创建对象的实例是在静态代码块中创建的,因为 static 类型的属性会在类被加载之后被初始化,当⼀个 Java 类第⼀次被真 正使⽤到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程 安全的,所以创建⼀个 enum 类型是线程安全的。
通过反射破坏枚举,实现代码如下:
java">import java.lang.reflect.Constructor;public class TestReflection {public static void main(String[] args) throws Exception {Singleton singleton1 = Singleton.INSTANCE;// 通过反射获取构造函数Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);Singleton singleton2 = constructor.newInstance("INSTANCE", 0);System.out.println(singleton1 == singleton2); // 输出 false,反射破坏不了枚举单例}
}
运⾏结果报如下错误:
java">Exception in thread "main"
java.lang.IllegalArgumentException: Cannot reflectively
create enum objectsat
java.base/java.lang.reflect.Constructor.newInstanceWithC
aller(Constructor.java:492)at
java.base/java.lang.reflect.Constructor.newInstance(Cons
tructor.java:480)at com.spring.demo.singleton.Test.main(Test.java:24)
查看反射创建实例的 newInstance() ⽅法,有如下判断:
适配器模式
适配器模式了解吗?
在我们的应⽤程序中我们可能需要将两个不同接⼝的类来进⾏通信,在不 修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程。 这个中间件就是适配器。所谓适配器模式就是将⼀个类的接⼝,转换成客 户期望的另⼀个接⼝。它可以让原本两个不兼容的接⼝能够⽆缝完成对 接。 作为中间件的适配器将⽬标类和适配者解耦,增加了类的透明性和可复⽤ 性。
类适配器 原理:通过类继承实现适配,继承 Target 的接⼝,继承 Adaptee 的实现
对象适配器 原理:通过类对象组合实现适配
- Target: 定义 Client 真正需要使⽤的接⼝。
- Adaptee: 其中定义了⼀个已经存在的接⼝,也是我们需要进⾏适配的
- 接⼝。
- Adapter: 对 Adaptee 和 Target 的接⼝进⾏适配,保证对 target 中接⼝的调⽤可以间接转换为对 Adaptee 中接⼝进⾏调⽤。
适配器模式的优缺点
优点:
1. 提⾼了类的复⽤;
2. 组合若⼲关联对象形成对外提供统⼀服务的接⼝;
3. 扩展性、灵活性好。
缺点:
1. 过多使⽤适配模式容易造成代码功能和逻辑意义的混淆。
2. 部分语⾔对继承的限制,可能⾄多只能适配⼀个适配者类,⽽且⽬标类
必须是抽象类
代理模式(proxy pattern)
什么是代理模式?
代理模式的本质是⼀个中间件,主要⽬的是解耦合服务提供者和使⽤者。
使⽤者通过代理间接的访问服务提供者,便于后者的封装和控制。是⼀种
结构性模式。
下⾯是 GoF 介绍典型的代理模式 UML 类图
- Subject: 定义 RealSubject 对外的接⼝,且这些接⼝必须被 Proxy 实现,这样外部调⽤ proxy 的接⼝最终都被转化为对 realsubject 的调⽤。
- RealSubject: 真正的⽬标对象。
- Proxy: ⽬标对象的代理,负责控制和管理⽬标对象,并间接地传递外部对
- ⽬标对象的访问。
- Remote Proxy: 对本地的请求以及参数进⾏序列化,向远程对象发送请求,并对响应结果进⾏反序列化,将最终结果反馈给调⽤者;
- Virtual Proxy: 当⽬标对象的创建开销⽐较⼤的时候,可以使⽤延迟或者异步的⽅式创建⽬标对象;
- Protection Proxy: 细化对⽬标对象访问权限的控制;
静态代理和动态代理的区别
1. 灵活性 :动态代理更加灵活,不需要必须实现接⼝,可以直接代理实
现类,并且可以不需要针对每个⽬标类都创建⼀个代理类。另外,静态
代理中,接⼝⼀旦新增加⽅法,⽬标对象和代理对象都要进⾏修改,这
是⾮常麻烦的!
2. JVM 层⾯ :静态代理在编译时就将接⼝、实现类、代理类这些都变成
了⼀个个实际的 class ⽂件。⽽动态代理是在运⾏时动态⽣成类字节
码,并加载到 JVM 中的。
观察者模式
说⼀说观察者模式
观察者模式主要⽤于处理对象间的⼀对多的关系,是⼀种对象⾏为模式。 该模式的实际应⽤场景⽐较容易确认,当⼀个对象状态发⽣变化时,所有 该对象的关注者均能收到状态变化通知,以进⾏相应的处理。 下⾯是 GoF 介绍的典型的类观察者模式的 UML 类图:
- Subject: 抽象被观察者,仅提供注册和删除观察者对象的接⼝声明。
- ConcreteSubject: 具体被观察者对象,该对象中收集了所有需要被通知的
- 观察者,并可以动态的增删集合中的观察者。当其状态发⽣变化时会通知所有观察者对象。
- Observer: 抽象观察者,为所有观察者定义获得通知的统⼀接⼝;
- ConcreteObserver: 观察者对象,其关注对象为 Subject,能接受 Subject变化时发出的通知并更新⾃身状态。
观察者模式的优缺点
优点:
1. 被观察者和观察者之间是抽象耦合的;
2. 耦合度较低,两者之间的关联仅仅在于消息的通知;
3. 被观察者⽆需关⼼他的观察者;
4. ⽀持⼴播通信;
缺点:
1. 观察者只知道被观察对象发⽣了变化,但不知变化的过程和缘由;
2. 观察者同时也可能是被观察者,消息传递的链路可能会过⻓,完成所有
通知花费时间较多;
3. 如果观察者和被观察者之间产⽣循环依赖,或者消息传递链路形成闭
环,会导致⽆限循环;
装饰器模式
什么是装饰器模式?
装饰器模式主要对现有的类对象进⾏包裹和封装,以期望在不改变类对象
及其类定义的情况下,为对象添加额外功能。是⼀种对象结构型模式。需
要注意的是,该过程是通过调⽤被包裹之后的对象完成功能添加的,⽽不
是直接修改现有对象的⾏为,相当于增加了中间层。
下⾯是 GoF 介绍的典型的装饰器模式的 UML 类图:
- Component: 对象的接⼝类,定义装饰对象和被装饰对象的共同接⼝;
- ConcreteComponent: 被装饰对象的类定义;
- Decorator: 装饰对象的抽象类,持有⼀个具体的被修饰对象,并实现接⼝类继承的公共接⼝;
- ConcreteDecorator: 具体的装饰器,负责往被装饰对象添加额外的功能;
讲讲装饰器模式的应⽤场景
如果你希望在⽆需修改代码的情况下即可使⽤对象, 且希望在运⾏时为对 象新增额外的⾏为, 可以使⽤装饰模式。 装饰能将业务逻辑组织为层次结构, 你可为各层创建⼀个装饰, 在运⾏时 将各种不同逻辑组合成对象。 由于这些对象都遵循通⽤接⼝, 客户端代码 能以相同的⽅式使⽤这些对象。 如果⽤继承来扩展对象⾏为的⽅案难以实现或者根本不可⾏, 你可以使⽤ 该模式。 许多编程语⾔使⽤ final 最终关键字来限制对某个类的进⼀步扩展。 复⽤ 最终类已有⾏为的唯⼀⽅法是使⽤装饰模式: ⽤封装器对其进⾏封装。
责任链模式
什么是责任链模式?
个请求沿着⼀条“链”传递,直到该“链”上的某个处理者处理它为⽌。 ⼀个请求可以被多个处理者处理或处理者未明确指定时。 责任链模式⾮常简单异常好理解,相信我它⽐单例模式还简单易懂,其应 ⽤也⼏乎⽆所不在,甚⾄可以这么说,从你敲代码的第⼀天起你就不知不觉 ⽤过了它最原始的裸体结构: switch-case 语句。
讲讲责任链模式的应⽤场景
当程序需要使⽤不同⽅式处理不同种类请求, ⽽且请求类型和顺序预
先未知时, 可以使⽤责任链模式。该模式能将多个处理者连接成⼀条
链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进⾏处理。
这样所有处理者都有机会来处理请求。
当必须按顺序执⾏多个处理者时, 可以使⽤该模式。 ⽆论你以何种顺
序将处理者连接成⼀条链, 所有请求都会严格按照顺序通过链上的处
理者。
责任链模式示例代码
java">// 抽象处理者
abstract class Handler {protected Handler nextHandler;public void setNextHandler(Handler nextHandler) {this.nextHandler = nextHandler;}public abstract void handleRequest(String request);
}// 具体处理者1
class ConcreteHandlerA extends Handler {@Overridepublic void handleRequest(String request) {if (request.equals("RequestA")) {System.out.println("HandlerA handled " + request);} else if (nextHandler != null) {nextHandler.handleRequest(request);}}
}// 具体处理者2
class ConcreteHandlerB extends Handler {@Overridepublic void handleRequest(String request) {if (request.equals("RequestB")) {System.out.println("HandlerB handled " + request);} else if (nextHandler != null) {nextHandler.handleRequest(request);}}
}// 具体处理者3
class ConcreteHandlerC extends Handler {@Overridepublic void handleRequest(String request) {if (request.equals("RequestC")) {System.out.println("HandlerC handled " + request);} else if (nextHandler != null) {nextHandler.handleRequest(request);}}
}// 客户端代码
public class ChainOfResponsibilityPattern {public static void main(String[] args) {// 创建具体处理者Handler handlerA = new ConcreteHandlerA();Handler handlerB = new ConcreteHandlerB();Handler handlerC = new ConcreteHandlerC();// 设置责任链handlerA.setNextHandler(handlerB);handlerB.setNextHandler(handlerC);// 发送请求handlerA.handleRequest("RequestA"); // Output: HandlerA handled RequestAhandlerA.handleRequest("RequestB"); // Output: HandlerB handled RequestBhandlerA.handleRequest("RequestC"); // Output: HandlerC handled RequestChandlerA.handleRequest("RequestD"); // No handler for RequestD}
}
代码解释
-
Handler 类:这是一个抽象类,定义了所有处理者必须实现的
handleRequest()
方法。它还包含一个nextHandler
属性,用于指向下一个处理者。这样就形成了一个处理链。 -
具体处理者(ConcreteHandlerA、ConcreteHandlerB、ConcreteHandlerC):每个具体处理者会检查请求的内容,并决定是否处理该请求。如果可以处理请求,它就会处理并停止传递。如果不能处理,就将请求传递给下一个处理者(如果有的话)。
-
客户端代码:在
main()
方法中,我们创建了三个具体的处理者,并通过setNextHandler()
方法将它们连接起来,形成一个责任链。当客户端请求某个处理时,责任链会依次传递请求,直到某个处理者处理完该请求。
输出
java">HandlerA handled RequestA
HandlerB handled RequestB
HandlerC handled RequestC
策略模式
什么是策略模式?
策略模式(Strategy Pattern)属于对象的⾏为模式。其⽤意是针对⼀组算
法,将每⼀个算法封装到具有共同接⼝的独⽴的类中,从⽽使得它们可以
相互替换。策略模式使得算法可以在不影响到客户端的情况下发⽣变化。
其主要⽬的是通过定义相似的算法,替换 if else 语句写法,并且可以随时
相互替换。
策略模式有什么好处?
定义了⼀系列封装了算法、⾏为的对象,他们可以相互替换。
举例: Java.util.List 就是定义了⼀个增( add )、删( remove )、改
( set )、查( indexOf )策略,⾄于实现这个策略的
ArrayList 、 LinkedList 等类,只是在具体实现时采⽤了不同的算法。但因
为它们策略⼀样,不考虑速度的情况下,使⽤时完全可以互相替换使⽤。
策略模式的示例代码
假设我们有一个计算价格的系统,不同的客户可能有不同的折扣策略。我们可以使用策略模式来为不同的客户提供不同的折扣计算方式。
1. 定义策略接口
java">// 策略接口
public interface DiscountStrategy {double calculateDiscount(double price);
}
2. 创建具体策略类
java">// 具体策略1:满减优惠
public class FullReductionDiscount implements DiscountStrategy {@Overridepublic double calculateDiscount(double price) {if (price > 100) {return price - 20; // 满100减20}return price;}
}// 具体策略2:打折优惠
public class PercentageDiscount implements DiscountStrategy {@Overridepublic double calculateDiscount(double price) {return price * 0.9; // 9折}
}// 具体策略3:无优惠
public class NoDiscount implements DiscountStrategy {@Overridepublic double calculateDiscount(double price) {return price; // 无折扣}
}
3. 创建上下文类
java">// 上下文类
public class ShoppingCart {private DiscountStrategy discountStrategy;// 设置策略public void setDiscountStrategy(DiscountStrategy discountStrategy) {this.discountStrategy = discountStrategy;}// 计算价格public double calculatePrice(double price) {return discountStrategy.calculateDiscount(price);}
}
4. 客户端代码
java">public class StrategyPatternDemo {public static void main(String[] args) {// 创建购物车对象ShoppingCart cart = new ShoppingCart();// 设置不同的折扣策略并计算价格cart.setDiscountStrategy(new FullReductionDiscount());System.out.println("价格(满减优惠): " + cart.calculatePrice(120));cart.setDiscountStrategy(new PercentageDiscount());System.out.println("价格(打折优惠): " + cart.calculatePrice(120));cart.setDiscountStrategy(new NoDiscount());System.out.println("价格(无优惠): " + cart.calculatePrice(120));}
}
代码解释
-
DiscountStrategy 接口:它定义了一个方法
calculateDiscount()
,每个具体策略类(如满减、打折等)都会实现该接口,提供具体的折扣算法。 -
具体策略类(FullReductionDiscount、PercentageDiscount、NoDiscount):这些类实现了
DiscountStrategy
接口,提供不同的折扣策略。每个类都根据不同的逻辑计算价格。 -
ShoppingCart 类:它作为上下文类,持有一个
DiscountStrategy
类型的对象。通过setDiscountStrategy()
方法,客户端可以改变折扣策略。然后,calculatePrice()
方法将调用相应策略的折扣算法来计算价格。 -
客户端代码:在
main()
方法中,创建了一个购物车对象,并依次设置了不同的折扣策略。每次设置不同的策略后,调用calculatePrice()
方法来计算商品价格。
输出
java">价格(满减优惠): 100.0
价格(打折优惠): 108.0
价格(无优惠): 120.0
规则树模式(Rule Tree Pattern)简介
规则树模式(Rule Tree Pattern)是一种常用于决策引擎、复杂系统和业务逻辑处理中,管理和执行决策规则的设计模式。在这种模式中,规则通常按照层次结构组织,每个节点代表一个决策规则,树的深度和广度通常反映了规则的复杂性。规则树的好处是可以清晰地表达复杂的决策逻辑,且能够灵活地根据条件执行不同的规则。
规则树模式的特点
- 层次结构:规则树是树状结构,每个节点可以包含一个判断条件和相应的执行操作。
- 条件判断:树的每一层或每个节点表示一个条件,当满足某个条件时,就进入下一个决策层级。
- 灵活扩展:通过新增节点来扩展新的规则,而不需要修改已有的逻辑。
- 规则执行顺序:节点的顺序决定了规则执行的先后顺序,树的深度决定了规则的复杂性。
示例代码:规则树模式
我们来实现一个简单的规则树,用来判定用户的订单是否符合某些条件,例如是否满足折扣要求。
1. 定义规则接口
java">// 规则接口
public interface Rule {boolean evaluate(Order order);
}
2. 创建具体规则类
java">// 规则1:订单金额大于1000元
public class AmountRule implements Rule {@Overridepublic boolean evaluate(Order order) {return order.getAmount() > 1000;}
}// 规则2:订单属于VIP客户
public class VIPCustomerRule implements Rule {@Overridepublic boolean evaluate(Order order) {return order.getCustomer().isVIP();}
}
3. 定义节点(RuleNode)类
java">// 规则树节点
public class RuleNode {private Rule rule; // 规则private RuleNode leftChild; // 左子节点private RuleNode rightChild;// 右子节点// 构造方法public RuleNode(Rule rule) {this.rule = rule;}// 设置左子节点public void setLeftChild(RuleNode leftChild) {this.leftChild = leftChild;}// 设置右子节点public void setRightChild(RuleNode rightChild) {this.rightChild = rightChild;}// 执行规则树的判断public boolean evaluate(Order order) {if (rule.evaluate(order)) {if (leftChild != null) return leftChild.evaluate(order);return true;} else {if (rightChild != null) return rightChild.evaluate(order);return false;}}
}
4. 创建订单类
java">// 订单类
public class Order {private double amount;private Customer customer;// 构造方法public Order(double amount, Customer customer) {this.amount = amount;this.customer = customer;}public double getAmount() {return amount;}public Customer getCustomer() {return customer;}
}
5. 创建客户类
java">// 客户类
public class Customer {private String name;private boolean isVIP;// 构造方法public Customer(String name, boolean isVIP) {this.name = name;this.isVIP = isVIP;}public boolean isVIP() {return isVIP;}
}
6. 测试代码
java">public class RuleTreeTest {public static void main(String[] args) {// 创建客户和订单Customer customer = new Customer("John Doe", true);Order order = new Order(1500, customer);// 创建规则节点RuleNode root = new RuleNode(new AmountRule());RuleNode leftNode = new RuleNode(new VIPCustomerRule());// 构建规则树root.setLeftChild(leftNode);// 测试规则树boolean result = root.evaluate(order);System.out.println("Order meets the discount criteria: " + result);}
}
数据库配置
为了存储规则树的结构以及规则本身,数据库通常需要设计以下几个表格:
-
Rule表:存储具体的规则信息。
id
:规则ID(主键)name
:规则名称description
:规则描述type
:规则类型(例如:金额规则、VIP客户规则等)
-
RuleNode表:存储规则树节点之间的关系。
id
:节点ID(主键)parent_id
:父节点ID(外键,指向该节点的父节点)rule_id
:规则ID(外键,指向Rule表)is_left
:是否为左子节点(布尔值,true表示左子节点,false表示右子节点)
-
Order表:存储订单信息。
id
:订单ID(主键)amount
:订单金额customer_id
:客户ID(外键,指向Customer表)
-
Customer表:存储客户信息。
id
:客户ID(主键)name
:客户姓名is_vip
:是否VIP客户(布尔值)