java中的clone方法
超类Object有一个protected的方法clone(),该方法返回Object对象的一个拷贝。我们知道所有的类都继承自Object,也就是所有的类都有clone方法,是不是直接调用clone()方法就可以对一个对象进行克隆呢?
然而并不是,在《java核心技术》里有这么一句话:”子类只能调用受保护的clone方法克隆它自己”,啥意思,这句话琢磨了半天,clone方法是protected的没问题啊,protected的访问范围包括同一个包的其他类和该类的子类,所有的类都是Object类的子类,所以所有的子类可以正常访问clone方法,没什么问题啊,我们写一个具体的例子看一看:
1 | //定义一个Boy类 |
我们发现报了编译错误:
1 | Error:(12, 31) java: clone() 在 java.lang.Object 中是 protected 访问控制 |
似乎有点明白”子类只能调用受保护的clone方法克隆它自己”这句话了,要克隆Boy对象,那么只能在Boy类里面调用clone方法,在Main类里调用clone方法只能克隆Main对象。
那么对代码做一下修改,在Boy类里面调用clone方法:
1 | package com; |
我们觉得这样应该是可行的,但是抛了异常:
1 | Exception in thread "main" java.lang.CloneNotSupportedException: com.Boy |
CloneNotSuported,这个是由于java的克隆机制决定的,因为Boy类没有实现接口Cloneable,java规定要想对一个类对象进行克隆,这个类必须要实现Cloeable接口。关于这个约束,似乎没少被喷,引用自某乎”这是java的一个设计缺陷,是个很糟糕的设计,如果java在今天被重新设计的话,多半就不会设计成这个样子了”。
但是既然有这么个机制约束,我们还是得弄清楚,为什么会抛这个异常。java对象要支持clone功能,但是不是所有的类都可以克隆,所有java就想让用户自己来标记哪些类可以克隆,如果这个类需要克隆,就给这个类一个标记,但是并没有像final这些关键字一样提供一个关键字来修饰类的克隆属性,java是通过让类实现一个接口的方式进行标记该类是否可被克隆,也就是Cloneable接口,像Cloneable这种接口就是一种标记接口。顺便说两句标记接口:java中把没有定义任何方法和常量的接口称为标记接口,也就是说这种接口不像正常的接口那样是用来描述类的某种功能的,只是单纯的对类进行标记,常用的三个标记接口为RandomAccess、Cloneable、Serializable。
那么我们基本上就知道了java进行clone的一个过程:JVM首先检查要clone的对象的类有没有打上Cloneable的标记,如果有这个标记,就进行clone,如果没有这个标记则抛异常,所有上面抛出的异常也就不难理解了。
那么对Boy类实现Cloneable接口即可:
1 | package com; |
结果输出:
1 | kobe's name is: kobe |
我们发现,成功地对Boy对象进行了拷贝,并且改变bryant的成员,kobe的成员并没有受到影响(这就是跟引用的区别了,如果直接让bryant=kobe,看似是拷贝,但是改变bryant的成员,kobe的成员也会同步被修改)。
但是呢,这里我们调用的方法是copyOfBoy,名字太别扭了,既然是clone,那么如果调用kobe.clone()方法不是更合适么,把方法名copyOfBoy替换成clone即可,其实就是对父类的clone方法进行了重写,注意一下,copyOfBoy里调用的是this.clone(),如果将copyOfBoy替换成clone,this.clone()需要替换成super.clone(),表示调用的是父类的clone方法,不然就形成递归了,会导致栈溢出。
总结一下,对象克隆的步骤:
step1:implements Cloneable接口;
step2:重写clone方法为public。
浅拷贝和深拷贝
上面我们已经很清楚的实现了对象clone,改变bryant的name,kobe的成员没有受到影响,但是呢,如果Boy包含可变对象的成员变量呢,这个时候clone对象修改该可变成员,原对象的成员是否受到影响呢?我们看一看具体的代码:
1 | //定义一个coach类 |
结果输出:
1 | kobe's coach name is: Jakson |
我们发现改变bryant的coach的名字,kobe的coach的名字也被同步修改了,这说明clone方法默认是浅拷贝。
为了实现深拷贝,需要对每一个可变对象成员对象均进行clone:
1 | package com; |
结果输出:
1 | kobe's coach name is: Jakson |
我们发现改变bryant的coach成员,kobe的coach成员并没有受到影响,至此已经实现了深拷贝。但是假如一个对象有好多可变对象成员,要实现深拷贝,岂不是要对每一个成员均进行clone,不是很麻烦吗?是的,需要对每一个成员都要clone,java还有另外一种方式实现深拷贝,Serializable序列化,关于这个下回再说。