创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节
创建型模式分为:
- 单例模式
- 工厂方法模式
- 抽象工厂模式
- 原型模式
- 建造者模式
1. 单例模式
1.1 介绍
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象
1.2 结构
单例模式的主要有以下角色:
- 单例类。只能创建一个实例的类
- 访问类。使用单例类
1.3 实现
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式 1:静态变量
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量并创建该对象,并且需要设置成 static,因为静态方法中只能调用 static 属性,并且需要控制只有一个对象
private static Singleton instance = new Singleton();
// 3. 由于构造器进行了私有化不能直接 new, 因此需要提供公共的静态的方法,返回类的对象
public static Singleton getInstance() {
return instance;
}
}
说明:
- instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费
饿汉式 2:静态代码块
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量
private static Singleton instance;
// 3. 使用静态代码块创建该对象
static {
instance = new Singleton();
}
// 4. 对外提供静态成员方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
说明:
- 该方式和饿汉式的方式 1 基本一样,所以该方式也存在内存浪费问题
饿汉式 3:枚举🚀
首先,枚举方式是饿汉式单例模式,如果不考虑浪费内存空间的问题,这是极力推荐的单例实现模式
/**
* 饿汉式:枚举实现
*/
public enum Singleton {
INSTANCE;
}
说明:
- 因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式
- 枚举的写法非常简单,而且枚举方式是所有单例实现中唯一一种不会被破坏的单例实现模式
懒汉式 1:线程不安全
该方式在成员位置声明 Singleton 类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?
当调用 getInstance()
方法获取 Singleton 类的对象的时候才创建 Singleton 类的对象,这样就实现了懒加载效果
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量
private static Singleton instance;
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
- 如果是多线程环境,会出现线程安全问题
懒汉式 2:线程安全
在懒汉式 1 的基础上,使用 synchronized
关键字解决了线程安全问题
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量
private static Singleton instance;
// 3. 对外提供访问对象的方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
- 在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低
- 其实只有在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了
懒汉式 3:双重检查锁🚀
再来讨论一下懒汉模式中加锁的问题,对于 getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机
由此也产生了一种新的实现模式:双重检查锁模式
/**
* 双重检查锁方式(有风险)
*/
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量
private static Singleton instance;
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
// 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断,抢占到锁以后再次判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
说明:
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题
- 在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性
/**
* 双重检查锁方式(标准)
*/
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 创建静态成员变量
private static volatile Singleton instance;
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
// 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断,抢占到锁以后再次判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题
懒汉式 4:静态内部类🚀
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序
/**
* 静态内部类方式
*/
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
说明:
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance(),虚拟机才加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费
1.4 单例破坏
有两种方式可以使上面定义的单例类可以创建多个对象(枚举方式除外),分别是序列化和反射
枚举方式是利用了 Java 特性实现的单例模式,不会被破坏,其他实现方式都有可能会被破坏
1.4.1 序列化和反序列化
(1)破坏演示
下面代码运行结果是false
,表明序列化和反序列化破坏了单例设计模式
Singleton类:
public class Singleton implements Serializable {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Test类:
/**
* 测试使用序列化破坏单例模式
*/
public class Test {
public static void main(String[] args) throws Exception {
//writeObjectToFile();
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
System.out.println(s1 == s2); // false
}
// 向文件中写数据(对象)
public static void writeObjectToFile() throws Exception {
// 1.获取Singleton对象
Singleton instance = Singleton.getInstance();
// 2.创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\a.txt"));
// 3.写对象
oos.writeObject(instance);
// 4.释放资源
oos.close();
}
// 从文件读取数据(对象)
public static Singleton readObjectFromFile() throws Exception {
// 1.创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\a.txt"));
// 2.读取对象
Singleton instance = (Singleton) ois.readObject();
// 3.释放资源
ois.close();
return instance;
}
}
(2)解决方案
在 Singleton 类中添加readResolve()
方法
在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象
Singleton类:
/**
* 静态内部类方式(解决序列化破解单例模式)
*/
public class Singleton implements Serializable {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
(3)源码解析
从代码中的读取输入流方法 readObject()
入手,逐个查看其底层源码
一路点击 readObject()
方法直到 readObject0()
方法,点击进去
找到switch通道中的对象入口,返回值会调用 readOrdinaryObject
方法,点进去 readOrdinaryObject()
方法中通过三目运算符判断了对象是否可实例化,如果是可实例化的会通过 newInstance()
方法反射实例化一个新的对象,所以序列化前的对象和反序列化后得到的对象不同
这时问题出现的原因就找到了,序列化/反序列化导致单例破坏的原因竟是因为其底层使用到了反射,而反射则是另外一个造成单例破坏的原因之一
- 在
Singleton
类中添加readResolve
方法后desc.hasReadResolveMethod()
方法执行结果为true
- 通过反射调用
Singleton
类中的readResolve
方法,将返回值赋值给rep
变量 - 这样多次调用
ObjectInputStream
类中的readObject
方法,继而就会调用我们定义的readResolve
方法,所以返回的是同一个对象
1.4.2 反射
(1)破坏演示
下面代码运行结果是false
,说明反射破坏了单例设计模式
Singleton类:
public class Singleton {
// 1. 私有化类的构造器
private Singleton() {
}
// 2. 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Test类:
/**
* 测试使用反射破坏单例模式
*/
public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}
(2)解决方案
当通过反射方式调用构造方法进行创建时,直接抛异常
Singleton类:
/**
* 静态内部类方式(解决反射破坏单例模式)
*/
public class Singleton {
private static boolean flag = false;
// 1. 私有化类的构造器
private Singleton() {
synchronized (Singleton.class) {
// 如果是true,说明非第一次访问,直接抛一个异常,如果是false,说明第一次访问
if (flag) {
throw new RuntimeException("不能创建多个对象");
}
flag = true;
}
}
// 2. 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供访问对象的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2. 工厂模式
根据百科的定义,工厂模式是用于创建其他对象的对象
2.1 概述
以咖啡店为例,设计一个咖啡类Coffee,并定义其两个子类(美式咖啡AmericanCoffee和拿铁咖啡LatteCoffee);再设计一个咖啡店类CoffeeStore,咖啡店具有点咖啡的功能
具体类的设计如下:
在上面的示例中,我们没有使用任何模式并且应用程序运行良好。但是如果我们考虑未来可能的变化并仔细观察,我们可以预见到当前实现存在以下问题:
- 在当前的应用程序中,无论哪里需要特定的咖啡,它都是使用具体类创建的。将来,如果是对类名进行修改/提出了不同的具体类,就必须在整个应用程序中进行更改;
- 目前创建具体的咖啡无论是美式还是拿铁,其构造函数参数列表都为空。如果后续对构造函数进行了修改,需要传入一些必要的参数,那么在每一个创建对象的客户端中都需要进行修改;
- 在创建对象时,在上面的代码中客户端知道具体类,也知道对象的创建过程,我们应该避免将此类对象创建细节和内部类暴露给客户端应用程序
通过上述问题我们总结出来,在不使用任何模式下程序运行正常,但是却有着耦合严重的问题。 假如我们要更换/修改对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就 是:解耦
2.2 简单工厂模式
2.2.1 介绍
简单工厂模式是 Factory
最简单形式的类(与工厂方法模式或抽象工厂模式相比)。换句话说,我们可以说:在简单工厂模式中,我们有一个工厂类,它有一个方法可以根据给定的输入返回不同类型的对象。简单工厂并不是一种设计模式,反而比较像是一种编程习惯
2.2.2 结构
简单工厂包含如下角色:
- 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能
- 具体产品 :实现或者继承抽象产品的子类
- 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品
2.2.3 实现
现在使用简单工厂对上面案例进行改进,类图如下:
工厂(factory)处理创建对象的细节,一旦有了 SimpleCoffeeFactory
,CoffeeStore
类中的 orderCoffee()
就变成此对象的客户,后期如果需要 Coffee
对象直接从工厂中获取即可。这样也就解除了客户端和Coffee实现类的耦合
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
} else {
throw new RuntimeException("对不起,您所点的咖啡没有");
}
return coffee;
}
}
在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的
public class SimpleCoffeeFactory {
public static Coffee createCoffee(String type) {
Coffee coffee = null;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
} else {
throw new RuntimeException("对不起,您所点的咖啡没有");
}
return coffee;
}
}
在设立工厂解除客户端与具体实现类之间耦合的同时,又产生了新的耦合,即 CoffeeStore
对象和 SimpleCoffeeFactory
工厂对象的耦合,工厂对象和商品对象的耦合。后期如果再加新品种的咖啡,我们势必要需求修改 SimpleCoffeeFactory
的代码,违反了开闭原则
但是如果我们考虑未来可能会出现的各种变化并仔细观察,简单工厂模式确实相对于没有使用任何模式的代码拓展性和可维护性更好。工厂类的客户端可能有很多,比如说我可能不只是这个咖啡店需要制作咖啡,奶茶店、早餐店可能都会生产咖啡,甚至是会生产不同的咖啡,这时我们新增对应的咖啡实现类之后,只需要修改工厂类的代码,省去其他类的修改操作,这种操作可以使得整个业务逻辑是符合开闭原则的
2.3 工厂方法模式
2.3.1 介绍
工厂方法模式(英语:Factory method pattern)属于创建模式类别。在GoF中对工厂方法模式的定义为:“定义用于创建对象的接口,但让子类决定要实例化哪个类。工厂方法将类的实例化推迟到子类”。这种模式能够解决简单工厂模式中存在的耦合问题,整体完全遵循开闭原则
2.3.2 结构
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
2.3.3 实现
在简单工厂模式中是以一个工厂来生产所有咖啡的,而在工厂方法模式中,则是将生产咖啡的工厂定义成抽象工厂,当我们需要生产美式咖啡和拿铁咖啡时,分别建立两个新工厂去实现抽象工厂,分别生产对应的咖啡
工厂的代码实现如下:
// 抽象工厂
public interface CoffeeFactory {
Coffee createFactory();
}
=========================
// 具体工厂
public class AmericanCoffeeFactory implements CoffeeFactory{
@Override
public Coffee createFactory() {
return new AmericanCoffee();
}
}
public class LatteCoffeeFactory implements CoffeeFactory{
@Override
public Coffee createFactory() {
return new LatteCoffee();
}
}
咖啡店即客户端的代码实现如下:
public class CoffeeStore {
private CoffeeFactory factory;
// 通过传递具体工厂的实现类生产对应的咖啡
public void setFactory(CoffeeFactory factory) {
this.factory = factory;
}
// 下单生产咖啡
public Coffee orderCoffee() {
Coffee coffee = factory.createFactory();
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。在工厂方法模式中使用了多态的特性,在保持简单工厂模式特点的同时,还解决了遗留下来的问题:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,符合开闭原则
但利与弊同在,每增加一个产品时就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,容易引起类爆炸
2.4 抽象工厂模式
2.4.1 介绍
抽象工厂模式(英语:Abstract factory pattern)是一种软件开发设计模式。抽象工厂模式的实质是“提供接口,创建一系列相关或独立的对象,而不指定这些对象的具体类。”在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来
2.4.2 结构
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系
2.4.3 实现
以Windows和Mac两个系统的不同样式为例,这两个系统都存在按钮和边框这两种元素,当我们需要切换两种不同系统的样式时,可以直接通过Windows工厂或Mac工厂进行统一切换。当我们添加另外一个系统的样式时,如Linux系统的样式,只需要新增一个Linux工厂去实现抽象工厂即可,无需去修改原有代码
- 定义产品抽象类,即上方说的抽象产品:
public abstract class Button {}
public abstract class Border {}
- 继承抽象类,即上方说的具体产品:
public class MacButton extends Button {}
public class MacBorder extends Border {}
public class WinButton extends Button {}
public class WinBorder extends Border {}
- 定义产品抽象工厂:
public interface AbstractFactory {
Button createButton();
Border createBorder();
}
- 实现抽象工厂,定义具体工厂:
public class MacFactory implements AbstractFactory {
public Button createButton() {
return new MacButton();
}
public Border createBorder() {
return new MacBorder();
}
}
---
public class WinFactory implements AbstractFactory {
public Button createButton() {
return new WinButton();
}
public Border createBorder() {
return new WinBorder();
}
}
2.4.4 使用场景
在以下情况可以考虑使用抽象工厂模式:
- 忽略创建细节:客户端不关心产品实例如何被创建、实现等细节
- 创建产品族:强调一系列相关的产品对象,一般是同一个产品族,一起使用创建对象需要大量重复的代码
- 产品类库:提供一个产品类的库,所有的产品以同样的接口出现,使客户端不依赖于具体实现
2.5 简单工厂模式改进
2.5.1 介绍
通常我们在使用简单工厂模式的时候会由创建方法create通过传入的参数来判断要实例化哪个对象,就像下面这样
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
这里面依旧是两种咖啡,通过type来判断需要生产哪种咖啡,即决定实例化哪个子类。现在遇到这么一个问题,如果新增一个子类的话,那就必须要修改工厂类代码了,这也是简单工厂模式的痛点。可能你会说“改一下又何妨?”,虽不说影响不影响什么开闭设计原则,但是有个情况你可曾想到,你这个类要打包发布给别人用呢?别人在没有源码的情况下如何扩展呢?这里就需要我们动态的通过配置文件来加载实现类了
2.5.2 实现
实现的基本思路为:通过读取本地的 .properties
文件来获取我们需要实例化的类,然后通过反射来生成对象,这样当你把发布出去的时候,使用者只用更改配置文件就可以让工厂去实例化自己后来才写的实现类。
在Resource目录下创建配置文件 bean.properties
,并在其中指明具体产品的全类名:
american=com.enndfp.pattern.factory.config_factory.AmericanCoffee
latte=com.enndfp.pattern.factory.config_factory.LatteCoffee
改进工厂类,不再是显式实例化具体的对象,而是在获取配置文件中的全类名之后,通过反射对产品进行实例化:
public class CoffeeFactory {
// 加载配置文件,获取配置文件中的全类名,并创建该类的对象进行存储
// 1. 定义容器对象存储咖啡对象
private static Map<String, Coffee> map = new HashMap<>();
// 2. 加载配置文件,只需要加载一次
static {
// 2.1 创建properties对象
Properties p = new Properties();
// 2.2 调用p对象中的load方法进行配置文件的加载
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
p.load(is);
// 从p集合中获取全类名并创建对象
Set<Object> keys = p.keySet();
for (Object key : keys) {
String className = p.getProperty((String) key);
// 通过反射技术创建对象
Class clazz = Class.forName(className);
Coffee coffee = (Coffee) clazz.newInstance();
// 将名称和对象存储到容器
map.put((String) key, coffee);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 根据名称获取对象
public static Coffee createCoffee(String name) {
return map.get(name);
}
}
2.6 JDK中的迭代器
对下面的代码大家应该很熟,使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("令狐冲");
list.add("风清扬");
list.add("任我行");
//获取迭代器对象
Iterator<String> it = list.iterator();
//使用迭代器遍历
while(it.hasNext()) {
String ele = it.next();
System.out.println(ele);
}
}
}
我们看通过类图看看结构:
Collection
接口是抽象工厂类,ArrayList
是具体的工厂类;Iterator
接口是抽象商品类,ArrayList
类中的Iter内部类
是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象
同时在JDK中还有许多地方也用到了工厂模式,如:
DateForamt
类中的getInstance()
方法使用的是工厂模式Calendar
类中的getInstance()
方法使用的是工厂模式
3. 原型模式
3.1 介绍
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。这种类型的设计模式属于创建型模式,用于创建重复对象的同时又能保证性能,是创建对象的最佳方式之一
3.2 优缺点
优点:
- 性能高:使用原型模式复用的方式创建实例对象,比使用构造函数重新创建对象性能要高;(针对类实例对象开销大的情况)
- 流程简单:原型模式可以简化创建的过程,可以直接修改现有的对象实例的值,达到复用的目的;(针对构造函数繁琐的情况)
缺点:
- 重写 clone 方法(必须):必须重写对象的 clone 方法,Java 中提供了 cloneable 标识该对象可以被拷贝,但是必须重写 Object 的 clone 方法才能被拷贝
- 深拷贝与浅拷贝风险:克隆对象时进行的一些修改,容易出错;需要灵活运用深拷贝与浅拷贝操作
3.3 结构
原型模式包含如下角色:
- 抽象原型类:规定了具体原型对象必须实现的 clone() 方法
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象
3.4 浅拷贝和深拷贝
- 浅拷贝:当拷贝对象只包含简单的数据类型比如int、float 或者不可变的对象(字符串)时,就直接将这些字段复制到新的对象中。而引用的对象并没有复制而是将引用对象的地址复制一份给克隆对象
- 深拷贝:不管拷贝对象里面简单数据类型还是引用对象类型都是会完全的复制一份到新的对象中
3.5 浅拷贝实现
最简单与常用的便是浅拷贝,以给三好学生发奖状为例
3.5.1 实现步骤
- 原型类实现Cloneable接口并重写clone方法:
@Data
public class Student {
private String name;
private String address;
public Student(String name, String address) {
this.name = name;
this.address = address;
}
}
@Data
public class Citation implements Cloneable {
/**
* 学期
*/
private int semester;
/**
* 学校
*/
private String school;
/**
* 时间戳
*/
private Long timestamp;
private Student student;
public Citation() {
System.out.println("正在创建奖状……");
}
public void show() {
System.out.println(student.getName() + "同学:在2023学年第一学期表现优秀,被评为三好学生。特发此状!");
System.out.println(school + "于" + timestamp + "颁发");
}
@Override
public Citation clone() {
try {
return (Citation) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
- 测试类
public class CitationTest {
public static void main(String[] args) {
// 生成原型奖状
Citation c1 = new Citation();
c1.setStudent(new Student("张三", "西安"));
c1.setSchool("牛马大学");
c1.setSemester(1);
c1.setTimestamp(System.currentTimeMillis());
// 复制奖状
Citation c2 = c1.clone();
// 分别调用show方法
System.out.println("===== c1的show =====");
c1.show();
System.out.println("===== c2的show =====");
c2.show();
System.out.println("===== ======= =====");
// 比较原型与克隆对象地址
System.out.println("比较原型与克隆对象地址:" + (c1 == c2));
// 比较成员属性地址
System.out.println("比较学生成员属性的地址(Student):\t" +
(c1.getStudent() == c2.getStudent()));
System.out.println("比较学校成员属性的地址(String):\t" +
(c1.getSchool() == c2.getSchool()));
System.out.println("比较学期成员属性的地址(int):\t" +
(c1.getSemester() == c2.getSemester()));
System.out.println("比较时间成员属性的地址(Long):\t" +
(c1.getTimestamp() == c2.getTimestamp()));
}
}
- 运行结果
3.5.2 结果分析
- 构造器
在克隆生成c2时并没有调用构造器,只有在手动创建c1时才会调用构造器,这证明这是使用了原型模式复制的方式创建实例对象
- 原型对象与克隆对象比较
通过比较,这两个对象的地址并不相同,这证明虽然复制过程没有调用构造器,但是生成的对象是一个全新的实例,在堆中有属于自己的地址
- 引用类型比较
在奖状类中定义了三种实际应用中常遇到的类型,分别是自定义引用类型、字符串类型和包装类型,但是通过比较,原型对象和拷贝对象的引用类型为同一个对象,共用一个地址
- 普通数据类型比较
这里还设置了一个int类型的普通数据类型,由于普通类型是将值直接赋值到新的拷贝对象中, 因此这里的 ==
比较的是两者的值
3.6 深拷贝思路
3.6.1 clone方法
经过上述浅拷贝的介绍,我们知道在实现原型模式最重要的便是实现Cloneable接口,重写 clone 方法,因此我们可以在重写clone方法时,再将成员变量里的引用类型变量进行克隆(前提是这一引用类型支持拷贝)
3.6.2 序列化与反序列化
正是可以破坏单例因素之一的序列化与反序列化,通过对象流将对象输出后再写入,这种方式生成的拷贝对象便是深拷贝后的实例对象
针对上面的两种写法其实都是可以实现原型模式的,但是不管用哪种方式,深拷贝都比浅拷贝花时间和空间,所以还是酌情考虑。其实在现在已经有很多针对浅拷贝和深拷贝的工具类
- 深拷贝(deep copy):SerializationUtils
- 浅拷贝(shallow copy):BeanUtils
3.7 使用场景
- 对象的创建成本较高:如果创建一个对象的成本很高,例如需要复杂的初始化过程、数据库连接或网络请求,可以使用原型模式来避免多次创建相同的对象。只需克隆现有对象即可降低创建成本
- 大量相似对象的创建:当需要创建大量类似的对象时,原型模式可以帮助减少对象创建的开销,因为你只需创建一个原型对象,然后克隆它来生成其他对象
- 对象的初始化配置复杂:有些对象的初始化配置非常复杂,包括多个步骤和依赖关系。在这种情况下,可以通过克隆现有对象来避免重新配置所有属性
- 避免构造函数的约束:有些对象的构造函数需要特定的参数,但你可能希望在不同的上下文中使用相同的对象。原型模式允许你在创建对象时绕过构造函数的参数限制,只需克隆一个已有对象并根据需要修改它的属性
- 对象的状态保存和恢复:原型模式也可用于对象的状态保存和恢复。你可以在需要保存对象状态时创建其原型,并在需要恢复状态时克隆原型对象
- 避免共享状态:某些情况下,你希望每个对象都具有独立的状态,而不是共享状态。原型模式可以确保每个对象都有自己的状态,而不会共享
4. 建造者模式
4.1 介绍
建造者模式(Builder Pattern)旨在将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。简单来说就是使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于建造者模式,是创建对象的最佳方式之一,其主要特点如下:
- 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于某个对象的构建过程复杂的情况
- 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用
- 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节
通过建造者模式,在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象
- 用户只需要给出指定复杂对象的类型和内容
- 建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)
4.2 结构
建造者(Builder)模式包含如下角色:
-
抽象建造者类(Builder):这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的部件对象的创建
-
具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例
-
产品类(Product):要创建的复杂对象
-
指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建
4.3 实现
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。
这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。类图如下:
- 产品类:
public class Bike {
// 车架
private String frame;
// 座椅
private String seat;
// 省略get、set和toString()方法
}
- 抽象建造者类:
public abstract class Builder {
protected Bike bike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
}
- 具体建造者类:
public class MobikeBuilder extends Builder{
@Override
public void buildFrame() {
bike.setFrame("铝合金车架");
}
@Override
public void buildSeat() {
bike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return bike;
}
}
public class OfoBuilder extends Builder {
@Override
public void buildFrame() {
bike.setFrame("碳纤维车架");
}
@Override
public void buildSeat() {
bike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return bike;
}
}
- 指挥者类:
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 创建指挥者对象
Bike moBike = new Director(new MobikeBuilder()).construct();
Bike ofoBike = new Director(new OfoBuilder()).construct();
System.out.println("摩拜单车:\t" + moBike);
System.out.println("Ofo单车:\t" + ofoBike);
}
}
- 结果展示
上面示例是 Builder 模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指挥具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类
4.4 模式扩展
除了上述的用途外,还有另外一个常用的使用方式,就是当一个类构造器需要传入很多参数时, 如果创建这个类的实例,代码可读性会非常差,而且很容易引发错误,此时就可以利用建造者模式进行重构
ps: 这个例子在开发过程中一般不需要我们自己编写,若有导入Lombok包的话,直接在实体类上方加入 @Builder 注解即可实现
这里用项目中常用到的用户类进行举例,用户类一般拥有众多属性,如果直接通过全参构造器进行赋值可读性将会极差,用建造者模式重构如下:
- 用户类:
public class User {
private String name;
private Integer age;
private String gender;
private String city;
// 私有化构造函数
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.gender = builder.gender;
this.city = builder.city;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", city='" + city + '\'' +
'}';
}
// 建造者
public static final class Builder {
private String name;
private Integer age;
private String gender;
private String city;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Builder gender(String gender) {
this.gender = gender;
return this;
}
public Builder city(String city) {
this.city = city;
return this;
}
public User build() {
return new User(this);
}
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
User user = new User.Builder()
.name("张三")
.age(21)
.gender("男")
.city("北京")
.build();
System.out.println(user);
}
}
运行结果如下,可以看到通过建造者构建得到的用户类是正常的,重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。这里演示的入参只有四个,如果有更多,那么建造者模式的优势则是更为明显:
4.5 优缺点
优点:
- 使用建造者模式可以使客户端不必知道产品内部组成的细节
- 具体的建造者类之间是相互独立的,这有利于系统的扩展
- 具体的建造者相互独立,因此可以对建造的过程逐步细化,而不会对其他模块产生任何影响
缺点:
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大
4.6 使用场景
- 创建复杂的对象:当需要创建一个包含多个部分或参数的复杂对象时,建造者模式可以将对象的构建细节封装在不同的建造者类中,使得客户端代码可以更简单地构建对象
- 参数选项灵活:如果一个对象具有多个可选参数,而不是所有参数都需要在每次构建对象时提供,建造者模式可以使用链式调用或者多个方法设置参数,使得客户端可以根据需要选择性地设置参数
- 避免构造器参数过多:当一个类的构造器参数较多时,使用构造函数传递所有参数可能会变得复杂和难以维护。建造者模式可以通过将参数逻辑划分到不同的建造者方法中来简化构造器
- 创建不可变对象:建造者模式可以用于创建不可变对象,因为一旦对象被构建,就可以防止进一步的修改
- 配置对象创建:在某些情况下,建造者模式可以用于创建配置对象,用于存储配置信息,并且可以在不同地方重用相同的配置对象
- 适用于流式API:建造者模式非常适合用于创建流式API,其中客户端可以链式调用多个方法来设置对象的属性
5. 创建者模式对比
5.1 工厂方法模式VS建造者模式
工厂方法模式:
每个具体超人工厂子类将负责创建不同类型的超人,例如"飞翔的超人工厂"、"力大无穷的超人工厂"等。客户端代码将根据需要选择适当的工厂来创建超人对象
建造者模式:
你将有一个超人建造者类,该类包含用于构建超人的各个部分的方法,例如设置头部、设置手部、设置躯干、设置脚部、设置内裤等。客户端代码将使用建造者逐步构建超人对象,并且可以以不同的顺序和方式配置超人的各个部分
比较:
- 工厂方法模式会创建具有特定特征的超人对象,但这些特征是预定义的,不一定需要客户端配置,此模式更注重的是整体对象的创建方式
- 建造者模式允许客户端以更灵活的方式自定义超人对象的不同部分,并按照需要组装它们。此模式更注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象
5.2 抽象工厂模式VS建造者模式
抽象工厂模式:
在汽车配件生产工厂的抽象工厂模式中,我们可以有不同的抽象工厂,每个工厂负责生产特定品牌的汽车及其相关配件。例如,我们可以有一个"德国汽车工厂",一个"日本汽车工厂"等。每个抽象工厂都有一组方法,用于生产一整个产品族,包括汽车、发动机、轮胎等。客户端可以选择使用特定的抽象工厂来生产一整套相关的汽车及其零部件,而无需关心具体的生产过程。这使得我们可以轻松地切换到不同品牌的汽车和零部件
建造者模式:
在汽车组装工厂的建造者模式中,我们有一个汽车建造者,它负责逐步构建一辆汽车。汽车建造者可以配置汽车的各个部分,例如车身、引擎、轮胎、内饰等。客户端代码可以使用汽车建造者,逐步构建自己所需的汽车。客户端可以以不同的顺序和方式配置汽车的各个部分,从而定制汽车的特性。最终,通过调用build()
方法,客户端可以获取一辆完整的汽车
比较:
- 抽象工厂模式关注一整个产品族的生产,例如德国汽车及其相关配件,它将这些产品视为一个整体。建造者模式关注逐步构建一个复杂对象,例如一辆汽车,客户端可以定制其各个部分
- 抽象工厂模式强调一致性,确保所有产品都适用于特定品牌的汽车。建造者模式强调个性化,允许客户端根据需要自定义汽车的各个特性