前言
“万事万物皆对象!”,这是面向对象的宗旨,在Java中我们无时无刻不在创建对象,那创建对象有哪些方式呢?可以通过new或者反射,还有就是今天要讲的原型模式。那什么是原型模式,为什么又要通过原型模式去创建对象呢?
正文
无论是通过new还是反射,我们都免不了要去手动设置其属性,如果类结构非常复杂,同时又需要创建大量相似的对象时,如果还老老实的去new就非常麻烦易出错了,而原型模式则是使用clone方法直接拷贝一个对象,在Java中实现非常简单,只需要重写Object的clone方法就行了。
Coding
首先创建一个People类,每个人都有头和手:
public class People implements Cloneable {
public String name;
public Integer age;
public Head head;
public Arm[] arms;
public People() {
this.name = "Jack";
this.age = 10;
this.head = new Head();
Arm left = new Arm();
Arm right = new Arm();
arms = new Arm[2];
arms[0] = left;
arms[1] = right;
}
/**
* 重写clone方法,需要注意的是Object中
*/
@Override
public People clone() throws CloneNotSupportedException {
return (People) super.clone();
}
}
public class Head implements Cloneable {
double weight = 10;
@Override
public Head clone() throws CloneNotSupportedException {
return (Head) super.clone();
}
}
public class Arm implements Cloneable {
double length = 5;
@Override
public Arm clone() throws CloneNotSupportedException {
return (Arm) super.clone();
}
}
这样我们需要创建同样的对象时,只需要调用clone方法就行了,这个被拷贝的对象就被称为原型,不过这两个对象之间有什么异同呢?修改其中一个对象的属性对另外一个对象又有什么影响呢?思考一下下面这段代码的结果:
People people = new People();
People clone = people.clone();
System.out.println(people == clone);
System.out.println(people.name == clone.name);
System.out.println(people.age == clone.age);
System.out.println(people.head == clone.head);
System.out.println(people.arms == clone.arms);
你会发现除了第一个为false以外,其他的全是true,换言之,它们的属性所引用的对象为同一个对象,改变任何一个对象的属性都会影响到另外一个对象的属性,使用下面代码验证一下:
System.out.println("-------name-------");
people.name = "Harry";
System.out.println(people.name);
System.out.println(clone.name);
System.out.println("--------age------");
people.age = 555;
System.out.println(people.age);
System.out.println(clone.age);
System.out.println("-------head-------");
System.out.println("改变前people:" + people.head.weight);
System.out.println("改变前clone:" + clone.head.weight);
people.head.weight = 55;
System.out.println("改变后people:" + people.head.weight);
System.out.println("改变后clone:" + clone.head.weight);
System.out.println("-------arm-------");
// 先检验数组中对象
System.out.println("改变前people:" + people.arms[0]);
System.out.println("改变前clone:" + clone.arms[0]);
people.arms[0] = null;
System.out.println("改变后people:" + people.arms[0]);
System.out.println("改变后clone:" + clone.arms[0]);
打印结果:
-------name-------
Harry
Jack
--------age------
555
1111
-------head-------
改变前people:10.0
改变前clone:10.0
改变后people:55.0
改变后clone:55.0
-------arm-------
改变前people:cn.dark.Arm@1b6d3586
改变前clone:cn.dark.Arm@1b6d3586
改变后people:null
改变后clone:null
这是怎么回事?head和arm属性确实如我们所想并没有被完全拷贝,但name和age属性却是独立的。
出现这样的结果主要是因为Java中的clone方法是本地方法实现的一个浅拷贝,除基本类型以外的引用类型只会将引用进行拷贝(基本类型则是拷贝值),因此得到的是同一个对象。
等等,name(String)和age(Integer)不也是引用类型么?
你说的没错,但仔细思考一下不难理解,String和Integer都是final修饰的不可变对象,意思对其中任何一个对象修改该值,原先的引用值是不会改变的,只会将一个新的引用赋值给该属性,但另外一个对象的该属性仍然是引用的之前的对象,也就出现了这样的结果。
那该如何实现深拷贝呢?
很简单,只需要在clone方法中调用属性的clone方法或是重新new一个即可:
public People clone() throws CloneNotSupportedException {
People clone = (People) super.clone();
clone.head = head.clone();
clone.arms = arms.clone();
return clone;
}
也就是说属性对象也需要重写clone方法,到这我们不难看出原型模式一个致命的缺点:
如果拷贝的对象层次非常深,那么实现原型拷贝就会非常的复杂。
因此我们也需要根据具体情况考虑是否使用原型模式。
等等,还没有完,你应该注意到了上面的类都实现了一个Cloneable接口,这个接口是做什么的呢?
它是一个标记接口,表示这个类是可以被clone的,如果不实现该接口,那么在调用该类对象的clone方法时就会抛出CloneNotSupportedException异常,在使用时需要注意。
总结
原型模式非常的简单,但它有以下三点需要注意:
- 被克隆的类必须实现Cloneable接口;
- Java中的clone方法是浅拷贝,在需要完全复制一个全新对象时,需要自己实现。
- clone是本地方法实现,它直接操作内存当中的二进制流(性能也就远远超过new和反射),不会调用对象的构造方法,意味者若遇到需要控制类构造器的访问权限的情况时(如单例模式),就无法使用原型模式了,它会绕开访问权限控制。