跳到主要内容

原型模式(Prototype Pattern)

引言

在Java中如果我们想要拷贝一个对象应该怎么做?第一种方法是使用 gettersetter方法一个字段一个字段设置。或者使用 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

第二种方式

只需要类实现 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);
}
}
java

以上方法的问题

以上方法的问题在于,如果有对象类型的数据。会直接引用对象地址,对象的内容修改后会同时影响拷贝对象和被拷贝对象,如下:

@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);
}
}
java

输出

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 对象的内容,两者都会改变。这是因为p1p2都指向了同一个 address 对象。

原型模式对象引用问题
原型模式对象引用问题

String 类型和 Integer 类型也是对象类型,为什么给name重新赋值时,p1p2不会相互影响呢?下面我们来解答这个问题。

这个问题其实很好回答,p1p2name字段在拷贝完成后其实指向的是同一个对象。从断点就可以看出。

原型模式对象引用问题调试
原型模式对象引用问题调试

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

原型模式对象引用问题调试
原型模式对象引用问题调试

希望不要在这个地方有疑惑。

我们回到正题,现在我们想让两个拷贝的对象,拷贝完成后就不再相互影响,怎么办?

那就是用序列化和反序列化的方式来实现对象的深拷贝。

深拷贝

深拷贝就是将对象序列化,然后再反序列化。这样新创建的对象跟原对象没有任何关系。任何字段都不会同时指向同一个对象。序列化方式主要有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);
}
}
java

输出结果:

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是对象的模版,当需要创建对象是,通过这个模板来创建一个新的。这与本篇文章中的原型模式有些区别。

总结

  • 原型模式就是通过一个以存在的对象来创建另一个。
  • 如果对象中存在有字段是对象类型,当这个字段被修改后,会同时影响拷贝和被拷贝对象。这时需要用深拷贝来处理。