原型模式(Prototype Pattern)
引言
在Java中如果我们想要拷贝一个对象应该怎么做?第一种方法是使用 getter
和setter
方法一个字段一个字段设置。或者使用 BeanUtils.copyProperties()
方法。这种方式不仅能实现相同类型之间对象的拷贝,还可以实现不同类型之间的拷贝。
如果仅考虑相同对象之间的拷贝,有没有什么更优雅的方式呢?那就是原型模式。
定义及实现
定义
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
结构

原型模式就是类中提供一个拷贝方法,用于拷贝一个和自身属性一模一样的对象。
代码实现
第一种方式
public interface Prototype<T> {
T copy();
}
@NoArgsConstructor
@Data
public class ConcretePrototype1 implements Prototype<ConcretePrototype1> {
private String name;
private Integer age;
public ConcretePrototype1(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public ConcretePrototype1 copy() {
return new ConcretePrototype1(this.name, this.age);
}
}
public class Main {
public static void main(String[] args) {
ConcretePrototype1 p1 = new ConcretePrototype1();
p1.setAge(18);
p1.setName("prototype1");
System.out.println(p1);
ConcretePrototype1 p2 = p1.copy();
System.out.println(p2);
}
}
第二种方式
只需要类实现 java.lang.Cloneable
借口,并实现clone()
方法即可。
@NoArgsConstructor
@Data
public class ConcretePrototype1 implements Cloneable {
private String name;
private Integer age;
@Override
public ConcretePrototype1 clone() {
try {
return (ConcretePrototype1) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Main {
public static void main(String[] args) {
ConcretePrototype1 p1 = new ConcretePrototype1();
p1.setAge(18);
p1.setName("prototype1");
System.out.println(p1);
ConcretePrototype1 p2 = p1.clone();
System.out.println(p2);
}
}
以上方法的问题
以上方法的问题在于,如果有对象类型的数据。会直接引用对象地址,对象的内容修改后会同时影响拷贝对象和被拷贝对象,如下:
@NoArgsConstructor
@Data
public class Address {
private String province;
private String city;
private String street;
public Address(String province, String city, String street) {
this.province = province;
this.city = city;
this.street = street;
}
}
@NoArgsConstructor
@Data
public class ConcretePrototype1 implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
public ConcretePrototype1 clone() {
try {
return (ConcretePrototype1) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Main {
public static void main(String[] args) {
Address address = new Address("河南省", "郑州市", "高新区");
ConcretePrototype1 p1 = new ConcretePrototype1();
p1.setAge(18);
p1.setName("prototype1");
p1.setAddress(address);
System.out.println(p1);
ConcretePrototype1 p2 = p1.clone();
System.out.println(p2);
// 修改p1的地址信息
p1.getAddress().setStreet("中原区");
System.out.println(p1);
System.out.println(p2);
// 修改p2的地址信息
p2.getAddress().setStreet("二七区");
System.out.println(p1);
System.out.println(p2);
}
}
输出
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=二七区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=二七区))
从输出的结构中可以看出,无论是 p1
对象修改了 address
对象的内容,还是p2
对象修改了 address
对象的内容,两者都会改变。这是因为p1
和p2
都指向了同一个 address
对象。

String
类型和Integer
类型也是对象类型,为什么给name
重新赋值时,p1
和p2
不会相互影响呢?下面我们来解答这个问题。
这个问题其实很好回答,p1
和p2
的name
字段在拷贝完成后其实指向的是同一个对象。从断点就可以看出。

但是我们重新给p1
的 name
赋值时,相当于将p1
的 name
指向了另一个字符串对象。如下图

希望不要在这个地方有疑惑。
我们回到正题,现在我们想让两个拷贝的对象,拷贝完成后就不再相互影响,怎么办?
那就是用序列化和反序列化的方式来实现对象的深拷贝。
深拷贝
深拷贝就是将对象序列化,然后再反序列化。这样新创建的对象跟原对象没有任何关系。任何字段都不会同时指向同一个对象。序列化方式主要有JSON序列化、Java原生序列化方式,当然还有其他的序列化方式。这里只列举JSON序列化方式,其他序列化如果有兴趣可以自行实现。
一些第三方库如Apache Commons的SerializationUtils类或Google的Gson库都提供了实现深拷贝的方法。
JSON序列化方式
在本例中使用 fastjson2
进行序列化和反序列化。
@NoArgsConstructor
@Data
public class ConcretePrototype1 implements Prototype<ConcretePrototype1> {
private String name;
private Integer age;
private Address address;
@Override
public ConcretePrototype1 copy() {
String json = JSON.toJSONString(this);
return JSON.parseObject(json, ConcretePrototype1.class);
}
}
public class Main {
public static void main(String[] args) {
Address address = new Address("河南省", "郑州市", "高新区");
ConcretePrototype1 p1 = new ConcretePrototype1();
p1.setAge(18);
p1.setName("prototype1");
p1.setAddress(address);
System.out.println(p1);
ConcretePrototype1 p2 = p1.copy();
System.out.println(p2);
p1.getAddress().setStreet("中原区");
System.out.println(p1);
System.out.println(p2);
}
}
输出结果:
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
从输出结果中可以看出,两个对象是不会相互影响的。

从上图的调试结果中也可以看出,所有字段指向的内存地址都不一样。
实际应用
原型模式的一个典型应用就是Spring中Bean的作用域。Spring框架中的原型作用域(Prototype Scope)就是基于原型模式实现的。
在Spring框架中,当一个bean的作用域被定义为原型作用域时,Spring容器在接收到对该bean的请求时,会为每个请求创建一个新的实例。这就类似于原型模式中的克隆操作,每次都创建一个新的对象实例,而不是返回同一个实例。
但是在Spring中Bean的创建是通过BeanDefinition创建的。BeanDefinition是Spring框架中用于描述和定义Bean的元数据接口。它包含了Bean的类名、依赖、作用域、生命周期回调等信息,可以理解为Bean的配置信息。
当Spring容器启动时,它会解析配置文件或注解,将Bean定义解析为BeanDefinition,并将其注册到容器中。然后,Spring容器根据BeanDefinition中的信息来创建和管理Bean的实例。
也就是说BeanDefinition是对象的模版,当需要创建对象是,通过这个模板来创建一个新的。这与本篇文章中的原型模式有些区别。
总结
- 原型模式就是通过一个以存在的对象来创建另一个。
- 如果对象中存在有字段是对象类型,当这个字段被修改后,会同时影响拷贝和被拷贝对象。这时需要用深拷贝来处理。