设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four (GoF) 的分类了,他们将设计模式分类为 23 种经典的模式,根据用途我们又可以分为三大类,分别为创建型模式、结构型模式和行为型模式。

创建型模式(Creational Patterns):主要用于对象的创建,包括多个不同的模式,如工厂方法模式、抽象工厂模式、建造者模式、单例模式和原型模式等。这些模式都有助于降低系统耦合度,并提高代码的可重用性和可扩展性。

参考文章:

设计模式——设计模式介绍和单例设计模式

设计模式——工厂模式

设计模式——原型模式

【设计模式】结合StringBuilder源码,探析建造者模式的特性和应用场景

单例模式

介绍

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)

比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式

优点:

  • 节省资源:单例模式实例只有一个,可以避免重复创建对象,从而节省了资源,提高了系统性能。

  • 管理全局变量:单例模式可以用于管理全局状态和变量,方便在整个系统中共享数据

  • 简化系统架构:使用单例模式可以简化系统架构,减少类的数量和接口的复杂度。

缺点:

  1. 可能引发并发问题:单例模式在多线程中使用时,需要保证线程安全,否则可能会引发并发问题。
  2. 可能增加系统复杂性:过度使用单例模式可能会增加系统复杂性,导致代码难以维护。
  3. 难以调试:由于单例模式全局共享状态,可能会导致调试过程中的问题难以定位和测试。

注意事项和使用场景

  • 单例模式保证了系统内存中该类只存在一个对象,节省系统资源,对一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多但又经常用到的对象(即:重量级对象)、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)

八种单例模式的创建方式

  1. 饿汉式(静态常量):线程安全,没用到会浪费内存。
  2. 饿汉式(静态代码块):线程安全,没用到会浪费内存。
  3. 懒汉式(线程不安全):懒加载,线程不安全。即用到时候再实例化,多线程时可能创建多个实例。不要用这种方式。
  4. 懒汉式(线程安全,同步方法):线程安全,但效率低(每次获取实例都要加锁),不推荐。
  5. 懒汉式(线程不安全,同步代码块):线程不安全,不要用这种方式。
  6. 双重检查
  7. 静态内部类
  8. 枚举

饿汉式(静态常量)

线程安全,可用,但如果没用到会浪费内存。

步骤示例:

1
2
3
4
5
6
7
8
9
10
public class Singleton {
// 1、构造器私有化
private Singleton() {}
// 2、类的内部创建 私有静态常对象
private static final Singleton instance = new Singleton();
// 3、向外暴露一个静态的公共方法 getInstance
public static Singleton getInstance() {
return instance;
}
}

优缺点

  • 优点:写法简单,就是在类装载的时候就完成实例化(类变量在JVM类加载的准备、初始化阶段会赋值)。避免了线程同步问题
  • 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。若从始至终从未使用过这个实例,则会造成内存的浪费。

这种方式基于 classloder 机制避免了多线程的同步问题。不过,instance 在类装载时就实例化,在单例模式中大多数都是调用getlnstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 Lazy loading 的效果。

饿汉式(静态代码块)

线程安全,可用,但是可能造成内存浪费

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
// 1、构造器私有化
private Singleton() {}
// 2、类的内部声明对象
private static Singleton instance;
// 3、在静态代码块中创建对象
static {
instance = new Singleton();
}
// 4、向外暴露一个静态的公共方法
public static Singleton getInstance() {
return instance;
}
}

优缺点:这种方式和上面静态常量的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

懒汉式(线程不安全)

懒加载,线程不安全。即用到时候再实例化,多线程时可能创建多个实例。不要用这种方式

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
// 1、构造器私有化
private Singleton() {}
// 2、类的内部声明对象
private static Singleton instance;
// 3、向外暴露一个静态的公共方法,当使用到该方法时,才去创建 instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}

优缺点

  • 起到了 Lazy Loading 的效果,但是只能在单线程下使用
  • 如果在多线程下,一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,便会产生多个实例

懒汉式(线程安全,同步方法)

线程安全,但效率低(每次获取实例都要加锁),不推荐。

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
// 1、构造器私有化
private Singleton() {}
// 2、类的内部声明对象
private static Singleton instance;
// 3、向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
// 向外暴露一个公共静态synchronized方法,当使用到该方法时,才去创建 instance
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

优缺点

  • 解决了线程不安全问题
  • 效率太低了,每个线程在想获得类的实例时候,执行getlnstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低

懒汉式(线程不安全,同步代码块)

线程不安全,在实际开发中,不能使用这种方式

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
// 1、构造器私有化
private Singleton() {}
// 2、类的内部声明对象
private static Singleton instance;
// 3、向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
public static Singleton getInstance() {
if (instance == null) { // 可能有多个线程同时通过检查,多次执行下面代码,产生多个实例
// 类级别的锁对象,锁对象是全局的,对该类的所有实例均有效。回顾锁对象是this时仅限于锁当前实例
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}

优缺点

  • 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
  • 但这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例

双重检查(推荐,线程安全、懒加载)

在实际开发中,推荐使用这种单例设计模式

  1. 构造器私有化
  2. 类的内部创建对象引用,同时用volatile关键字修饰
  3. 向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
// 1、构造器私有化
private Singleton() {}

// 2、类的内部声明对象,同时用`volatile`关键字修饰,为了保证可见性。
// 原子性、可见性(修改立即更新到内存)、有序性
private static volatile Singleton instance;

// 3、向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,可能有多个线程同时通过检查
// 类级别的锁对象,锁对象是全局的,对该类的所有实例均有效。回顾锁对象是this时仅限于锁当前实例
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,只会有1个线程通过检查并创建实例
instance = new Singleton();
}
}
}
return instance;
}
}

