Java设计模式之创建型模式

欢迎关注我的个人博客:Penghc_Blogicon-default.png?t=N7T8https://penghc.cn/

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,有以下特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

饿汉式

这种方式比较常用,但容易产生垃圾对象

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

1
2
3
4
5
6
7
8
9
10
11
12
copy
public class Singleton {
    // 私有静态成员变量,直接初始化实例
    private static Singleton instance = new Singleton();

    // 私有构造方法,防止外部实例化
    private Singleton() {}

    // 公有静态方法,返回对象实例
    public static Singleton getInstance() {
        return instance;
    }
}

由于饿汉式单例模式在类加载时就创建了对象,因此无需考虑多线程同步问题,是线程安全的。但是如果这个单例对象很大或者需要在程序运行过程中才能初始化完成,那么饿汉式单例模式可能会耗费不必要的资源。

懒汉式

懒汉式单例模式是指在需要的时候才创建对象,它的实现方式比较简单,但需要注意线程安全问题。

1
2
3
4
5
6
7
8
9
10
copy
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在上面的代码中,私有静态成员变量 instance 在第一次调用 getInstance() 方法时才被初始化。如果多个线程同时调用 getInstance() 方法,可能会导致创建多个实例的问题,因此需要考虑线程安全问题。可以通过加锁来解决线程安全问题,但这样会影响程序的性能。

懒汉式+双检锁

在懒汉式基础上加入双重检验锁,保证线程安全和性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
copy
public class Singleton {
    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;
    }
}

在上面的代码中,私有静态成员变量 instance 使用了 volatile 关键字修饰,确保多线程环境下的可见性和有序性。在 getInstance() 方法中,首先检查 instance 是否为 null,如果为 null,进入同步块,再次检查 instance 是否为 null,这个双重检查的过程避免了多个线程同时创建实例的问题。如果 instance 为 null,则创建新的实例,最后返回对象实例。

这种方式既实现了懒加载,也保证了线程安全。只有在第一次调用 getInstance() 方法时才会创建对象,避免了不必要的资源消耗。同时,通过双重检查锁定机制,在保证线程安全的前提下减少了锁的使用,提高了程序的性能。

需要注意的是,在使用双检锁机制时,必须将 instance 成员变量声明为 volatile,确保多线程环境下的可见性和有序性。

为什么使用双检锁机制时,必须将instance成员变量声明为volatile

懒汉式+静态内部类

使用静态内部类来实现懒汉式单例模式,可以保证线程安全和性能,因为静态内部类只有在被使用时才会被加载,从而实现了懒加载的效果。这种方式能达到双检锁方式一样的功效,但实现更简单。

1
2
3
4
5
6
7
8
9
10
11
copy
public class Singleton {
    private Singleton() {}
	// 内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

在上面的代码中,私有构造方法 Singleton() 防止外部实例化对象。私有静态内部类 SingletonHolder 包含私有静态成员变量 INSTANCE,在第一次调用 getInstance() 方法时才被初始化。由于静态内部类只有在被使用时才会被加载,因此实现了懒加载的效果。同时,由于静态内部类的加载是线程安全的,因此这种方式无需考虑多线程同步问题,也不会影响程序性能。

为什么静态内部类的加载是线程安全的

枚举实现单例模式

当使用枚举来实现单例模式时,每个枚举常量就代表了一个单例对象,因为在 Java 中,枚举常量是唯一的。并且使用使用枚举来实现单例模式可以防止保证线程安全和防止反射攻击。

1
2
3
4
5
6
7
8
copy
public enum Singleton {
    INSTANCE;
    // 添加其他需要的实例变量和方法

    public void doSomething() {
        // 单例对象的操作
    }
}

在上述代码中,我们定义了一个名为 Singleton 的枚举类型,并在其中声明了一个名为 INSTANCE 的枚举常量。这个枚举常量就代表了单例对象。

使用枚举的方式,保证了在任何情况下只有一个实例被创建,并且能够提供全局访问点来获取该实例。

你可以通过以下方式来使用这个单例对象:

1
2
copy
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();

在这个示例中,我们通过 Singleton.INSTANCE 来获取单例对象,并调用其方法。

使用枚举实现单例模式的好处包括:

