定义:用于创建重复的对象,同时又能保证性能。在这种模式中我们要实现一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
这个定义是菜鸟教程中的定义,我第一眼看上去还是有些不明所以的,但是当熟悉这种涉及模式之后就会发现这种模式真正的存在意义。接下来我们就来解开它朦胧的面纱吧。
这个设计模式比较抽象,我们先来介绍一下Java中创建对象的几种方式吧。
首先我们先设计一个Java对象类
import lombok.Data;
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 23:05
* @version: 1.0
*/
@Data
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
在Java中,我们平时创建对象的方式有以下5种方式:
1. 通过new关键字去创建(可以调用任意的构造器)
2.通过反射创建对象
弱类型,易回收,只可调用public类型的空参构造器。
这里就用到了类加载的方式,通过类加载器去创建对象。
这里我们要说一下这两种创建对象方式的区别:
new关键字是强类型的,效率相对较高,可调用有参构造或无参构造,GC不会自动回收,等待对象指针为0时才有可能被回收。
newInstance方法是弱类型的,效率相对较低,可调用无参构造,GC会自动回收。
newInstance方法虽然效率较低,但是可以提供给我们更高的灵活性。运用这种方式,我们就可以在不改变代码的情况下通过类名字符串去创建对应的对象。一般用于工厂模式,例子如下:
// CodeFactory、TestFactory是一个实现类,AbstractFactory是一个接口
// forName会启动类加载器去加载同名类
Class<?> factoryClass = Class.forName("CodeFactory");
Class<?> factoryClass = Class.forName("TestFactory");
// 获取类对象进行创建对应对象
AbstractFactory factory= (AbstractFactory) factoryClass.newInstance();
3.通过构造器对象的newInstance方法去创建对象
我们通过构造器对象去创建对象的时候,虽然同样是弱类型,但是我们可以调用有参数的构造器了。而且也可以调用类的私有构造器了。
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 22:01
* @version: 1.0
*/
public class PrototypePatternDemo {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 获取类的构造器,如果有参数根据顺序填入类型
Constructor<Person> constructor = Person.class.getConstructor(String.class, int.class);
// 获取类的私有构造器,如果有参数根据顺序填入类型
Constructor<Person> constructor = Person.class.getDeclaredConstructor(String.class, int.class);
// 调用私有的构造器需要将此属性设置为true
constructor.setAccessible(true);
// 构建参数
Person person = constructor.newInstance("张三", 12);
System.out.println(person.toString());
}
}
4.反序列化(天然深拷贝)
我们创建的Java对象需要实现Serializable接口才能进行序列化及反序列化
我们可以通过序列化,将一个Java对象以文件的方式保存到硬盘中,当我们需要创建对象的时候可以直接访问文件,通过反序列化的方式创建一个Java对象。需要注意的是这种方式及其消耗内存,工作中不推荐使用。
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 22:01
* @version: 1.0
*/
public class PrototypePatternDemo {
public static void main(String[] args) {
String filePath = "E:\\data.obj";
try {
//序列化过程
Path path = Paths.get(filePath);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(path));
objectOutputStream.writeObject(new Person("张三", 12));
objectOutputStream.close();
//反序列化过程
ObjectInputStream inputStream = new ObjectInputStream(Files.newInputStream(path));
Person person = (Person) inputStream.readObject();
inputStream.close();
//console打印
System.out.println("====>[5]使用反序列化创建对象");
System.out.println("姓名:" + person.getName());
System.out.println("年龄:" + person.getAge());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
5.克隆
我们可以通过实现Cloneable接口并重写clone方法,通过clone方法来创建对象。
我们将构建好的Person类代码重新放在了下面。
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 23:05
* @version: 1.0
*/
@Data
public class Person implements Serializable, Cloneable {
private String name;
private int age;
private Person person;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
this.person = new Person();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 深拷贝
@Override
protected Person clone() throws CloneNotSupportedException {
Person clone = (Person) super.clone();
// 单属性进行clone
clone.person = person.clone();
// clone之后再进行返回
return clone;
}
// 浅拷贝
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
创建Person对象的方法
Person person = new Person("张三", 15);
// 属于浅拷贝
Person clone = person.clone();
这里也顺便一提什么是浅拷贝,什么是深拷贝。
我们通过clone方法创建的对象都是一个新对象,深拷贝与浅拷贝都是一样的,他俩的区别在于对拷贝对象中引用类型属性的拷贝。
浅拷贝:对象中的引用类型不拷贝,直接使用原对象中的引用类型属性。
深拷贝:对象中的引用类型同样进行拷贝,创建新对象,并将原引用类型属性的值赋值给新属性。
clone方法没有调用构造方法,效率一般来说没有new关键字创建对象效率高(只有当构造方法中含有耗时操作时clone方法的效率会高于new关键字创建对象),可用于创建重复对象使用。
下面总结一下上面这五种创建对象方式的区别:
new关键字 | 反射newInstance方法 | 构造器newInstance方法 | 反序列化 | clone方法 | |
是否调用了构造器 | 是 | 是(仅可使用无参构造) | 是 | 否 | 否 |
创建效率 | 高 | 中 | 中 | 慢 | 中 |
我们今天讲到的原型模式就是基于clone方法去创建对象的一种模式,上面介绍的这些只是一些铺垫,有了上面的基础我们下面就可以更加容易的理解原型模式了。
举一个生活中的例子,当工厂想要生产手机(创建对象)的时候,我们需要去搭建一个生产的流水线(去实现Cloneable接口,重写clone方法),当我们想要生产手机的时候就可以启动机器(调用clone方法),流水线就会直接帮我们生产一个手机(创建好对象返回)。如图:
首先我们要创建一个模板,规定手机都有哪些属性与功能。
/**
* @description: 手机模板
* @author: Me
* @createDate: 2022/10/26 22:00
* @version: 1.0
*/
@Data
public abstract class Phone implements Cloneable {
// 手机型号
private String model;
// 手机类型
protected String type;
// 开机方法
abstract void startUp();
// 制造手机的方法
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
接下来我们要创建手机的实现,明确各个不同类型的手机的不同逻辑。
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 22:00
* @version: 1.0
*/
public class HuaWei extends Phone {
// 当通过构造方法创建对象时,直接将type属性进行赋值
public HuaWei(){
type = "HuaWei";
}
// 开机方法
@Override
void startUp() {
System.out.println("Welcome use HuaWei");
}
}
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 22:00
* @version: 1.0
*/
public class XiaoMi extends Phone {
// 当通过构造方法创建对象时,直接将type属性进行赋值
public XiaoMi(){
type = "XiaoMi";
}
// 开机方法
@Override
void startUp() {
System.out.println("Welcome use XiaoMi");
}
}
/**
* @description:
* @author: Me
* @createDate: 2022/10/26 22:00
* @version: 1.0
*/
public class IPhone extends Phone {
// 当通过构造方法创建对象时,直接将type属性进行赋值
public IPhone(){
type = "IPhone";
}
// 开机方法
@Override
void startUp() {
System.out.println("Welcome use IPhone");
}
}
实现类创建之后我们要进行原型的创建,并将原型进行保存,之后流水线就会根据原型来进行新手机的生产。
下一步就是流水线。
/**
* @description: 手机流水线
* @author: Me
* @createDate: 2022/10/26 22:01
* @version: 1.0
*/
public class PhoneMake {
// 手机设计模板(原型的缓存),因为可能多线程共享,所以使用线程安全的集合进行缓存
private static final ConcurrentHashMap<String, Phone> phoneMap = new ConcurrentHashMap<>();
// 从缓存列表中根据名字获取原型对象,相当于通过流水线去创建手机
public static Phone makePhone(String phoneModel) {
Phone cachedPhone = phoneMap.get(phoneModel);
return (Phone) cachedPhone.clone();
}
// 项目启动时我们就将原型对象放到缓存集合中
public static void loadCache() {
IPhone iPhone = new IPhone();
// 创建苹果13的对象放入缓存集合
iPhone.setModel("iphone 13");
phoneMap.put(iPhone.getModel(), iPhone);
// 创建华为mate50的对象放入缓存集合
HuaWei huaWei = new HuaWei();
huaWei.setModel("huawei mate 50");
phoneMap.put(huaWei.getModel(), huaWei);
// 创建小米12的对象放入缓存集合
XiaoMi xiaoMi = new XiaoMi();
xiaoMi.setModel("xiaomi 12");
phoneMap.put(xiaoMi.getModel(), xiaoMi);
}
}
这里整个原型模式的代码就都写好了,当我们需要一个新手机进行使用时,我们就可以通过如下方法进行生产。
// 加载流水线可生产的手机类型
PhoneMake.loadCache();
// 生产一个新手机
Phone phone = PhoneMake.makePhone("iphone 13");
// 手机开机
phone.startUp();
到这里原型模式中涉及到的内容就基本上都介绍完了,大家应该也熟悉这种模式了。接下来还是说一下这个模式的优缺点吧。
优点:逃避构造方法的束缚,某些情况下构造方法中会有耗时操作的时候使用原型模式去创建对象会提高性能。
缺点:当对一个业务逻辑复杂的核心类进行改造的时候很困难。
注意:由于是浅拷贝,需要考虑引用类型的属性是否需要二次克隆,因为如果不克隆的话两个对象可能就会共用一个属性,当一个对象更改了该属性,另一个对象的此属性也会随之改变带来风险。
这种模式我认为在实战中不太常用,对我来说更多是用来理解一些平时使用的框架的。例如Spring中的原型模式。
大家都知道IOC容器中创建的对象默认都是单例的,但是Spring中也提供了原型模式为基础去创建Bean对象的能力,这里我们就也简单学习一下。
这里我们先介绍一下Spring中Bean的作用域,这也是个比较常问的面试题。
Bean的作用域一共有5种。
singleton作用域(默认):此时的Bean是单例的,表示当项目启动后,所有调用者都会调用同一个Bean对象,我们可以用单例模式来理解这个作用域。不了解的同学可以看一下我单例模式的文章哈。
prototype作用域:此时的Bean是多例的,在SpringBoot中可以通过如下注解进行开启。
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
request作用域:每一次Http请求都会去创建Bean,仅适用于WebApplicationContext环境。
session作用域:每创建一个会话就会有一个Bean,仅适用于WebApplicationContext环境。
global-session作用域:WebApplicationContext环境中唯一的Bean,同样仅适用于WebApplicationContext环境。
这里的WebApplicationContext环境表示在Web应用中使用的。这三个作用域用的比较少,没有详细了解,有需要的小伙伴可以自行了解一下。
Spring中创建Bean对象的流程概括
1. Spring所管理的Bean实际上是缓存在一个ConcurrentHashMap中的(singletonObjects对象中)。
2. 该对象本质上是一个key-value对的形式,key指的是beanName,value是一个Object对象,就是所创建的bean对象。
3. 在创建Bean之前,首先需要将该Bean的创建标识制定好,表示该Bean已经或是即将被创建,目的是增强缓存的效率。
4. 根据bean的scope属性来确定当前这个bean是singleton还是prototype的bean,然后创建相应的对象。
5. 无论是singleton还是prototype的bean,其创建的过程是相同的。
6. 通过Java反射机制来创建Bean的实例,在创建之前需要检查构造方法的访问修饰符,如果不是public的,则会调用setAccessible(true) 方法来突破Java的语法限制,使得可以通过非public构造方法来完成对象实例的创建。
7. 当对象创建完毕后,开始进行对象属性的注入。
8. 在对象属性注入的过程中,Spring除了去使用之前通过BeanDefinition对象获取的Bean信息外,还会通过反射的方式获取到上面所创建的Bean中的真实属性信息(还包括一个class属性,表示该Bean所对应的class类型)。
9. 完成Bean属性的注入(或者抛出异常)
10. 如果Bean是一个单例的,那么将所创建出来的Bean添加到singletonObjects对象中(缓存中),供程序后续再次使用。如果Bean是多例的则不会放入缓存中,每次调用getBean()方法时都会重新获取。
在多例对象的创建中,我们可以根据proxyMode来进行进一步对我们创建的多例对象进行限制。
共有四种作用域,不太常用,我这边就简单介绍一下两种。
DEFAULT:默认类型,可以修改Bean中的属性。
TARGET_CLASS:此类型不会返回多例对象,而是返回代理的多例对象,不支持Bean中属性的修改。
我们在使用中会遇到一个普遍的问题:当一个单例对象去调用一个多例对象的时候并不会返回多个对象,还是返回一个对象,原因是因为单例对象在生成bean定义的时候就已经确定了依赖的对象,此时我们可以通过@Lookup注解去解决这种情况,解决方式如下(可以修改Bean中的属性):
// 当单例对象去调用多例对象的时候
// 创建一个返回值为多例对象类的方法
// 方法只可以是公共的或受保护的
// 可以是抽象方法,抽象方法则CGLIB会进行实现,如果是普通方法则会被覆盖(总而言之就是不需要方法体,方法体也是无效的)
// 参数列表需要为空,携带参数列表会报错
@Lookup
public MyService myService (){ return null; };
@GetMapping("/exam1")
public String irocess() {
// 在方法中通过调用Lookup注解声明的方法来获取bean
MyService myService = myService();
return "1OK";
}
原型模式就介绍到这里啦,希望对您有所帮助。