1、简介
在软件工程中,原型(Prototype)模式是一种创建型模式,它允许用户复制一个已经存在的对象,而不必通过构造函数创建它们。它可以在运行时创建对象的副本,这样就不必在每次需要对象时都使用构造函数。
2、组成部分
原型模式可以分为三个部分:
①Prototype: 声明一个克隆自身的接口。
②ConcretePrototype: 实现接口 (—>具体的"原型":如何克隆)
③Client: 让一个原型克隆自身从而创建一个新对象。
原型模式是一种浅复制,也就是说如果原始对象中存在引用类型的属性,那么克隆出来的对象的引用类型属性也会指向原始对象的引用类型属性所指向的对象。如果需要深复制,需要对引用类型属性进行复制。
3、浅复制
浅复制是指在复制一个对象时,只复制它的基本类型属性值,而不复制它的引用类型属性。浅复制得到的新对象与原始对象的引用类型属性指向同一个对象。
例如:在Java中, 使用Object的clone()方法来复制对象, 如果这个对象中有其他对象类型的属性, 使用clone() 得到的副本会和原始对象共用这些对象类型的属性。
浅复制常常被用来复制基本类型和不可变类型的对象,因为它能够满足要求,而且比较简单,性能也比较高。
需要注意的是, 浅复制得到的新对象与原始对象的引用类型属性指向同一个对象, 如果新对象对这些对象类型的属性进行修改,也会影响到原始对象, 所以浅复制不能保证对象的独立性.
4、深复制
深复制是指在复制一个对象时,不仅复制它的基本类型属性值,还复制它的引用类型属性。深复制得到的新对象与原始对象的引用类型属性指向不同的对象
例如:在Java中, 如果需要对一个对象进行深复制, 需要对其中的引用类型的属性也进行复制,递归的方式来实现。
深复制能够保证复制出来的对象与原始对象的独立性,在修改新对象中的引用类型属性时不会影响到原始对象。
深复制通常用来复制可变类型的对象,因为它能够保证对象的独立性。但是深复制的代价也比较高,需要编写大量的代码来实现,性能也比较低。
4.1、几种实现方法
深复制是指复制对象及其内部对象。
Java中可以使用以下几种方式实现深复制:
①序列化和反序列化:可以使用Java的序列化和反序列化机制,将对象写入流中,再从流中读取出来
②重写clone()方法:可以重写clone()方法,实现对象的深复制。
③使用Apache的Commons BeanUtils工具类:使用BeanUtils.cloneBean()方法实现对象的深复制。
5、内存空间
当使用原型模式克隆对象时,JVM虚拟机会在内存中为新对象分配一块新的内存空间。这块内存空间中会保存新对象的所有属性的值。
如果使用浅复制,那么新对象中的引用类型属性指向的对象与原始对象中的引用类型属性指向的对象是同一个对象,在内存中只有一块空间,新对象中的引用类型属性与原始对象中的引用类型属性指向同一个对象。
如果使用深复制,那么新对象中的引用类型属性指向的对象与原始对象中的引用类型属性指向的对象是不同的对象,在内存中会有两块空间,新对象中的引用类型属性与原始对象中的引用类型属性指向不同的对象。
⭐使用原型模式克隆对象时, 一般都需要重写Object的clone()方法, 使用重写的clone()方法可以更好的控制对象的克隆过程, 也可以在克隆过程中设置一些限制条件.
总结来说,使用原型模式克隆对象时,JVM虚拟机会在内存中为新对象分配一块新的内存空间,这块内存空间中会保存新对象的所有属性的值。对于浅复制和深复制的区别就在于,浅复制只复制对象的基本类型属性值,而深复制需要复制对象的所有属性值,并为新对象的引用类型属性分配新的内存空间。
6、Object clone()
Java的Object类中有一个clone()方法,它可以用来创建一个对象的副本。这个方法是一个受保护的方法,默认实现是浅复制。
clone()方法的原型如下:
这个方法有一个受保护的访问权限,所以如果要使用它,需要在类中重写这个方法。
如果一个类实现了Cloneable接口,那么这个类就可以使用Object类中的clone()方法。Cloneable接口是一个空接口,它只是告诉JVM这个类可以被克隆。
在重写clone()方法时,可调用super.clone()来实现浅复制,也可以自己实现对属性的复制来实现深复制。
⭐如果在clone()方法中抛出CloneNotSupportedException异常,表示这个类不支持克隆操作,需要在类中重写clone()方法来解决。
实现clone()的代码示例如下:
上面的代码表示我们使用默认的浅复制.
如果要使用深复制, 就需要自己实现对属性的复制,如:
如果类中有一些不能被复制的属性, 那么在重写clone()方法的时候要考虑这些属性的复制方式.
总之, 使用Object类中的clone()方法可以创建一个对象的副本,但是默认实现只是浅复制, 若要进行深复制需要自己实现对属性的复制. 在使用clone()方法之前, 还要确保类实现了Cloneable接口.
7、代码实现
Java 代码实现可以如下:
1. class Student implements Cloneable { 2. private String name; 3. private int age; 4. private List<String> hobbies; 5. 6. //深复制 7. public Student clone() throws CloneNotSupportedException { 8. Student student = (Student) super.clone(); 9. student.hobbies = new ArrayList<>(this.hobbies); 10. return student; 11. } 12. //浅复制 13. public Student clone() throws CloneNotSupportedException { 14. Student student = (Student) super.clone(); 15. return student; 16. } 17. 18. 19. // setter and getter
代码中,深复制和浅复制的区别在于:有没有拷贝引用数据类型List<String>。
深复制中,重新new了一个ArrayList对象来复制原来的List对象,实现对象相互独立。
在Client中的使用如下:
1. Student student = new Student(); 2. student.setName("John Doe"); 3. student.setAge(20); 4. student.setHobbies(new ArrayList<>(Arrays.asList("reading", "running"))); 5. // student.setHobbies(Arrays.asList("reading", "running")); 6. Student newStudent = student.clone(); 7. // 修改原型对象 8. student.setAge(21); 9. // System.out.println(student.getHobbies()); 10. student.getHobbies().add("swimming"); 11. 12. // 实现深复制后的新对象不受影响 13. System.out.println(newStudent.getAge()); // 20 14. System.out.println(newStudent.getHobbies()); // [reading, running]
在上面的示例代码中,首先创建了一个student对象,并设置了其属性。然后使用clone()方法创建了一个新的对象newStudent。接着修改了student对象的属性,发现newStudent对象的属性并没有受到影响,证明实现了深拷贝。 需要注意的是,如果原型对象中存在引用其他对象,则需要对这些对象也进行复制。
8、UnsupportedOperationException
在上述的代码中有一个比较隐蔽的问题:UnsupportedOperationException(不支持的操作异常)
如上所示的两种方式:
①Arrays.asList("reading", "running")
②new ArrayList<>(Arrays.asList("reading", "running"))
若采用方式①,在使用add()或remove()方法时,会报UnsupportedOperationException异常!
采用方式②,把得到的对象的内容重新赋值给新的ArrayList对象,即可解决问题
8.1、Arrays.asList
Arrays.asList源码如下:
出现异常的原因,则是此处的ArrayList并非java.util.ArrayList,而是Arrays的一个内部类
(在Arrays类中并未找到相关的导包操作)
8.2、Arrays.ArrayList
Arrays.ArrayList源码如下:
Objects.requireNonNull():
由"①Arrays.asList("reading", "running")"知,源码中的array即为("reading", "running")。
则Arrays.ArrayList()中的a,即为array,a的类型为List。
综上,当client调用add()时,实际上调用的并不是List的add(),而是a.add(),而Arrays.ArrayList内部类中并没有编写add()方法!
8.3、AbstractList<E>
AbstractList<E>源码:
在AbstractList<E>中找到了add方法:
AbstractList中的add、set、remove方法都默认throw new UnsupportedOperationException()
9、使用场景
原型模式在下面几种情况下通常会被使用:
①在创建对象的过程中耗时较长或资源消耗较大。
特别是在对象的创建过程涉及复杂的构造函数或者创建过程涉及很多的资源消耗的时候,通过克隆 已有对象来代替重新创建对象可以显著降低系统资源消耗。
②创建新对象的类型已经确定,但是类的实例化过程不确定。
③创建新对象的类型已经确定,且需要大量的类似的对象。
另外需要注意线程安全问题,如果原型对象被多个线程同时使用,需要考虑同步机制。
10、优缺点
原型模式具有下面几个优点:
①通过克隆对象来创建新对象,避免了对象的重新创建过程,能够大幅度提高程序的性能。
②通过原型模式可以更加灵活地控制对象的创建过程
③在深复制时,能够对对象的引用类型属性进行复制,进而保证克隆出来的对象与原始对象的独立性
另外,原型模式也有一些缺点需要注意:
①需要为每一个类型编写一个克隆方法,这对全局所有类都是需要的,这将增加系统中类的个数。
②需要对每一个类都提供一个构造函数,这将增加类的复杂度。
③在进行深复制时,需要编写代码来复制引用类型的属性,这可能需要大量的时间和空间。
11、总结
原型模式在选择时需要权衡其优缺点,并适当地使用。
它是一个比较简单的模式,但是需要注意的问题还是有一些的。
最后, 如果项目中需要大量的创建对象且对象的创建过程较为复杂,可以考虑使用原型模式来优化性能。