优缺点

  • Double-Check 概念是多线程开发中常使用到的,我们进行了两次检查,这样就可以保证线程安全了
  • 这样实例化代码只用执行一次,后面再次访问时直接 return 实例化对象,也避免的反复进行方法同步
  • 线程安全;延迟加载;效率较高

静态内部类(推荐)

线程安全、延迟加载、效率高,推荐使用。

步骤:

  • 1)构造器私有化

  • 2)定义一个静态内部类,内部定义当前类的静态属性

  • 3)向外暴露一个静态的公共方法

知识加油站:

  • 类的加载机制是延迟加载的,也就是说,只有在需要使用到某个类时才会进行加载。

  • 类加载过程中会加载其所有静态成员到内存中,包括静态变量、静态成员方法和静态内部类。

  • 类加载包括加载、链接(验证、准备(为类变量分配内存并赋零值)、解析)、初始化(类变量赋初值、执行静态语句块)。

步骤示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
// 1、构造器私有化
private Singleton() {}
// 2、定义一个静态内部类,内部定义当前类的静态属性
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
// 3、向外暴露一个静态的公共方法
public static Singleton getInstance() {
return SingletonInstance.instance;
}
}

优缺点

  • 这种方式采用了类装载的机制,来保证初始化实例时只有一个线程
  • 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getlnstance方法,才会装载Singletonlnstance 类,从而完成 Singleton 的实例化。
  • 类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
  • 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

枚举(推荐)

推荐,线程安全,延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("Hello World");
}
}

public class SingletonTest {
public static void main(String[] args){
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2); // true
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
public enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("Hello World");
}
}
}

优缺点

  • 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
  • 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式

JDK 源码里单例模式分析

1
2
3
4
5
6
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime(){ return currentRuntime; }
// Don't let anyone else instantiate this class
private Runtime(){}
}

工厂模式

工厂模式介绍

以提高复杂性为代价,提高可维护性和复用性。

工厂模式是一种创建型设计模式(跟创建对象有关),它主要解决了对象的创建过程中的灵活性和可维护性问题。工厂模式允许在不暴露对象创建逻辑的情况下,统一由工厂类负责创建对象并返回,从而降低了代码的耦合性。

  1. 简单工厂模式

    简单工厂模式是通过工厂类(抽象或非抽象)的静态方法来创建对象实例,并将实例作为方法的返回值。在使用时,只需要调用该静态方法来创建对象,而无需创建工厂类的实例。静态工厂方法也是简单工厂模式的一种

  2. 工厂方法模式

    工厂方法模式是在抽象工厂类里定义创建抽象产品对象的抽象方法,由具体工厂类决定要实例化的产品类。抽象工厂类的构造器里调用“创建抽象产品对象”的抽象方法,具体工厂类重写抽象方法,按情景实例化具体产品类。使用时直接创建具体工厂对象,将执行抽象工程类构造器调用重写后的创建方法,创建具体产品对象。

    一个抽象工厂类能派生多个具体工厂类。每个具体工厂类只能建立一个具体产品类的实例。一个抽象产品类能派生多个具体产品类。

  3. 抽象工厂模式

    抽象工厂模式是抽象工厂接口里定义创建抽象产品对象的抽象方法,各具体工厂实现类根据情景创建具体产品对象。

    一个抽象工厂类能够派生出多个具体工厂类。每一个具体工厂类能够建立多个具体产品类的实例。多个抽象产品类,每个抽象产品类能够派生出多个具体产品类。

优点:

  1. 可以避免直接使用new关键字创建对象带来的耦合性,提高了代码的可维护性
  2. 可以将对象的创建逻辑封装到一个工厂类中,提高了代码的复用性
  3. 可以对对象的创建逻辑进行统一管理,方便代码的维护和升级。

缺点:

  1. 增加了代码的复杂度,需要创建工厂类,会增加代码规模
  2. 如果产品类发生变化,需要修改工厂类,可能会影响到其他代码的功能

综上所述,工厂模式是一种常用的创建型设计模式,可以提高代码的可维护性、复用性和灵活性。但是,在使用时需要权衡利弊,避免过度使用,增加代码的复杂度。

工厂模式的意义:

实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。

设计模式的依赖抽象原则:

  • 创建对象实例时,不要直接 new 类,而是把这个 new 类的动作放在一个工厂的方法中并返回。有的书上说,变量不要直接持有具体类的引用
  • 不要让类继承具体类,而是继承抽象类或者是实现 interface(接口)
  • 不要覆盖基类中已经实现的方法

披萨项目需求

披萨项目:要便于披萨种类的扩展,要便于维护

  • 披萨的种类很多(比如 GreekPizz、CheesePizz 等)
  • 披萨的制作有 prepare、bake、cut、box
  • 完成披萨店订购功能

传统方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public abstract class AbstractPizza {
protected String name;
public void setName(String name) {
this.name = name;
}
// 准备原材料,不同具体披萨不一样,所以是抽象方法。
public abstract void prepare();
public void bake() {
System.out.println(name + " baking...");
}
public void cut() {
System.out.println(name + " cutting...");
}
public void box() {
System.out.println(name + " boxing...");
}
}
// 希腊风味披萨
public class GreekPizza extends Pizza {
@Override
public void prepare() {
setName("GreekPizza");
System.out.println(name + " preparing...");
}
}
// 奶酪披萨
public class CheesePizza extends Pizza {
@Override
public void prepare() {
setName("CheesePizza");
System.out.println(name + " preparing...");
}
}

