前言
写这篇博客的缘由是之前在做网易的笔试的时候,简答题问了浅拷贝和深拷贝的区别和在Java的实现,因为之前都没了解过深,浅拷贝所以白白丢分,所以今天就查阅了资料,好好整理下深,浅拷贝的区别
深拷贝和浅拷贝
浅拷贝: 浅拷贝指的是把原对象的所有属性都拷贝到新对象上去,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其新对象引用同一个对象。但是String例外,为啥我们下面会讲
深拷贝: 深拷贝指的是把原对象的所有属性都复制一份新的再拷贝到新对象上,也就是无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
实现
浅拷贝通过clone()方法实现,clone属于Object里的方法,也就是说所有对象都可以拷贝
但是要让类能够实现拷贝,类必须实现Cloneable接口,否则会抛出CloneNotSupportedException 异常
简单来说就是在拷贝的时候,clone()会先检查你要拷贝的类是否实现了Cloneable接口,如果没有会抛出异常。
实现
浅拷贝
我们先写一个Son类
public class Son implements Cloneable, Serializable {
private String name = "I am son";
private Integer age = 21;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
再写一个Father类,类中有Son对象, 注意:必须实现Cloneable接口
public class Father implements Cloneable, Serializable {
private String name = "I am father";
private Integer age = 30;
private Son son = new Son();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Son getSon() {
return son;
}
public void setSon(Son son) {
this.son = son;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试:
public static void main(String[] args) {
Father father = new Father();
try {
Father ganFather = (Father) father.clone();
System.out.println("ganFather == father : "+ (ganFather == father));
System.out.println("ganFather: "+ganFather);
System.out.println("father: "+father);
System.out.println("ganFather.getName() == father.getName() : "+(ganFather.getName() == father.getName()));
System.out.println(ganFather.getName().hashCode());
System.out.println(father.getName().hashCode());
father.setName("Father");
System.out.println(father.getName()+" , "+ganFather.getName());
father.setAge(55);
System.out.println(father.getAge()+" , "+ganFather.getAge());
System.out.println("ganFather.getSon() == father.getSon() : "+(ganFather.getSon() == father.getSon()));
System.out.println("ganFather.getSon() : "+ganFather.getSon());
System.out.println("father.getSon() : "+father.getSon());
father.getSon().setName("Son");
System.out.println(father.getSon().getName()+" , "+ganFather.getSon().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
结果:
从结果可以看到,父亲的名字和年龄改变了,但干爹的对应数据没有改变,但是修改父亲中儿子的名字,干爹中儿子的名字也跟着改变了,这就是因为浅拷贝只复制了对象的引用。
可能有人要问String也是引用类型,为啥父亲中String类型的name改变,干爹对应的却没有变呢,这是因为String是不可变的,指向常量池,因为String的不可变性,改变name时并不是改变它本身,而是新创建了个字符串并指向他
初始:
修改name之后:
可以看到父亲的引用改变了但干爹的引用并没有改变,所以String虽然是引用类型,但是它的改变并不会影响原始对象或者新对象。
而改变父亲Son的名字,干爹Son的名字也会改变是因为,他们引用的是同一个对象
初始:
修改name之后:
所以修改原对象的引用类型,新对象的引用类型也会跟着改变
深拷贝
深拷贝有两种方法
- 让每个引用类型都重写clone() 方法
既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Father类有一个引用类型 Son,我们在 Son类内部也重写 clone 方法。如下:
Son类
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
Father类
@Override
protected Object clone() throws CloneNotSupportedException {
Father father = (Father) super.clone();
father.son = (Son) father.son.clone();
return father;
// return super.clone();
}
测试跟上面一样,结果如下:
可以看到Son是两个对象,父亲的Son改变并没有影响干爹的Son。
这方法简单,但是有个弊端,这里我们Father类只有一个 Son 引用类型,而 Son 类没有,所以我们只用重写 Son 类的clone 方法,但是如果 Son 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。
- 序列化
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里将对象序列化写到缓冲区中,然后通过反序列化从缓冲区中获取这个对象。
注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。
public Father deepClone() throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (Father) objectInputStream.readObject();
}
因为序列化产生的是两个完全独立的对象,所以无论嵌套多少个引用类型,序列化都是能实现深拷贝的。