  • 线程安全:枚举实例的创建是由 JVM 在加载枚举类时完成的,保证了线程安全。
  • 防止反序列化创建新实例:枚举类默认实现了 Serializable 接口,并且在序列化和反序列化过程中会保持单例的状态。
  • 简洁明了:利用语言层面的特性,代码简洁,逻辑清晰。

总之,使用枚举实现单例模式是一种简单、安全且易于理解的方式。不过需要注意的是,这种方式只适用于支持枚举类型的编程语言

为什么使用枚举来实现单例模式可以保证线程安全并且可以防止反序列化创建新实例

举个例子:

假设我们有一个名为 Logger 的日志记录器类,我们希望在整个应用程序中只有一个日志记录器实例。

使用枚举来实现单例模式可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
copy
public enum Logger {
    INSTANCE;

    private Logger() {
        // 初始化日志记录器
    }

    public void log(String message) {
        // 日志记录操作
        System.out.println(message);
    }
}

在上述代码中,我们定义了一个名为 Logger 的枚举类型,并在其中声明了一个名为 INSTANCE 的枚举常量。这个枚举常量就代表了日志记录器的单例对象。

Logger 类的构造方法是私有的,这意味着不能通过 new 关键字直接创建 Logger 对象。然而,由于枚举实例是在 JVM 加载枚举类时完成创建的,所以 Logger.INSTANCE 实际上是一个已经初始化好的日志记录器单例对象。

你可以通过以下方式来使用这个单例对象:

1
2
copy
Logger logger = Logger.INSTANCE;
logger.log("Hello, world!");

在这个示例中,我们通过 Logger.INSTANCE 来获取日志记录器的单例对象,并调用其 log 方法记录日志信息。

无论在应用程序的任何位置,只要可以访问到 Logger.INSTANCE,都可以获取到同一个日志记录器实例。这就保证了在任何情况下只有一个日志记录器实例被创建,并且能够提供全局访问点来获取该实例。

原型模式

原型设计模式允许通过复制现有对象来创建新对象,而不是通过实例化类来创建新对象。在需要创建大量相似对象时非常有用,它可以避免重复创建对象,从而提高性能,并且可以根据需要实现浅拷贝或深拷贝。在Java中,原型模式的实现通常涉及到实现Cloneable接口和重写clone()方法Cloneable接口是一个标记接口,用于指示该对象可以被复制。

举个例子

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
copy
// 实现Cloneable接口
class Sheep implements Cloneable {
    private String name;

    public Sheep(String name) {
        this.name = name;
    }

    // 重写clone()方法
    // 这里是浅拷贝方法
    @Override
    public Sheep clone() {
        try {
            return (Sheep) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void setName(String name) {
        this.name = name;
    }

    public void display() {
        System.out.println("Sheep: " + name);
    }
}

public class PrototypePatternExample {
    public static void main(String[] args) {
        Sheep originalSheep = new Sheep("Dolly");

        // 复制原始对象
        Sheep clonedSheep = originalSheep.clone();
        clonedSheep.setName("Molly");

        originalSheep.display();  // 输出:Sheep: Dolly
        clonedSheep.display();    // 输出:Sheep: Molly
    }
}

在上面的示例中,我们创建了一个Sheep类,并实现了Cloneable接口。通过调用clone()方法,我们可以复制原始对象,并修改复制后的对象的属性。

原型模式的优点包括:

  1. 可以避免重复创建对象,提高性能。
  2. 可以通过复制现有对象来创建新对象,无需调用构造函数,简化对象的创建过程。

需要注意的是,在使用原型模式时,需要注意对象的深拷贝与浅拷贝的问题。默认情况下,clone()方法执行的是浅拷贝,即仅复制对象的引用,而不复制对象的内容。如果需要进行深拷贝,即复制对象及其所有引用的对象内容,需要在clone()方法中进行相应的处理

工厂模式

通过一个工厂类来实现对象的创建,而无需直接暴露对象的创建逻辑给客户端。
简单工厂模式的优点在于客户端 无需了解具体产品类的创建细节,只需通过工厂类来创建对象,并且工厂类可以根据客户端的需求来动态创建不同类型的对象。但是缺点也比较明显,如果需要创建的产品类数量较多,则工厂类的代码会变得很臃肿,不便于维护

举个例子

​编辑

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
copy
abstract class Animal {
    public abstract void sound();
}
class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("喵喵喵");
    }
}
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪汪");
    }
}
// 创建一个工厂类
class AnimalFactory {
    // 定义一个静态方法,根据传入的参数创建具体的产品类对象
    public static Animal createAnimal(String type) {
        if (type.equalsIgnoreCase("dog")) {
            return new Dog();
        } else if (type.equalsIgnoreCase("cat")) {
            return new Cat();
        } else {
            throw new IllegalArgumentException("Invalid animal type: " + type);
        }
    }
}
// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 使用工厂类创建不同的 Animal 对象
        Animal dog = AnimalFactory.createAnimal("dog");
        dog.sound();
        Animal cat = AnimalFactory.createAnimal("cat");
        cat.sound();
    }
}

