定义
使用原型实例指定待创建的类型,并且通过克隆这个原型实例来创建新的对象。需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通过不同的方式修改可以得到一系列相似但不完全相同的对象。
类型
创建型
角色
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
原型模式的核心在于如何实现克隆方法,而Java语言提供了clone()方法
所有的Java类都继承自 java.lang.Object。Object 类提供一个 clone() 方法,在Java中可以直接使用 Object 提供的 clone() 方法来实现对象的克隆,这样Java语言中的原型模式实现就会很简单。
示例:
抽象原型类:java.object 有一个native的clone() 方法
public class Object {
//创建并返回此对象的副本。 “复制”的确切含义可能取决于对象的类别。 一般意图是,对于任何对象x ,表达式:
//x.clone() != x
//将是真的,并且表达式:
// x.clone().getClass() == x.getClass()
//会是true ,但这些不是绝对的要求。 虽然通常的情况是:
// x.clone().equals(x)
//会是true ,这不是绝对的要求。
//按照惯例,返回的对象应该通过调用super.clone获得。 如果一个类和它的所有超类(除了Object )都遵守这个约定,那么x.clone().getClass() == x.getClass() 。
//按照惯例,这个方法返回的对象应该独立于这个对象(它被克隆)。 为了实现这种独立性,可能需要在返回之前修改super.clone返回的对象的一个或多个字段。 通常,这意味着复制包含被克隆对象的内部“深层结构”的任何可变对象,并将对这些对象的引用替换为对副本的引用。 如果一个类只包含原始字段或对不可变对象的引用,那么通常情况下, super.clone返回的对象中没有字段需要修改。
//类Object的方法clone执行特定的克隆操作。 首先,如果该对象的类没有实现接口Cloneable ,则抛出CloneNotSupportedException 。 请注意,所有数组都被认为实现了接口Cloneable并且数组类型T[]的clone方法的返回类型是T[] ,其中 T 是任何引用或原始类型。 否则,此方法将创建此对象的类的新实例,并使用此对象的相应字段的内容来初始化其所有字段,就像通过赋值一样; 字段的内容本身不会被克隆。 因此,此方法执行此对象的“浅拷贝”,而不是“深拷贝”操作。
//类Object本身并不实现接口Cloneable ,因此在类为Object的对象上调用clone方法将导致在运行时抛出异常
//返回:此实例的克隆。
//抛出:CloneNotSupportedException – 如果对象的类不支持Cloneable接口。 覆盖clone方法的子类也可以抛出此异常以指示无法克隆实例。
//也可以看看:Cloneable
protected native Object clone() throws CloneNotSupportedException;
}
具体原型类Prototype
需要注意的是能够实现克隆的Java类必须实现一个标识接口 Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个 CloneNotSupportedException 异常。
Cloneable接口本身没有任何实现
public interface Cloneable {
}
public class Prototype implements Cloneable {
private String name;
private int age;
private String sex;
public Prototype(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Prototype{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}'+super.toString();
}
@Override
public Object clone() throws CloneNotSupportedException {
Prototype proto = (Prototype) super.clone();
return proto;
}
}
Client(客户类)
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Prototype prototype = new Prototype("xt",23,"man");
System.out.println(prototype.toString());
Prototype prototype2 = (Prototype) prototype.clone();
System.out.println(prototype2.toString());
}
}
结果:
就像下面这幅图一样,在堆内存在有两个实例。
图片来自https://blog.csdn.net/one_Jachen/article/details/78246047
利用java的clone()方法可以很方便的实现简单的原型设计模式,但是拷贝又分深拷贝和浅拷贝
深拷贝和浅拷贝
浅拷贝:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
深拷贝:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。(String,Integer等包装类没有这种浅克隆问题)
深拷贝实现方案:
一、为原型使用到的所有对象实现Cloneable接口并重写clone方法
最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
顶层的原型类
public class Department implements Cloneable{
private Integer departmentID;
private String departmentName;
private Person person;
public Department(int departmentID, String departmentName, Person person) {
this.departmentID = departmentID;
this.departmentName = departmentName;
this.person = person;
}
public int getDepartmentID() {
return departmentID;
}
public void setDepartmentID(int departmentID) {
this.departmentID = departmentID;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public String toString() {
return "Department{" +
"departmentID=" + departmentID +
", departmentName='" + departmentName + '\'' +
", person=" + person +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj=null;
//调用Object类的clone方法——浅拷贝
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//调用person类的clone方法进行深拷贝
//先将obj转化为Department类实例
Department department=(Department) obj;
//Department类实例的Person对象属性,调用其clone方法进行拷贝
department.person = (Person) department.getPerson().clone();
return department;
}
}
原型引用的类
public class Person implements Cloneable{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}'+super.toString();
}
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj=null;
try {
obj=super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
客户端Test
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person("xt",23);
Department department = new Department(1,"computer",person);
System.out.println(department.toString());
Department departmentClone = (Department) department.clone();
//因为是=号赋值,所以
departmentClone.setDepartmentName("medicine");
departmentClone.setDepartmentID(2);
departmentClone.getPerson().setName("Tom");
System.out.println(departmentClone.toString());
}
}
结果:
细心的同学会发现,person类是我们实现了Cloneable接口,重写了clone()方法,所以clone的时候新原型样本会生成新的person实例,如下图标号3,4所示。但是Integer类和String类是没有实现Cloneable接口的。在克隆之后,两个样本所指向的DepartmentID和DepartmentName实例地址都是一样的
//因为是=号赋值,所以
departmentClone.setDepartmentName("medicine");
departmentClone.setDepartmentID(2);
但是调用set方法修改DepartmentID和DepartmentName值的时候,却会生成下图标号5,6和7,8的情况。实例地址发生了变化。
是因为在set方法中会生成新的实例,然后将这个实例传给this.departmentName和this.departmentID。所以就避免了浅拷贝的问题
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public void setDepartmentID(int departmentID) {
this.departmentID = departmentID;
}
二、通过对象序列化实现深拷贝
虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。
将对象序列化为字节序列后,默认会将该对象的整个对象链进行序列化,再通过反序列即可完美地实现深拷贝。
顶层原型类
public class Department implements Serializable {
private int departmentID;
private String departmentName;
private Person person;
public Department(int departmentID, String departmentName, Person person) {
this.departmentID = departmentID;
this.departmentName = departmentName;
this.person = person;
}
public int getDepartmentID() {
return departmentID;
}
public void setDepartmentID(int departmentID) {
this.departmentID = departmentID;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public String toString() {
return "Department{" +
"departmentID=" + departmentID +
", departmentName='" + departmentName + '\'' +
", person=" + person +
'}'+super.toString();
}
}
原型类引用到的类
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}'+super.toString();
}
}
客户端Test
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("xt",23);
Department department = new Department(1,"computer",person);
System.out.println(department.toString());
//通过序列化方法实现深拷贝
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(department);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Department departmentClone=(Department)ois.readObject();
System.out.println(departmentClone.toString());
System.out.println();
//尝试修改departmentClone中的各属性,观察department的属性有没有变化
//改变Person这个引用类型的成员变量的值
departmentClone.getPerson().setName("Tom");
System.out.println(department.toString());
System.out.println(departmentClone.toString());
}
}
可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。
原型模式总结:
优点
- 原型模式提供了简化的创建结构,在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者
- 可以使用深克隆的方式保存对象的状态,将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销和回滚操作。
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
缺点
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
- 在做深克隆的时候,如果对象之间存在多重嵌套的引用时,为了实现克隆,对每一层对象对应的类都必须支持深克隆,实现起来比较麻烦。
适用场景
- 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
- 通过new产生的一个对象需要非常繁琐的数据准备或者权限,这时可以使用原型模式。
- 难以根据类生成实例时。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
应用:
实现了Cloneable()接口的类是很多的
ArrayList 的clone()
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
References:
- https://whirlys.blog.csdn.net/article/details/82710378
- https://blog.csdn.net/qq_37960603/article/details/104081344
- 《菜鸟教程》
- 《大话设计模式》
- https://blog.csdn.net/one_Jachen/article/details/78246047
- https://www.cnblogs.com/shakinghead/p/7651502.html