什么是对象克隆(拷贝/复制)?浅克隆与深克隆有什么区别?
一、什么是对象克隆?
首先我们需要知道,什么是对象的克隆,或者说复制。一个业务逻辑,需要一个新的对象,但是类型和值都是之前的,也就是说,新状态和之前完全一样。使用new和赋值语句或者set注入都是可以的,但是,这会花费大量开销去做,效率低,并且还会产生冗余代码。
恰好java语言本身契合了原型设计模式
,给我们提供了一个clone
方法在Object
对象中,只要需要克隆的对象实现Cloneable
接口,那么我们只要简单的调用一下该方法就可以获得表面完全不同的对象。
例1:
public class User implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
......
User u = new User();
User uCopy = u.clone();
//显然u和uCopy已经是两个不同的对象了
System.out.println(u == uCopy);//false
二、如何实现对象克隆
上文也是提到,克隆对象基本只要两步:
- 实现
Cloneable
接口 - 实现
clone()
方法,并调用父类clone()
需要注意,Object
的clone()
方法是在java平台层实现的native
方法,具有开销小,速度快的特点。而且,原始的Object
方法是被protected
修饰的,在这里需要修改为public
,如果不这么做,浅克隆时没有问题,深克隆就会遇到权限不够的问题。java继承还有个原则,就是子类覆写父类方法,访问修饰符权限不能低于父类。
三、什么是浅克隆和深克隆
例2:
/**
* 浅克隆/浅拷贝
* @datetime 2018-10-09 09:43:22
* @author Belmode
*
*/
public class ShallowClone {
public static void main(String[] args) throws Exception {
Person p = new Person("李达康", "55", "汉东省京州市");
Person p2 = (Person) p.clone();
//一级对象
System.out.println(p == p2);//false
//二级对象
System.out.println(p.personalInfo == p2.personalInfo);//true
//三级对象
System.out.println(p.personalInfo.name == p2.personalInfo.name);//true
}
}
class PersonalInfo implements Cloneable{
String name;
String age;
String address;
public PersonalInfo(String name, String age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
PersonalInfo personalInfo;
public Person(String name, String age, String address) {
this.personalInfo = new PersonalInfo(name, age, address);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
在本例中,Person
类拥有PersonalInfo
类作为属性,两者都实现了Cloneable
接口,并且重写了clone()
方法。
在ShallowClone
类的main
方法中,构建了一个Person
对象p
,调用clone()
方法,产生p2
,p
和p2
不是一个对象,但是p
和p2
内部的personalInfo
属性是同一对象,这就会导致一个致命的问题,当我修改p.personalInfo
内部的属性值,结果p2.personalInfo
也被修改了。
p.personalInfo.name = "孙正义";
System.out.println(p2.personalInfo.name); //孙正义
-
这是什么原因导致的?
这是因为java中不仅仅有基本变量,还有引用变量
浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量
深克隆:既克隆基本类型变量,也克隆引用类型变量 -
其实本来原生的
clone()
方法也是,复制了引用的,但是为什么引用类型还是同一个呢?
就是因为java中,只有值传递,没有引用传递,引用在内存中还是值,是地址的hash值,一个特殊标记唯一堆内存块的值。
图1:
在JVM虚拟机中,事实上就是一个java.exe
或者javaw.exe
进程里, 会存在栈内存和堆内存,堆内存又可细分,这里就不多说了,想深入了解的可以问度娘。在方法区中存储着Person
和PersonalInfo
的类模板,通过new
派生出 p
和pesonalInfo
的实例,如图中箭头所示。此时p里引用着personalInfo实例(其实应该从p实例里的方框画箭头到栈位置的personalInfo
标签,这里为了下面说明,就没那么做了)
在栈内存中存储的都是hashcode
,类似于eb5238b
,存在p
的位置,就产生了图中的箭头
图2:调用clone()
方法,克隆出p2
如图所示,
p2
是真的新实例,但是personalInfo
也是复制的值,p2
里的personalInfo
属性保存personalInfo
实例的地址值,所以,如黄线所示,还是引用到原本的personalInfo实例。因此,修改
p.personalInfo
里的属性,p2.personalInfo
里的值也会收到影响!!
那么,如果我修改p.personalInfo
属性,p.personalInfo = new PersonalInfo.....
那p2.pernalInfo
会不会收到影响呢?
答案是不会的。
图3:
如图所示,会断开红叉处的连接,生成橙色连接。如橙线所示。
p
对象和p2
的personalInfo
属性其实已经不同了。
话题有点跑偏了,因为会出现上面的原因,所以需要深克隆。
例3:
public class DeepClone {
public static void main(String[] args) throws Exception {
Person2 person = new Person2("李达康", "55", "汉东省京州市");
Person2 person2 = (Person2) person.clone();
//一级对象
System.out.println(person == person2);//false
//二级对象
System.out.println(person.personalInfo == person2.personalInfo);//false
//三级对象
System.out.println(person.personalInfo.name == person2.personalInfo.name);//false
}
}
class PersonalInfo2 implements Cloneable{
String name;
String age;
String address;
public PersonalInfo2(String name, String age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
PersonalInfo2 pInfo = (PersonalInfo2) super.clone();
pInfo.name = new String(this.name);
pInfo.age = new String(this.age);
pInfo.address = new String(this.address);
return pInfo;
}
}
class Person2 implements Cloneable{
PersonalInfo2 personalInfo;
public Person2(String name, String age, String address) {
this.personalInfo = new PersonalInfo2(name, age, address);
}
@Override
public Object clone() throws CloneNotSupportedException {
Person2 p = (Person2) super.clone();
p.personalInfo = (PersonalInfo2) this.personalInfo.clone();
return p;
}
}
在这里,Person2调用父类clone(),克隆出新实例p,也给p.personalInfo调用 PersonalInfo2的父类方法,而在此中
@Override
public Object clone() throws CloneNotSupportedException {
PersonalInfo2 pInfo = (PersonalInfo2) super.clone();
pInfo.name = new String(this.name);
pInfo.age = new String(this.age);
pInfo.address = new String(this.address);
return pInfo;
}
对所有引用类型String
全部new
产生和之前不同的对象,达到深度克隆的目的。
(因为String
没有实现Cloneable
接口,需要通过new
派生,这里是举特殊例子。如果是实现Cloneable
接口的类,直接嵌套调用clone()
方法就好了。)
这样,新的Person2
实例,与之前的引用完全不同,但是值是一样的!深度克隆成功。
Person2 person = new Person2("李达康", "55", "汉东省京州市");
Person2 person2 = (Person2) person.clone();
//一级对象
System.out.println(person == person2);//false
//二级对象
System.out.println(person.personalInfo == person2.personalInfo);//false
//三级对象
System.out.println(person.personalInfo.name == person2.personalInfo.name);//false
System.out.println(person.personalInfo.name);//李达康
System.out.println(person2.personalInfo.name);//李达康
三、还有其他深度克隆方式吗?
对象序列化与反序列化。但是这么做开销更大,不建议如此。(这里不做举例)
总结:
1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone
方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。
2.深克隆:是在引用类型的类中也实现了clone
,是clone
的嵌套,并且在clone
方法中又对没有clone
方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。
3.使用序列化也能完成深复制的功能:对象序列化后写入流中,此时也就不存在引用什么的概念了,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。