在这个示例中,有一个抽象类 Animal,它定义了一个抽象方法 sound()Cat 和 Dog 类都继承自 Animal 类,并实现了 sound() 方法,分别输出 “喵喵喵” 和 “汪汪汪”。

AnimalFactory 是工厂类,其中定义了一个静态方法 createAnimal(String type),根据传入的参数来创建具体的产品类对象。如果传入的参数是 “dog”,则返回一个 Dog 对象;如果是 “cat”,则返回一个 Cat 对象。如果传入的参数不是合法的动物类型,则抛出一个异常。

在客户端代码中,通过调用 AnimalFactory.createAnimal() 方法,可以根据需要创建不同的 Animal 对象。在这个例子中,首先创建了一个 Dog 对象 dog,然后调用了它的 sound() 方法,输出 “汪汪汪”;接着创建了一个 Cat 对象 cat,并调用了它的 sound() 方法,输出 “喵喵喵”。

工厂方法模式的优点是,客户端代码与具体产品类解耦,只依赖于抽象产品类和工厂类。这样,如果需要添加新的具体产品类,只需要添加一个对应的产品类和工厂方法即可,而不需要修改客户端代码。这符合开闭原则,使得系统更加灵活和可扩展。

抽象工厂模式

通过定义一个创建对象的接口来创建对象,但将具体实现的决定留给子类来决定。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

举个例子

​编辑

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
copy
// 创建一个抽象产品类
abstract class Animal {
    public abstract void sound();
}
// 创建具体产品类,继承自 Animal 类
class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("喵喵喵");
    }
}
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪汪");
    }
}

abstract class AnimalFactory {
    // 定义一个抽象方法,用于创建 Animal 对象
    public abstract Animal createAnimal();
}
class CatFactory extends AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Cat();
    }
}

class DogFactory extends AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}
// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建一个 Dog 对象
        AnimalFactory dogFactory = new DogFactory();
        Animal dog = dogFactory.createAnimal();
        dog.sound();

        // 创建一个 Cat 对象
        AnimalFactory catFactory = new CatFactory();
        Animal cat = catFactory.createAnimal();
        cat.sound();
    }
}

这段代码是一个抽象工厂方法模式的示例。抽象工厂方法模式是在工厂方法模式的基础上进行了扩展,它将具体工厂类的创建抽象化,使得客户端可以通过选择不同的具体工厂类来创建不同系列的产品。

在这个示例中,有一个抽象产品类 Animal,其中定义了一个抽象方法 sound()Cat 和 Dog 类都继承自 Animal 类,并实现了 sound() 方法,分别输出 “喵喵喵” 和 “汪汪汪”。

AnimalFactory 是一个抽象工厂类,其中定义了一个抽象方法 createAnimal(),用于创建 Animal 对象。

CatFactory 和 DogFactory 是具体工厂类,它们分别继承自 AnimalFactory 并实现了 createAnimal() 方法,分别返回 Cat 和 Dog 对象。

在客户端代码中,首先创建了一个 DogFactory 对象 dogFactory,然后通过 dogFactory.createAnimal() 方法创建了一个 Dog 对象 dog,并调用了它的 sound() 方法,输出 “汪汪汪”。接着创建了一个 CatFactory 对象 catFactory,通过 catFactory.createAnimal() 方法创建了一个 Cat 对象 cat,并调用了它的 sound() 方法,输出 “喵喵喵”。

抽象工厂方法模式的优点是,它提供了一种创建产品族的方式,即客户端可以通过选择不同的具体工厂类来创建不同系列的产品。这种方式更加符合开闭原则,使得系统更加灵活和可扩展。

建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,用于创建复杂对象。它将对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式允许你通过一步一步地构建复杂对象来创建不同类型的对象。它使用一个建造者类来封装对象的创建过程并将其分解为多个简单的步骤。这使得你可以通过更改这些步骤来创建不同类型的对象。

建造者模式通常包含以下角色:

  1. 产品(Product):要创建的复杂对象,它由多个部分组成。
  2. 抽象建造者(Abstract Builder):定义了创建产品各个部分的抽象接口。
  3. 具体建造者(Concrete Builder):实现了抽象建造者接口,具体实现产品各个部分的构建方法。
  4. 指挥者(Director):负责使用建造者来构建产品,它只与抽象建造者进行交互,不直接与具体产品类交互。

建造者模式的核心思想是将一个复杂对象的构建过程分解为多个简单对象的构建过程,并通过一个指挥者来协调这些过程,从而实现对复杂对象的创建。通过使用建造者模式,可以将对象的构建过程与其表示相分离,使得同样的构建过程可以创建不同的表示。

举个例子

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
copy
// 产品类
class Car {
    private String brand;
    private String color;

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void display() {
        System.out.println("Car: " + brand + ", Color: " + color);
    }
}

// 抽象建造者
interface CarBuilder {
    void buildBrand();
    void buildColor();
    Car getResult();
}

// 具体建造者
class ConcreteCarBuilder implements CarBuilder {
    private Car car;

    public ConcreteCarBuilder() {
        car = new Car();
    }

    @Override
    public void buildBrand() {
        car.setBrand("BMW");
    }

    @Override
    public void buildColor() {
        car.setColor("Red");
    }

    @Override
    public Car getResult() {
        return car;
    }
}

// 指挥者
class Director {
    private CarBuilder carBuilder;

    public Director(CarBuilder carBuilder) {
        this.carBuilder = carBuilder;
    }

    public void construct() {
        carBuilder.buildBrand();
        carBuilder.buildColor();
    }
}

public class BuilderPatternExample {
    public static void main(String[] args) {
        CarBuilder builder = new ConcreteCarBuilder();
        Director director = new Director(builder);

        director.construct();
        Car car = builder.getResult();
        car.display();  // 输出:Car: BMW, Color: Red
    }
}

在上面的示例中,我们以汽车的创建为例。Car类是要创建的复杂对象,CarBuilder是抽象建造者,ConcreteCarBuilder是具体建造者,Director是指挥者。

通过指挥者来构建产品,客户端只需与指挥者进行交互,无需直接与具体建造者或产品类交互。通过不同的具体建造者,可以创建不同的表示。

建造者模式的优点包括:

  • 可以创建复杂对象,而无需直接调用其构造函数。
  • 可以更加精细地控制对象的创建过程,使得不同的构建过程可以创建不同的表示。
  • 可以将对象的构建过程与其表示相分离,提高代码的可维护性和可扩展性。

答疑解惑

为什么使用双检锁机制时,必须将instance成员变量声明为volatile

我们先来看一下Java单例设计模式中的懒汉式+双检锁的Java代码是如何写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
copy
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;
    }
}

我们注意到,这与懒汉式+普通锁机制不太一样,那就是instance实例前加了volatile关键字,详细解释如下:

在使用双检锁机制时,确保 instance 成员变量声明为 volatile 是很重要的,主要是因为 Java 内存模型中的指令重排问题

在不考虑线程安全的情况下,如果多个线程同时调用 getInstance() 方法,可能会导致创建多个实例的问题。为了解决这个问题,我们需要加锁来保证只有一个线程可以访问 getInstance() 方法,从而保证只会创建一个实例。

但是,仅加锁还不够。由于指令重排的存在,当一个对象被初始化时,可能会出现以下情况:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向分配的内存空间

