引言
在Java中如果我们想要拷贝一个对象应该怎么做?第一种方法是使用 getter
和setter
方法一个字段一个字段设置。或者使用 BeanUtils.copyProperties()
方法。这种方式不仅能实现相同类型之间对象的拷贝,还可以实现不同类型之间的拷贝。
如果仅考虑相同对象之间的拷贝,有没有什么更优雅的方式呢?那就是原型模式。
定义及实现
定义
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
结构
原型模式就是类中提供一个拷贝方法,用于拷贝一个和自身属性一模一样的对象。
代码实现
第一种方式
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
| 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()
方法即可。
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
| @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); } }
|
以上方法的问题
以上方法的问题在于,如果有对象类型的数据。会直接引用对象地址,对象的内容修改后会同时影响拷贝对象和被拷贝对象,如下:
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
| @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.getAddress().setStreet("中原区");
System.out.println(p1); System.out.println(p2);
p2.getAddress().setStreet("二七区"); System.out.println(p1); System.out.println(p2); } }
|
输出
1 2 3 4 5 6
| 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
进行序列化和反序列化。
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
| @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); } }
|
输出结果:
1 2 3 4
| 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是对象的模版,当需要创建对象是,通过这个模板来创建一个新的。这与本篇文章中的原型模式有些区别。
总结
- 原型模式就是通过一个以存在的对象来创建另一个。
- 如果对象中存在有字段是对象类型,当这个字段被修改后,会同时影响拷贝和被拷贝对象。这时需要用深拷贝来处理。