// 订购披萨,不断输入披萨类型,输出披萨的生产包装过程。
// 耦合度高,违反了设计模式的 OCP 原则,即对扩展开放,对修改关闭。
public class OrderPizza {
public OrderPizza() {
Pizza pizza = null;
String orderType;
do {
// 每次新增披萨类型,都要改这个订购类,而订购类可能会有很多,都要改就太麻烦了
orderType = getType();
if ("cheese".equals(orderType)) {
pizza = new CheesePizza();
} else if ("greek".equals(orderType)) {
pizza = new GreekPizza();
} else {
System.out.println("输入类型错误,程序退出");
break;
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
private String getType() {
System.out.println("请输入披萨类型:");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
return reader.readLine();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
// 披萨商店
// 相当于一个客户端,发出订购
public class PizzaStore {
public static void main(String[] args) {
new stubOrderPizza();
}

传统方式优缺点:

  • 优点是比较好理解,简单易操作

  • 缺点是违反了设计模式的 OCP 原则,即对扩展开放,对修改关闭。即当给类增加新功能时,尽量不修改代码,或尽可能少修改。

  • 比如我们这时要新增加一个Pizza的种类(Cheese技萨),需要做如下修改订购类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 胡椒披萨
    public class PepperPizza extends Pizza {
    @Override
    public void prepare() {
    setName("PepperPizza");
    System.out.println(name + " preparing...");
    }
    }
    public class OrderPizza {
    public OrderPizza() {
    // ...
    else if ("pepper".equals(orderType)) {
    pizza = new PepperPizza();
    }
    // ...
    }
    // ...
    }

改进的思路分析:

分析:修改代码可以接受,但是如果我们在其它的地方也有创建 Pizza 的代码,就意味着也需要修改。而创建Pizza的代码,往往有多处
思路:把创建 Pizza 对象封装到一个类中,这样我们有新的 Pizza 种类时,只需要修改该类就可,其它有创建到 Pizza 对象的代码就不需要修改了 ==> 简单工厂模式

非静态简单工厂模式

  • 简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式
  • 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
  • 在软件开发中,如果要用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式

简单工厂模式优化披萨项目

创建一个披萨工厂类,用于根据披萨类别创建披萨对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 创建一个披萨工厂类,用于根据披萨类别创建披萨对象。
public class PizzaFactory {
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
case "cheese": pizza = new CheesePizza();break;
case "greek": pizza = new GreekPizza(); break;
case "pepper": pizza = new PepperPizza(); break;
default: break;
}
return pizza;
}
}
// 修改订购披萨类,披萨对象从披萨工厂类获取
public class OrderPizza {
private PizzaFactory pizzaFactory;
public OrderPizza(PizzaFactory pizzaFactory) {
this.pizzaFactory = pizzaFactory;
orderPizza();
}
public void orderPizza() {
Pizza pizza = null;
do {
pizza = pizzaFactory.createPizza(getType());
if (pizza == null) {
System.out.println("Failed to Order Pizza");
} else {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
} while (true); // 无限循环,不断输入披萨类型,进行加工、包装等操作;
}
// ...
}

静态简单工厂模式

静态工厂模式也是简单工厂模式的一种,只是将工厂方法改为静态方法

静态工厂模式通过工厂类的静态方法来创建对象实例,并将实例作为方法的返回值。在使用时,只需要调用该静态方法来创建对象,而无需创建工厂类的实例。

静态工厂模式的优点在于可以简化代码实现,无需创建工厂对象的实例,提高代码的简洁性;对比普通工厂模式的优点在于更方便扩展和修改,如果需要新增一个产品线,只需要添加具体的产品类和对应的工厂类即可,无需修改已有的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 简单静态工厂
public class PizzaFactory {
public static Pizza createPizza2(String orderType) {
// ...
}
}
// 订购披萨类
public class OrderPizza {
public OrderPizza() {
Pizza pizza;
do {
// 直接通过静态方法创建工厂对象,不用再像之前通过“构造器参数赋值成员变量”方式创建对象。
pizza = PizzaFactory.createPizza(getType());
// ...
} while (true);
}
}

工厂方法模式

工厂方法模式:定义创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。

实现方式:抽象工厂类的构造器里调用“创建抽象产品对象”的抽象方法,具体工厂类重写抽象方法,按情景实例化具体产品类。

一个抽象产品能够派生出多个具体产品类。 一个抽象工厂类能够派生出多个具体工厂类。每一个具体工厂类只能建立一个具体产品类的实例。

工厂方法模式包含以下几个角色:

  1. 抽象产品类(Product):定义了产品的抽象接口,具体产品将按照抽象产品类所定义的接口来实现。

  2. 具体产品类(Concrete Product):是抽象产品类的一个具体实现,定义了具体产品的实现方法。

  3. 抽象工厂类(Factory):是工厂方法模式的核心,定义了工厂类需要实现的接口,用于创建产品对象。

  4. 具体工厂类(Concrete Factory):是抽象工厂类的一个具体实现,实现了工厂方法创建具体对象实例的逻辑。

披萨项目新的需求:客户可以点不同口味的披萨,如北京奶酪 Pizza、北京胡椒 Pizza 或者是伦敦奶酪 Pizza、伦敦胡椒 Pizza

思路1:使用简单工厂模式,创建不同的简单工厂类,比如 BJPizzaFactory、LDPizzaFactory 等等。从当前这个案例来说,也是可以的,但是考虑到项目的规模,以及软件的可维护性、可扩展性并不是特别好

思路2:工厂方法模式设计方案:将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 披萨商店
public class PizzaStore{
public static void main(String[] args) {
String loc = "bj";
if (loc.equals("bj")) { // 创建北京口味的各种Rizza
new BJOrderPizza();
} else {
new LDOrderPizza(); // 创建伦敦口味的各种Pizza
}
}
}
// 抽象工厂类,订购披萨类:
public abstract class AbstractOrderPizzaFactory{
// 构造方法不断根据用户输入披萨类型,调用创建披萨对象的抽象方法,实现加工披萨。
public void OrderPizza() {
Pizza pizza = null;
do {
pizza = createPizza(getType());
if (pizza == null) {
System.out.println("Failed to Order Pizza");
} else {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
} while (true);
}
// 抽象方法
public abstract Pizza createPizza(String orderType);
// ...
}
// 具体工厂类,订购伦敦披萨类:
public class LDOrderPizza extends AbstractOrderPizzaFactory{
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
case "cheese": pizza = new LDCheesePizza(); break;
case "pepper": pizza = new LDPepperPizza(); break;
default: break;
}
return pizza;
}
}
// 具体工厂类,订购北京披萨类:
public class BJOrderPizza extends AbstractOrderPizzaFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
case "cheese": pizza = new BJCheesePizza(); break;
case "pepper": pizza = new BJPepperPizza(); break;
default: break;
}
return pizza;
}
}

抽象工厂模式

抽象工厂接口里有创建抽象产品的方法,各具体工厂实现类根据情景创建具体产品对象。

