image-20240420163459407

1. 设计模式

1.1. UML 图

1.1.1. 类与类之间的关系

  1. 关联关系,关联关系表现为一个类的对象与另一个类的对象之间存在某种关联,一个类的对象可以作为另一个类的成员变量,或者作为方法的参数或返回值等形式出现。例如,学生和课程之间的关系、订单和产品之间的关系等都可以用关联关系来描述。
  2. 聚合关系:像学校和老师,可以分离。整体类对象可以利用部分类对象,但不能管理部分类对象的生命周期,整体归西,部分不一定归西
  3. 组合关系:头和嘴,不能脱离。整体类对象可以利用部分类对象,也可以管理部分类对象的生命周期,整体归西,部分一定归西
  4. 依赖关系:主要描述一个类对另一个类的使用。当一个类需要使用到另一个类的功能时,就形成了依赖关系。这种关系在代码层面表现为一个类以局部变量、方法参数或静态方法调用的形式使用另一个类。例如,一个类可能需要使用另一个类提供的数据或方法来完成其任务,这时就形成了依赖关系。
  5. 继承关系
  6. 实现关系

1.1.2. 软件设计原则

  1. 开闭原则:扩展开放,修改封闭

  2. 里氏代换原则:任何基类出现的地方,子类可以出现,就是尽量子类增加方法,不要重写

  3. 依赖倒转原则:对抽象进行编程而不是对实现进行编程。面向接口编程

  4. 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上,避免一个类依赖过多的类,接口要精简

  5. 迪米特法则:一个对象应该对其他对象有最少的了解,两个类不直接通信,通过中介类,比如明星和经纪人

  6. 合成复用原则:尽量使用合成/聚合,而不是继承。比如汽车和颜色,汽车和引擎

1.1.3. 创建者模式

1.1.3.1. 单例模式:一个类只有一个实例,比如线程池,数据库连接池

  1. 饿汉式:类加载时就初始化,线程安全

    1. 私有构造器
    2. 类中创建静态成员变量,初始化实例
    3. 提供一个公有的静态方法,返回实例
  2. 懒汉式:类加载时不初始化,调用时初始化,线程不安全

    1. 线程不安全
    2. 线程安全
      1. 加锁, 双重检查
      2. 可能会出现指令重排,加volatile
    3. 静态内部类实现,返回静态内部类的实例
  3. 枚举类型:线程安全,防止反射和反序列化

  4. 序列化和反序列化:序列化和反序列化会破坏单例模式,可以通过readResolve()方法解决,当Singleton对象被反序列化时,readResolve()方法会被自动调用。由于readResolve()方法返回的是通过getInstance()方法获取的单例对象,而不是新创建的实例,因此确保了单例模式的正确性。

  5. 反射:反射可以破坏单例模式,可以通过私有构造器抛出异常解决,或者在构造器中加flag判断是否已经创建实例

  6. Runtime类:Runtime类是一个恶汉式单例类,它的构造方法是私有的,只能通过静态方法getRuntime()获取实例。Runtime类的实例代表了当前Java虚拟机的运行时环境,是一个进程级别的单例。

    public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
    }
public class Singleton {  
private volatile static Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

在这个例子中,instance = new Singleton(); 这行代码并不是原子性的。它实际上可以分为三个步骤:

  1. 分配内存空间给 instance。
  2. 初始化 Singleton 对象。
  3. 将 instance 引用指向分配的内存地址。

在单线程环境中,这三个步骤的顺序是固定的,没有问题。然而,在多线程环境中,由于指令重排,这三个步骤的执行顺序可能会发生变化。特别是,步骤2和步骤3的顺序可能会被交换。如果发生这种情况,当一个线程执行到步骤3但尚未执行步骤2时,另一个线程可能看到 instance 引用已经不为 null,但 instance 所指向的对象实际上还没有被完全初始化。这就会导致其他线程获取到一个尚未完全初始化的 Singleton 实例,引发程序错误。

为了避免这种指令重排导致的问题,需要在 instance 变量前加上 volatile 关键字。volatile 关键字可以确保多线程环境下变量的可见性和禁止指令重排优化。在Java内存模型中,volatile 变量写入时会立即同步到主存,读取时也会直接从主存中读取,保证了对变量的操作对所有线程是立即可见的。同时,volatile 还能防止编译器优化时对指令的重排,保证单例创建的顺序性。


1.1.3.2. 工厂模式:一个工厂类,可以生产多个产品,比如发送短信,发送邮件