在单线程环境下,这些操作的执行顺序是确定的,因为虚拟机会按照代码编写的顺序执行这些操作。但是,在多线程环境下,由于指令重排的存在,可能会出现以下这种情况:

  1. 线程 A 进入 getInstance() 方法。
  2. 线程 A 判断 instance 是否为 null,发现 instance 为空,于是进入同步块。
  3. 线程 B 也进入 getInstance() 方法。
  4. 线程 B 判断 instance 是否为 null,发现 instance 为空,于是进入同步块。
  5. 线程 A 获取锁,开始执行 instance = new Singleton() 的语句。由于指令重排,可能先分配内存空间,再将对象指向分配的内存空间。
  6. 线程 B 获取锁,开始执行 instance = new Singleton() 的语句。由于指令重排,可能先判断 instance 是否为 null,发现不是 null,然后直接返回 instance
  7. 线程 B 返回 instance,但此时 instance 并没有被初始化,访问 instance 将会导致空指针异常。

为了避免以上情况的发生,我们需要使用 volatile 关键字来保证 instance 成员变量的可见性和有序性。当一个变量被声明为 volatile 时,Java 内存模型会确保所有线程对该变量的读写都是原子的,并且在读写该变量时会进行内存屏障操作,防止指令重排。

因此,在使用双检锁机制时,必须将 instance 成员变量声明为 volatile,以确保多线程环境下的可见性和有序性,避免出现空指针异常等问题。

为什么静态内部类的加载是线程安全的

静态内部类的加载是线程安全的,主要是因为 Java 虚拟机在加载类的过程中会使用一种叫做类初始化(Class Initialization)的机制来保证线程安全。

在 Java 中,类的加载过程是由虚拟机负责的,它会按需加载类的字节码,并进行类的初始化操作。类初始化阶段是一个线程安全的过程,Java 虚拟机会确保同一时刻只有一个线程去执行类的初始化操作,并且在类初始化期间,虚拟机会对类进行加锁,防止其他线程同时进行初始化。

静态内部类的加载是在使用时才会被加载,而且在类初始化阶段只会加载一次。这意味着在多线程环境下,即使多个线程同时请求获取静态内部类的实例,虚拟机也会保证只有一个线程进行类的初始化,并且在类初始化期间对类进行加锁,避免了多线程并发访问的问题。

因此,静态内部类的加载是线程安全的,可以放心使用该机制来实现懒汉式单例模式,避免了显式加锁和双重检查锁定带来的性能损耗和复杂性。

为什么使用枚举来实现单例模式可以保证线程安全并且可以防止反序列化创建新实例

保证线程安全

枚举类型是Java中的一种特殊类型,JVM会保证在任何情况下,每个枚举值只被实例化一次。因此,使用枚举来实现单例模式可以避免多线程并发访问导致的线程安全问题。即使在高并发场景下,也能保证只有一个实例对象。

防止反序列化创建新实例

在Java中,对象序列化是将对象转换为字节序列以便于存储和传输的过程,而反序列化则是将字节序列转换回对象。在某些场景下,需要将对象序列化后存储到磁盘或通过网络传输,然后在需要时再反序列化得到原对象。但是,如果不加特殊处理,序列化和反序列化过程中可能会导致单例模式失效,从而创建多个实例对象。

对于使用常规方式(如饿汉式、懒汉式等)实现的单例模式,反序列化操作可能会导致创建新的实例对象。这是因为在反序列化过程中,JVM会根据序列化数据重新创建一个新的对象实例,并不会调用单例类的构造方法。因此,在反序列化过程中,即使已经存在单例实例,仍然可能会生成新的实例。

为了防止反序列化创建新实例,可以采用以下两种方式:

  1. 使用枚举类型:枚举类型是Java中的一种特殊类型,JVM会保证在任何情况下,每个枚举值只被实例化一次。因此,使用枚举来实现单例模式可以避免反序列化操作导致的单例失效问题,从而保证单例对象的唯一性。
  2. 自定义序列化和反序列化方法:通过在单例类中自定义writeObject()和readObject()方法,可以在序列化和反序列化过程中对单例实例进行特殊处理,以避免创建新的实例对象。例如,在writeObject()方法中,将单例实例写入序列化流;在readObject()方法中,先从序列化流中读取单例实例,然后返回该实例。这样就能够保证反序列化得到的对象和序列化前的对象是同一个实例。

综上所述,使用枚举类型或自定义序列化和反序列化方法都可以解决反序列化创建新实例的问题,从而保证单例对象的唯一性。

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值