  • 抽象工厂模式:定义了一个接口用于创建相关或有依赖关系的对象簇,而无需指明具体的类
  • 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合
  • 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)
  • 将工厂抽象成两层,抽象工厂接口和具体工厂类。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展

抽象工厂接口里有创建抽象产品的方法,各具体工厂实现类根据情景创建具体产品对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public interface AbsPizzaFactory {
Pizza createPizza(String orderType);
}
public class BJPizzaFactory implements AbsPizzaFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
// ...
}
return pizza;
}
}
public class LDPizzaFactory implements AbsPizzaFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
// ...
}
return pizza;
}
}
// 订购披萨
public class OrderPizza {
// 构造器
public OrderPizza(AbsFactory factory) {
setFactory(factory);
}
public void orderPizza(AbsFactory factory) {
Pizza pizza = null;
String orderType = ""; // 用户输入
this.factory = factory;
do {
// factory 可能是北京的工厂子类,也可能是伦敦的工厂子类
pizza = factory.createPizza(orderType);
if (pizza != null) { // 订购ok
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} else {
System.out.println("Failed to Order Pizza");
break;
}
} while (true); // 无限循环,不断输入披萨类型,进行加工、包装等操作;
}
}
1
2
// 创建订购单,由具体工厂类创建具体产品对象
new OrderPizza(new BJFactory());

JDK 源码分析

JDK 中的 Calendar 类中,就使用了静态简单工厂模式。

静态简单工厂模式:由一个工厂对象(可以是抽象类,可以是非抽象类)决定创建出哪种产品类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 测试代码
public class Test {
public static void main(String[] args) {
// Calendar类是工厂类,getInstance()是静态方法,用于创建对象
Calendar cal = Calendar.getInstance();
// 注意月份下标从O开始,所以取月份要+1
System.out.println("年:" + cal.get(Calendar.YEAR));
System.out.println("月:" + (cal.get(Calendar.MONTH) + 1));
System.out.println("日:" + cal.get(Calendar.DAY_OF_MONTH));
System.out.println("时:" + cal.get(Calendar. HOUR_OF_DAY)) ;
System.out.println("分:" + cal.get(Calendar. MINUTE)) ;
System.out.println("秒:" + cal. get(Calendar.SECOND));
}
}
// Calendar工厂类,决定创建出哪一种产品类的实例
// Calendar类是工厂类,getInstance()是静态方法,用于创建对象
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
// 根据时区地区返回Calendar 实例
public static Calendar getInstance() {
Locale aLocale = Locale.getDefault(Locale.Category.FORMAT);
return createCalendar(defaultTimeZone(aLocale), aLocale);
}
// 根据时区地区创建Calendar 实例
private static Calendar createCalendar(TimeZone zone, Locale aLocale){
CalendarProvider provider = LocaleProviderAdapter
.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist": cal = new BuddhistCalendar(zone, aLocale); break;
case "japanese": cal = new JapaneseImperialCalendar(zone, aLocale); break;
case "gregory": cal = new GregorianCalendar(zone, aLocale); break;
}
}
}
if (cal == null) {
// 根据不同地区,返回不同的具体的产品类
// If no known calendar type is explicitly specified,
// perform the traditional way to create a Calendar:
// create a BuddhistCalendar for th_TH locale,
// a JapaneseImperialCalendar for ja_JP_JP locale, or
// a GregorianCalendar for any other locales.
// NOTE: The language, country and variant strings are interned.
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
}

原型模式

经典的克隆羊问题(复制10只属性相同的羊)

问题描述:现在有一只羊,姓名为 Tom,年龄为 1,颜色为白色,请编写程序创建和 Tom 羊属性完全相同的 10 只羊。

传统方案:循环new对象

实现方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 羊
public class Sheep {
private String name;
private Integer age;

public Sheep(String name, Integer age) {
this.name = name;
this.age = age;
}
// getter and setter
}
// 克隆羊
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Sheep sheep = new Sheep("Tom", 1, "白色");
System.out.println(sheep);
}
}
}

优缺点和改进思路

优点:好理解,简单易操作

缺点:

  • 每次获取再复制效率低:在创建新的对象时,总是需要重新获取原始对象的属性,如果创建的对象比较复杂时,效率较低
  • 不灵活:总是需要重新初始化对象,而不是动态地获得对象运行时的状态,不够灵活

改进的思路分析:Object 类的 clone() 方法

Object 类是所有类的根类,Object 类提供了一个 clone 方法,该方法可以将一个 Java 对象复制一份,但是对应的类必须实现Cloneable接口,该接口表示该类能够复制且具有复制的能力 ==> 原型模式

原型模式(Prototype模式)

基本介绍

原型模式(Prototype 模式):原型实例指定创建对象种类,并通过拷贝原型创建新的对象

原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节。

原理:将一个原型对象传给要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即对象.clone()

创建型设计模式:关注如何有效地创建对象,以满足不同的需求和情境。

包括:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式

原理及代码演示

  • Prototype:原型类。包含一个用于复制对象的克隆方法。可以使用Cloneable接口作为原型接口。
  • ConcretePrototype:具体原型类。实现原型接口、重写克隆方法clone()的具体类。
  • Client:让一个原型对象克隆自己,创建一个属性相同的对象

原型接口: 可以是Cloneable接口也可以是自定义带clone()方法的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 步骤1:定义原型接口
interface Prototype extends Cloneable {
Prototype clone();
}
// 步骤2:实现具体原型类
class ConcretePrototype implements Prototype {
@Override
public Prototype clone() {
try {
return (Prototype) super.clone(); // 使用浅拷贝
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
// 步骤3:客户端代码,通过clone()方法创建原型对象
public class Client {
public static void main(String[] args) {
ConcretePrototype prototype = new ConcretePrototype(); // 创建具体类对象
ConcretePrototype clonedObject = (ConcretePrototype) prototype.clone(); // 通过clone方法创建对象
}
}

原型模式解决克隆羊问题

问题回顾:

现在有一只羊,姓名为 Tom,年龄为 1,颜色为白色,请编写程序创建和 Tom 羊属性完全相同的 10 只羊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 原型接口:Cloneable接口。
// 具体原型类:实现Cloneable接口
@Data
public class Sheep implements Cloneable {
private String name;
private Integer age;
private String color;
public Sheep(String name, Integer age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
@Override
protected Object clone() {
Sheep sheep = null;
try {
sheep = (Sheep) super.clone();
} catch (Exception e) {
e.printStackTrace();
}
return sheep;
}
}
// 客户端: 调用具体原型类的clone()方法创建10个对象
public class Client {
public static void main(String[] args) {
Sheep sheep = new Sheep("Tom", 1, "白色");
for (int i = 0; i < 10; i++) {
Sheep sheep1 = (Sheep) sheep.clone();
System.out.println(sheep1);
}
}
}

优缺点和使用场景

优点

  • 构造方法复杂时开销小:如果构造函数的逻辑很复杂,此时通过new创建该对象会比较耗时,那么就可以尝试使用克隆来生成对象。

  • 运行时动态创建对象:不用重新初始化对象,而是动态地获得对象运行时的状态

  • 开闭原则(OCP原则):如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需更改客户端代

    码。相反,如果使用new方式,就需要在客户端修改构造参数。这使得系统更加灵活和可维护。

  • 对象封装性:原型模式可以帮助保护对象的封装性,因为客户端代码无需了解对象的内部结构,只需知道如何克隆对象。

  • 多态性:原型模式支持多态性,因为克隆操作可以返回具体子类的对象,而客户端代码不需要关心对象的具体类。

缺点

  • 构造方法简单时开销大:如果构造函数的逻辑很简单,原型模式的效率不如new,因为JVM对new做了相应的性能优化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) {
    // 验证构造方法简单时,原型模式开销大
    long startTime = System.currentTimeMillis();
    Sheep sheep = new Sheep("Tom", 1, "白色");
    // 克隆循环十万次
    for (int i = 0; i < 100000; i++) {
    sheep.clone();
    }
    long midTime = System.currentTimeMillis();
    // 20ms
    System.out.println("克隆生成对象耗费的时间:" + (midTime - startTime) + "ms");
    // new10万次
    for (int i = 0; i < 100000; i++) {
    new Sheep();
    }
    // 5ms
    System.out.println("new生成对象耗费的时间:" + (System.currentTimeMillis() - midTime) + "ms");
    }
  • 要注意深拷贝和浅拷贝问题:实现Cloneable接口时,如果具体原型类直接返回super.clone(),则是浅拷贝。克隆的对象里,引用类型变量只拷贝引用,依然指向旧的地址。

  • 代码复杂性:在实现深拷贝的时候可能需要比较复杂的代码。设计模式一般都是以代码复杂性为代价,提高可扩展性、可读性。

适用场景

  1. 构造方法复杂:要创建的对象构造方法逻辑很复杂,即创建新的对象比较复杂时,使用原型模式会比直接new效率更高;
  2. 经常需要克隆:经常要创建一个和原对象属性相同的对象时,可以考虑原型模式。

Spring源码中的原型模式

Spring 框架Bean的生命周期中,ApplicationContext类的getBean()方法中,有用到原型模式。

获取Bean时会判断配置的Bean是单例还是原型,如果是原型,则用原型模式创建Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create bean instance.
if (mbd.issingleton()){...}
else if (mbd.isPrototype()){
// It's a prototupe -> create a new instance.
0bject prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
beanInstance = getobjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

验证:bean指定原型模式后,getBean()获取到的多个Bean是不同对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component("id01")
@Scope("prototype") // 注解方式是@Scope("prototype")。
public class Monster {
private String name;
private int health;
// 默认构造函数
public Monster() {}
public Monster(String name, int health) {
this.name = name;
this.health = health;
}
// 添加其他属性和方法
}
// 测试
public class ProtoType {
public static void main(String[] args) {
// TODO Auto-generated method stub
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 通过ID获取Monster
Object bean = applicationContext.getBean("id01");
System.out.println("bean: " + bean); // 输出“牛魔王"....
Object bean2 = applicationContext.getBean("id01");
System.out.println("bean2: " + bean2); // 输出“牛魔王"....
System.out.println(bean == bean2); // false
}
}

也可以用xml形式注册Bean:

1
2
<!-- 这里使用scope="prototype"即 原型模式来创建 -->
<bean id="id01" class="com.atquigu.spring.bean.Monster" scope="prototype"/>

浅拷贝和深拷贝

浅拷贝:引用类型变量拷贝引用

  • 浅拷贝:拷贝后对象是新地址,基本类型变量拷贝值,引用类型变量拷贝引用。只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存。

实现方案:具体原型类直接返回super.clone()

实现Cloneable 接口,重写 clone()方法, 直接返回super.clone()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person implements Cloneable {   // 虽然clone()是Object类的方法,但Java规定必须得实现一下这个接口
public int age; // 基本类型变量拷贝值
public Person(int age) {
this.age = age;
}
@Override
public Person clone() {
try {
return (Person) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
public static void main(String[] args) {
Person p1 = new Person(18);
Person p2 = p1.clone(); // p2将是p1浅拷贝的对象
p2.age = 20;
System.out.println(p1 == p2); // false。拷贝后对象是新地址
System.out.println(p1.age); // 18。基本类型变量拷贝值
}
}

深拷贝:引用类型变量拷贝值

深拷贝:拷贝后对象是新地址,基本类型变量拷贝值,引用类型变量拷贝克隆后的值。创造一个一摸一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对对象。反序列化创建对象是深拷贝。

实现方案:具体原型类专门克隆引用类型变量

实现Cloneable 接口,重写 clone()方法, 给super.clone()的引用类型成员变量也clone()一下,然后再返回克隆的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person implements Cloneable {
public int age; // 基本类型变量拷贝值
public int[] arr = new int[] {1, 2, 3};
public Person(int age) {
this.age = age;
}
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.arr = this.arr.clone(); // 用引用类型的 clone 方法,引用类型变量拷贝克隆后的值
return person;
} catch (CloneNotSupportedException e) {
return null;
}
}
}

建造者模式

经典的盖房子问题

问题描述:

  • 建房子过程:打桩、砌墙、封顶。虽然建造过程一样,但实际造的房子有差别,因为房子有各种各样的,比如普通房,高楼,别墅。
  • 需求:写代码能建造各类房子

传统方案盖房子

实现方案:产品和创建产品过程耦合

创建以下几个类:

  • AbsHouse(抽象盖房类):包含打桩、砌墙、封顶三个方法;
  • NormalRoom(普通房间类)、Villa(别墅类):继承抽象盖房类,根据情况重写三个方法;

抽象盖房类:包含打桩、砌墙、封顶三个方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 抽象房间类:包含打桩、砌墙、封顶
public abstract class AbsHouse {
protected abstract void piling(); // 打桩
protected abstract void walling(); // 砌墙
protected abstract void capping(); // 封顶
public void build() { // 盖房子
piling();
walling();
capping();
}
}
// NormalRoom(普通房间类)、Villa(别墅类):继承抽象盖房类,根据情况重写三个方法;
// 普通房
public class NormalRoom extends AbsHouse {
@Override
protected void piling() {
System.out.println("普通房打桩...");
}
@Override
protected void walling() {
System.out.println("普通房砌墙...");
}
@Override
protected void capping() {
System.out.println("普通房封顶...");
}
}
// 高楼
public class HighRise extends AbsHouse {
@Override
protected void piling() { ... }
@Override
protected void walling() { ... }
@Override
protected void capping() { ... }
}
// 别墅
public class Villa extends AbsHouse {
@Override
protected void piling() { ... }
@Override
protected void walling() { ... }
@Override
protected void capping() { ... }
}

优缺点和改进思路

优点:简单,好理解易操作

缺点产品和创建产品过程耦合:没有设计缓存层对象,程序的扩展和维护不好。也就是说,这种设计方案把产品(即:房子)和创建产品的过程(即:建房子流程)封装在一起,耦合性太高。

改进思路分析:使用建造者模式,将产品和产品建造过程解耦

建造者模式/生成器模式

基本介绍

建造者模式(Builder Pattern):使用多个步骤来创建一个复杂对象,而不是在一个构造函数或工厂方法中直接返回该对象。它将产品和产品建造过程进行了解耦。

建造者模式又叫生成器模式,是一种创建型设计模式。

特点:

  • 分步骤创建对象:对象的构建是分多个步骤进行的,而不是直接使用构造方法new对象,适用于需要分阶段构造的复杂对象。
  • 同一个建造过程:同样的构建过程可以创建不同的对象表示(不同的配置组合)。

四个角色

  • Product(产品):一个包含多个部件的类。

    每个部件是一个成员变量。例如房间包括地基、墙和屋顶等组件,又例如电脑包括CPU、内存条、主板等组件。

  • Builder(抽象建造者):一个包含产品变量及其所有部件建造方法的抽象类或接口

    包含每个部件的建造方法,和一个返回产品的build()方法。例如房间的打桩、砌墙、封顶过程。

  • ConcreteBuilder(具体建造者):实现抽象建造者所有抽象建造方法

  • Director(指挥者):一个以抽象建造者为变量的、包含建造方法的类

    • 包含一个抽象建造者变量,和一个建造并返回产品的build()方法。
    • 实际建造产品时,先创建一个指挥者对象,然后设置它的建造者变量,最后调用它的build()方法返回产品。

优缺点和使用场景

优点:

  • 耦合性降低
    • 产品与建造解耦:客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
    • 具体建造者之间解耦:每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象
  • 代码可读性高:可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
  • 开闭原则:增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”,即代码对修改不开放,而对扩展开放。

缺点:

  • 代码复杂性:没了解过建造者模式的人阅读代码更困难,这也是设计模式的通用缺点。
  • 过度设计风险:建造者模式只适合复杂产品对象,太简单的产品对象则没必要使用,或者建造方式只有一种的产品,都没必要使用。这也是设计模式的通用缺点。

使用场景

  • 复杂产品对象:部件比较多的产品,例如房屋有墙、屋顶、地基、横梁等等部件。
  • 建造方式多样:例如房屋对于同一些材料,可以建成普通房屋、高楼等等。
  • 建造产品共同点:建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制

不适用场景

  • 简单产品对象;
  • 建造方式单一;

建造者模式和抽象工厂模式的区别

区别:

  • 抽象工厂模式:适用于不同产品的情况。用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类)。
  • 建造者模式:适用于一个产品有多个部件的情况。用来创建一种类型的复杂对象,通过设置不同可选参数,“定制化”创建不同对象。

示例:顾客走进一家餐馆点餐

  • 工厂模式:根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。
  • 建造者模式:对于披萨来说,用户又有各种配料可以定制,我们通过建造者模式根据用户选择的不同配料来制作披萨。

建造者模式盖房子

产品类

产品类包含多个部件的对象。
每个部件是一个成员变量。例如房间包括地基、墙和屋顶等组件,又例如电脑包括CPU、内存条、主板等组件。

1
2
3
4
5
6
7
8
9
// 产品:房间类,包含多个部件
@Data
public class House {
private String pile; // 地基
private String wall; // 墙
private String roof; // 屋顶

// getter和setter
}

抽象建造者

抽象建造者是个抽象类,包含每个部件的建造方法,和一个返回产品的build()方法
例如房间的打桩、砌墙、封顶过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 抽象建造者:房间建造抽象类,包含每个部件的抽象建造方法,和一个build()方法返回产品
public abstract class HouseBuilder {
private House house = new House(); // 产品对象

public abstract void piling(); // 打地基
public abstract void walling(); // 砌墙
public abstract void capping(); // 封顶

// build()方法返回产品
public House build() {
return house;
}
}

具体建造者

具体建造者是抽象建造者的实现类,实现各个部件具体构建逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 具体建造者:普通房屋建造者
public class NormalRoomBuilder extends HouseBuilder {
@Override
public void piling() { System.out.println("普通房打桩..."); }
@Override
public void walling() { System.out.println("普通房砌墙..."); }
@Override
public void capping() { System.out.println("普通房封顶..."); }
}
public class HighRiseBuilder extends HouseBuilder {
@Override
public void piling() { ... }
@Override
public void walling() { ... }
@Override
public void capping() { ... }
}

指挥者

指挥类包含一个抽象建造者变量,和一个建造并返回产品的build()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 指挥者:负责建造并返回产品
public class HouseDirector {
// 抽象建造者变量
private HouseBuilder houseBuilder;
public HouseDirector() {}
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
// 建房子方法
public House buildHouse() {
// 调用建造者变量的各部门建造方法,然后建造返回
houseBuilder.piling();
houseBuilder.walling();
houseBuilder.capping();
return houseBuilder.build();
}
}

测试类

步骤:

  1. 创建指挥者:创建一个指挥者对象
  2. 设置建造者:设置它的建造者变量为具体建造者对象
  3. 返回产品:调用它的build()方法返回产品。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BuilderTest {
public static void main(String[] args) {
// 1.创建指挥者对象
HouseDirector houseDirector = new HouseDirector();
House house;
// 2.用指挥者对象的setter方法设置建造者变量
houseDirector.setHouseBuilder(new NormalRoomBuilder());
// 3.指挥者对象的build()方法获取产品
house = houseDirector.buildHouse();
// 4.建造高楼
houseDirector.setHouseBuilder(new HighRiseBuilder());
house = houseDirector.buildHouse();
}
}

StringBuilder 中的建造者模式

JDK源码中的建造者模式

StringBuilder不是严格的建造者模式,但是使用了建造者模式思想。

1
2
3
4
5
6
// JDK中,不止StringBuilder、StringBuffer使用了建造者模式,还有:stream流
// 1.过滤只保留长度为3的字符串,收集成List<String>类型
List<String> ansList = list.stream().filter(item -> item.length() == 3).collect(Collectors.toList());
// 时间API
LocalDate date = LocalDate.of(2023, Month.JULY, 4);
LocalDateTime dateTime = LocalDateTime.of(2023, 7, 4, 10, 30);

角色分析

产品:char数组

char数组,数组可以包含多个元素,每个元素是一个部件。

1
2
3
4
abstract class AbstractStringBuilder implements Appendable, CharSequence {
// The value is used for character storage.
char[] value;
}

抽象建造者:AbstractStringBuilder

AbstractStringBuilder包含append()、delete()等方法,用来对产品(字符数组)里的部件(数组内每个元素)进行追加和删除。

部件组装方法:append() 、delete()

以append()为例:

核心流程

  1. 判空:如果传入的字符串为null,则不追加
  2. 校验扩容:若新容量大于当前数组长度,则进行扩容
  3. 拷贝数组:校验数组越界后,调用System.arraycopy()拷贝数组

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 抽象建造者:简化版AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value; // 产品:一个字符数组
int count; // 实际字符串长度

/**
* 产品部件建造方法:追加字符串
* 这个方法不一定必须是抽象方法,子类可以重写
* @param str
* @return {@link AbstractStringBuilder }
*/
public AbstractStringBuilder append(String str) {
// 1.判空:如果传入的字符串为null,则不追加
if (str == null)
return appendNull();
int len = str.length();
// 2.校验扩容:若新容量大于当前数组长度,则进行扩容
ensureCapacityInternal(count + len);
// 3.拷贝数组
str.getChars(0, len, value, count);
// 调整字符串长度:给count变量加上追加字符串长度
count += len;
return this;
}
// 扩容底层数组,@param minimumCapacity 新容量
private void ensureCapacityInternal(int minimumCapacity) {
// 检查是否需要扩容,若新容量大于当前数组长度,则进行扩容
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
// 检查源开始索引是否合法
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
// 检查源结束索引是否合法
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
// 检查开始索引是否大于结束索引
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
// 使用System.arraycopy复制字符
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
// ...
}

具体建造者:重写append()返回类型

StringBuilder

具体建造者StringBuilder继承了AbstractStringBuilder,并重写了append()方法。

append()逻辑都是用父类的append()方法,主要重写的地方在于返回类型由父类AbstractStringBuilder改成了子类StringBuilder

重写规则:重写时

  • 返回类可以是原返回类的子类。例如工厂方法设计模式里,抽象工厂类的createObject()方法返回值是抽象产品类,具体工厂类的createObject()方法返回类是具体产品类
  • 访问权限不能比其父类更为严格
  • 抛出异常不能比父类更广泛
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 具体建造者:简化版StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence {
/**
* 追加字符串
* @param str
* @return {@link java.lang.StringBuilder }
*/
@Override
public java.lang.StringBuilder append(String str) { // // 由AbstactStringBuilder重写成StringBuilder
super.append(str);
return this;
}
// ...
}

StringBuffer

相比StringBuilder的append()方法,主要修改了返回值,方法内第一步清空toStringCache变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 具体建造者:简化版StringBuffer
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
/**
* 缓存上一次调用toString()方法返回的值。
* 每当StringBuffer被修改时,这个缓存会被清除。
* transient关键字表明此字段不会被序列化。
*/
private transient char[] toStringCache;

/**
* 追加字符串
* 1.重写加了synchronized关键字,保证线程安全
* 2.返回类由AbstractStringBuilder改为StringBuffer
* 3.方法内第一步清空toStringCache
* @param str
* @return {@link java.lang.StringBuffer }
*/
@Override
public synchronized java.lang.StringBuffer append(String str) {
// 缓存上一次调用toString()方法返回的值,所以每次更新底层字符数组时,都需要清空;
toStringCache = null;
super.append(str);
return this;
}
/**
* 获取实际字符串长度
* StringBuffer线程安全的原因:有线程同步风险的方法都加了synchronized锁,锁的粒度是当前实例
* @return int
*/
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
}

扩展:String、StringBuffer、Stringbuilder有什么区别

得分点:是否可变、复用率、效率、线程安全问题

标准回答

**String:**不可变字符序列,效率低,但是复用率高、线程安全。

  • 不可变:指String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。
  • 复用率高:指String类型对象创建出来后归常量池管,可以随时从常量池调用同一个String对象。StringBuffer和StringBuider在创建对象后一般要转化成String对象才调用。

StringBuffer和StringBuilder都是字符序列可变的字符串,方法也一样,有共同的父类AbstractStringBuilder。

  • StringBuilder:可变字符序列、效率最高、线程不安全
  • StringBuffer:可变字符序列、效率较高(增删)、线程安全

扩展:为什么StringBuffer是线程安全的

点进源码后发现,有线程同步风险的方法(例如length()、append()、delete()等)都加了synchronized锁,锁的粒度是当前实例。

synchronized关键字作用于三个位置:

  1. 作用在静态方法上,则锁是当前类的Class对象。
  2. 作用在普通方法上,则锁是当前的实例(this)。
  3. 作用在代码块上,则需要在关键字后面的小括号里,显式指定锁对象,例如this、Xxx.class。

为什么StringBuffer效率低?

  1. 锁本身效率低:存在线程竞争时,加锁的代码块本身就比不加锁要慢很多,因为锁内的共享资源同一时刻只能一个线程访问。
  2. 锁粒度大:锁的粒度是当前实例,直接给整个方法加synchronized,而不是具体需要在有同步风险的代码块上加锁,性能低。这个方法内部分共享资源可能是线程安全的,并不需要加锁。

指挥者:StringBuilder

StringBuilder和StringBuffer自身既是具体建造者,也是指挥者。