  1. 简单工厂模式:一个工厂类,可以生产多个产品,比如各种类型的发送消息
    1. 优点:工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖
    2. 缺点:工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,违背了开闭原则

  1. 工厂方法模式:每个工厂类生产一个产品,比如发送短信工厂,发送邮件工厂
    1. 优点:定义一个抽象工厂类,每个产品对应一个工厂实现类,增加新的产品时,只需要增加一个新的工厂类,不需要修改原有的工厂类,符合开闭原则
    2. 缺点:每个产品对应一个工厂类,类的数量会增加,增加了系统的复杂度

  1. 抽象工厂模式:一个工厂类,可以生产多个产品族,比如服装厂,生产衣服,裤子,鞋子
    1. 品牌族:华为手机,华为电脑,华为平板
    2. 产品族:手机,电脑,平板
    3. 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
    4. 缺点:增加新的产品族时,需要修改抽象工厂类,违背了开闭原则
    5. 问:工厂方法模式和抽象工厂模式的区别?
      1. 工厂方法模式:一个工厂类生产一个产品,比如发送短信工厂,发送邮件工厂
      2. 抽象工厂模式:一个工厂类,可以生产多个产品族,比如发送短信,发送邮件,发送图片
    6. JDK中的应用:JDBC中的Connection,Statement,ResultSet,这三个接口的实现类都是由数据库厂商提供的,不同的数据库厂商提供不同的实现,这就是抽象工厂模式的应用
    7. 源码分析:JDK中的Calendar,Locale,NumberFormat,DateFormat,这些类都是抽象类,通过工厂方法来获取实例。Collection.iterator(),通过工厂方法获取迭代器

原型模式:通过复制一个已经存在的实例来创建新的实例,比如克隆羊

  1. 浅拷贝:只拷贝对象的引用,不拷贝对象的属性,对象的引用指向的是同一个对象
  2. 深拷贝:拷贝对象的引用和属性,对象的引用指向的是不同的对象
    1. 重写clone()方法,实现深拷贝
    2. 序列化和反序列化实现深拷贝
    3. 使用BeanUtils实现深拷贝
    4. 使用JSON实现深拷贝
  3. Cloneable接口是抽象原型类,实现Cloneable接口,重写clone()方法就是具体原型类

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

  1. 产品角色:包含多个组成部件的复杂对象
  2. 抽象建造者:规范产品的组建,一般是由子类实现
  3. 具体建造者:实现抽象建造者,构建和装配各个部件
  4. 指挥者:构建一个使用Builder接口的对象,指导构建过程
  5. 优点:封装性好,创建和使用分离,扩展性好,建造者独立,容易控制细节风险
  6. 缺点:产品必须有共同点,范围有限
  7. JDK中的应用:StringBuilder,StringBuffer, 通过append()方法构建字符串.还有Mybatis中的SqlSessionFactoryBuilder,通过build()方法构建SqlSessionFactory
  8. 工厂方法模式VS建造者模式
    1. 工厂方法模式:关注的是创建单个产品,建造者模式:关注的是创建复杂对象
    2. 工厂方法模式:创建对象是一步完成,建造者模式:创建对象是多步完成
    3. 工厂方法模式:创建单个对象,建造者模式:创建复杂对象

结构型模式

代理模式:为其他对象提供一种代理以控制对这个对象的访问

  1. 静态代理:代理类和被代理类在编译期间就确定下来
    1. 优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展
    2. 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类