3.原型模式
一、引言
在 Java 编程中,原型模式(Prototype)是一种创建对象的方式,通过拷贝原型实例来创建新的对象,为对象的创建提供了一种高效且灵活的途径。本文将详细探讨原型模式的概念、包含的角色、浅克隆与深克隆的实现,以及克隆对单例模式的影响和相应的解决办法。
二、原型模式的定义
原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。调用者不需要知道任何创建细节,不调用构造函数。
三、原型模式的角色
- 抽象原型类:规定了具体原型对象必须实现的
clone()
方法。 - 具体原型类:实现抽象原型类的
clone()
方法,是可被复制的对象。 - 访问类:使用具体原型类中的
clone()
方法来复制新的对象。
四、简单的原型模式示例
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Cloneable {private String name;private String sex;private Integer age;/*** 重写 clone 方法,实现对象的复制* @return 复制后的对象* @throws CloneNotSupportedException 若不支持克隆则抛出此异常*/@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}/*** 主函数,用于测试原型模式的基本用法* @param args 命令行参数* @throws Exception 可能抛出的异常*/public static void main(String[] args) throws Exception{Student stu1 = new Student("小明", "男", 18);// 通过克隆创建 stu2 对象Student stu2 = (Student)stu1.clone();stu2.setName("小红");// 输出 stu1 的信息System.out.println(stu1); // Student(name=小明, sex=男, age=18)// 输出 stu2 的信息System.out.println(stu2); // Student(name=小红, sex=男, age=18)}
}
可以看到,把一个学生复制过来,只是改了姓名而已,其他属性完全一样没有改变。需要注意的是,一定要在被拷贝的对象上实现 Cloneable
接口,否则会抛出 CloneNotSupportedException
异常。
四、浅克隆
浅克隆的定义:浅克隆创建一个新对象,新对象的属性和原来对象完全相同,但对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
@Data
public class Clazz implements Cloneable {private String name;private Student student;/*** 重写 clone 方法,实现浅克隆* @return 浅克隆后的对象* @throws CloneNotSupportedException 若不支持克隆则抛出此异常*/@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {private String name;private String sex;private Integer age;
}/*** 主函数,用于测试浅克隆* @param args 命令行参数* @throws Exception 可能抛出的异常*/
public static void main(String[] args) throws Exception{Clazz clazz1 = new Clazz();clazz1.setName("大一");Student stu1 = new Student("小明", "男", 18);clazz1.setStudent(stu1);// 输出 clazz1 的信息System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小明, sex=男, age=18))Clazz clazz2 = (Clazz)clazz1.clone();Student stu2 = clazz2.getStudent();stu2.setName("小红");// 输出 clazz1 的信息System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小红, sex=男, age=18))// 输出 clazz2 的信息System.out.println(clazz2); // Clazz(name=大一, student=Student(name=小红, sex=男, age=18))
}
可以看到,当修改了 stu2
的姓名时,stu1
的姓名同样也被修改了,这说明 stu1
和 stu2
是同一个对象,这就是浅克隆的特点,对具体原型类中的引用类型的属性进行引用的复制。同时,这也可能是浅克隆所带来的弊端,因为结合该例子的原意,显然是想在班级中新增一名叫小红的学生,而非让所有的学生都改名叫小红,于是我们这里就要使用深克隆。
五、深克隆
深克隆的定义:深克隆创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
@Data
public class Clazz implements Cloneable, Serializable {private String name;private Student student;/*** 重写 clone 方法,实现浅克隆* @return 浅克隆后的对象* @throws CloneNotSupportedException 若不支持克隆则抛出此异常*/@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}/*** 实现深克隆的方法* @return 深克隆后的对象* @throws IOException 输入输出异常* @throws ClassNotFoundException 类未找到异常*/protected Object deepClone() throws IOException, ClassNotFoundException {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(this);ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);return ois.readObject();}
}/*** 主函数,用于测试深克隆* @param args 命令行参数* @throws Exception 可能抛出的异常*/
public static void main(String[] args) throws Exception{Clazz clazz1 = new Clazz();clazz1.setName("大一");Student stu1 = new Student("小明", "男", 18);clazz1.setStudent(stu1);Clazz clazz3 = (Clazz)clazz1.deepClone();Student stu3 = clazz3.getStudent();stu3.setName("王五");// 输出 clazz1 的信息System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小明, sex=男, age=18))// 输出 clazz3 的信息System.out.println(clazz3); // Clazz(name=大一, student=Student(name=王五, sex=男, age=18))
}
可以看到,当修改了 stu3
的姓名时,stu1
的姓名并没有被修改了,这说明 stu3
和 stu1
已经是不同的对象了,说明 Clazz
中的 Student
也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz
类和 Student
类都需要实现 Serializable
接口,否则会抛出 NotSerializableException
异常。
六、克隆破坏单例与解决办法
// Clazz 类
@Data
public class Clazz implements Cloneable, Serializable {private static Clazz clazz = new Clazz();private String name;private Student student;/*** 私有构造函数,防止外部创建实例*/private Clazz(){}/*** 获取单例实例的方法* @return 单例实例*/public static Clazz getInstance() {return clazz;}/*** 重写 clone 方法,解决克隆破坏单例的问题* @return 单例实例* @throws CloneNotSupportedException 若不支持克隆则抛出此异常*/@Overrideprotected Object clone() throws CloneNotSupportedException {return clazz;}
}/*** 测试克隆是否破坏单例* @param args 命令行参数* @throws Exception 可能抛出的异常*/
public static void main(String[] args) throws Exception{Clazz clazz1 = Clazz.getInstance();Clazz clazz2 = (Clazz)clazz1.clone();// 输出比较结果System.out.println(clazz1 == clazz2); // false// 重写 clone 方法后的测试Clazz clazz3 = Clazz.getInstance();Clazz clazz4 = (Clazz)clazz3.clone();System.out.println(clazz3 == clazz4); // true
}
可以看到 clazz1
和 clazz2
并不相等,也就是说他们并不是同一个对象,单例被破坏了。
解决办法:
- 不实现
Cloneable
接口即可,但不实现Cloneable
接口进行clone
则会抛出CloneNotSupportedException
异常。 - 重写
clone()
方法,返回单例对象。
另外我们知道,单例就是只有一个实例对象,如果重写了 clone()
方法保证单例的话,那么通过克隆出来的对象则不可以重新修改里面的属性,因为修改以后就会连同克隆对象一起被修改,所以是需要单例还是克隆,在实际应用中需要好好衡量。
七、总结
- 适用场景:
- 类初始化消耗资源较多。
new
产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。- 构造函数比较复杂。
- 循环体中生产大量对象时。
- 优点:
- 性能优良,Java 自带的原型模式是基于内存二进制流的拷贝,比直接
new
一个对象性能上提升了许多。 - 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。
- 性能优良,Java 自带的原型模式是基于内存二进制流的拷贝,比直接
- 缺点:
- 必须配备克隆(或者可拷贝)方法。
- 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 深克隆、浅克隆需要运用得当。