1. 原型模式的定义与特点
- 定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。它属于
创建型
设计模式,用于创建重复的对象,同时又能保证性能(用这种方式创建对象非常高效)。 - 特点:这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
2. 原型模式的优点与缺点
-
优点:
- 性能优良:原型模式是在内存二进制流的拷贝,要比new一个对象性能好很多。特别是在一个循环体类产生大量对象时,不仅可以简化对象的创建过程,同时也能够提高效率。
- 逃避构造函数的约束:这是优缺点共存的一点,直接在内存中拷贝,构造函数是不会执行的。
- 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码。
-
缺点:
- 在实现深克隆的时候可能需要比较复杂的代码。
- 需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了OCP 原则。
3. 原型模式的使用场景
- 资源初始化场景:当一个对象的构建代价过高时。例如某个对象里面的数据需要访问数据库才能拿到,而我们却要多次构建这样的对象。
- 性能和安全要求的场景:通过new产生一个对象需要非常繁琐的数据准备和访问权限的时候。
- 一个对象多个修改者的场景:一个对象需要提供给其他对象访问,而各个调用者可能都需要修改其值时考虑使用。
4. 原型模式的结构与实现
-
由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单,只需要实现Cloneable接口并重写clone()方法,简单程度仅次于单例模式和迭代器模式。
-
原型模式包含以下主要角色:
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
-
其结构图如图所示:
5. 浅拷贝和深拷贝
5.1 浅拷贝
-
定义:就是有一个类的属性是引用类型,比如A 类,A创建一个对象A ,引用属性是B,在克隆A 的时候,B 是克隆一个内存地址,而不是将对应的内存里面的东西克隆一份,之后就是浅拷贝。
-
对于数据类型是基本数据类型、和String类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
-
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是将该成员变量的引用值(内存地址)复制一份给新的对象。
-
因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
-
浅拷贝是使用默认的 clone 方法来实现。
-
具体案例:
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class Teacher implements Cloneable { private String name; private int age; private List<String> list; @Override public Object clone() { Teacher teacher = null; try { teacher = (Teacher) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return teacher; } public void addListValue(String s) { list.add(s); } }
public class TeacherTest { public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("卢本伟"); Teacher teacher1 = new Teacher("老王",20,arrayList); Teacher teacher2 = (Teacher)teacher1.clone(); System.out.println("teacher1:" + teacher1); //teacher1:Teacher(name=老王, age=20, list=[卢本伟]) System.out.println("teacher2:" + teacher2); //teacher2:Teacher(name=老王, age=20, list=[卢本伟]) System.out.println(teacher1.hashCode()); //284720968 System.out.println(teacher2.hashCode()); //189568618 System.out.println("teacher1 == teacher2?" + (teacher1 == teacher2)); //teacher == teacher1?false teacher1.setAge(888); teacher1.setName("老六"); teacher1.addListValue("马飞飞"); System.out.println("teacher1:" + teacher1); //teacher1:Teacher(name=老六, age=888, list=[卢本伟, 马飞飞]) System.out.println("teacher2:" + teacher2); //teacher2:Teacher(name=老王, age=20, list=[卢本伟, 马飞飞]) } }
如上述代码所述,当对原型的基本数据类型和String进行修改时,并不会影响新对象,因为浅拷贝直接将属性复制一份新的给新对象。但是当对原型的引用数据进行修改时,会影响新对象,因为此时的浅拷贝是将原型的引用数据的内存地址拷贝一份给新对象。
5.2 深拷贝
-
深拷贝相较于浅拷贝需要解决的问题:通过重写clone方法对原型对象进行拷贝时,引用类型的成员变量的拷贝并不像基本数据类型的拷贝形式:直接复制一份给新对象,而是将引用类型的地址复制一份传递给新对象。
-
解决方案:对于原型对象的引用类型成员变量(下文简称A),单独重写A的clone方法,通过clone方法可以获得A的一个实例,再把这个实例赋值给新对象,那么新对象的A就与原型对象的A不再是同一个对象。
-
具体案例:
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class People implements Cloneable{ private String name; private Address address; @Override public Object clone() throws CloneNotSupportedException { People people = (People)super.clone(); people.address = (Address) address.clone(); return people; } public void updateAddress(String province,String city,String county){ address.setProvince(province); address.setCity(city); address.setCounty(county); } }
@Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class Address implements Cloneable{ private String province; private String city; private String county; @Override public Object clone() throws CloneNotSupportedException { return (Address) super.clone(); } }
public class PeopleTest { public static void main(String[] args) throws CloneNotSupportedException { Address address = new Address("福建省", "福州市", "闽侯县"); People people1 = new People("张三", address); People people2 = (People) people1.clone(); System.out.println("people1:" + people1); //people1:People(name=张三, address=Address(province=福建省, city=福州市, county=闽侯县)) System.out.println("people2:" + people2); //people2:People(name=张三, address=Address(province=福建省, city=福州市, county=闽侯县)) System.out.println(people1.hashCode()); //495053715` System.out.println(people2.hashCode()); //1922154895 System.out.println("people1 == people2?" + (people1 == people2)); //people1 == people2?false people1.setName("李四"); people1.updateAddress("广东省", "广州市", "猎德村"); System.out.println("people1:" + people1); //people1:People(name=李四, address=Address(province=广东省, city=广州市, county=猎德村)) System.out.println("people2:" + people2); //people2:People(name=张三, address=Address(province=福建省, city=福州市, county=闽侯县)) } }
6. 注意事项
- 构造方法在clone的时候并不会执行,因为对象是从内存以二进制流的方式进行拷贝,当然不会执行。
- 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。因此:要使用clone()方法,类的成员变量上不要增加final关键字。
- 深拷贝和浅拷贝要分开实现,不然会导致程序变得非常复杂。
- 带有final类型的变量是不可以进行拷贝的,这样是无法实现深拷贝。
参考博客: