Java 基础知识之 深拷贝与浅拷贝(Cloneable接口、clone方法)
概述
浅拷贝(Shallow Copy)和深拷贝(DeepCopy)是在复制对象或数据结构(ArrayList
、HashMap
...)时的两种不同拷贝方式,它们的主要区别在于拷贝的程度。
Java 的类型有基本数据类型和引用类型,基本数据类型是可以由 CPU 直接操作的类型,无论是深拷贝还是浅拷贝,都是会复制出另一份。而引用类型仅仅是一个指针,指向的是这个对象在堆内存中分配的内存。
在浅拷贝时,仅仅是将这个指针拷贝了一份出来,两个指针都指向相同的堆内存地址;而深拷贝时,拷贝的就不仅仅是一个指针,还会在堆内存中将原来的对象也拷贝出来一份。
我们将在例子中讲解这个方法和接口的使用。
浅拷贝(Shallow Copy)
浅拷贝创建一个新的对象,但是只复制原始对象的基本数据类型的字段或引用(地址),而不复制引用指向的对象。这意味着新对象和原始对象中的引用指向相同的对象,这样的话,对新对象所做的修改可能会影响到原始对象,因为它们引用了相同的堆内存。
例如下面的类,Person
类包含一个基本数据类型 int age
,和一个引用数据类型 String name
:
class Person implements Cloneable {
String name;
int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
这个类使用了 Object
的默认 clone
方法,也就是浅拷贝。在调用这个类的 clone
方法时,仅仅会浅拷贝出一个新的 Person
对象,且这种拷贝方法不会调用构造方法。
我们通过如下代码进行拷贝操作:
try {
Person person1 = new Person("AAA", 30);
Person person2 = (Person) person1.clone(); //拷贝操作
System.out.println("person1 = " + person1 + ", person1.name = String@" + Integer.toHexString(System.identityHashCode(person1.name)));
System.out.println("person2 = " + person2 + ", person2.name = String@" + Integer.toHexString(System.identityHashCode(person2.name)));
} catch (CloneNotSupportedException exception) {
exception.printStackTrace();
}
person2
是通过 person1
拷贝出来的对象,由于默认使用了 Object.clone
的浅拷贝,person2
拷贝了 person1
的 age
,但由于 name
是引用类型,person2
只是拷贝了 person1
的 name
的指针而言。看一下输出:
mi@mi-HP:~/develop/code/JavaCode$ java Hello.java
person1 = Person@291caca8, person1.name = String@385e9564
person2 = Person@445b295b, person2.name = String@385e9564
可以看到,两个 Person
对象是不同的,但是它们引用的 name
是相同的。这就是浅拷贝。
深拷贝(Deep Copy)
深拷贝创建一个新的对象,并且递归地复制原始对象的所有字段和引用指向的对象,而不仅仅是复制引用本身。
深拷贝会递归复制整个对象结构,包括对象内部的对象,确保新对象和原始对象之间的所有关系都是独立的,这也意味着对新对象所做的修改不会影响到原始对象,因为它们拥有彼此独立的副本。
在编码时,让对象支持深拷贝需要对其依赖的每一个引用类型添加额外的处理,例如上面的浅拷贝的例子,我们将其更改为深拷贝:
class Person implements Cloneable {
String name;
int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.name = new String(this.name);
return person;
}
}
此处只有 clone
方法的更改,深拷贝需要对类中的每一个引用类型做处理,这里就是对 name
这个引用类型做了处理,新创建了一个对象。如果这个类中还包含其他的引用类型,就还需要依次处理,例如调用其 clone
方法或是其拷贝构造方法。
以下是深拷贝的输出结果:
mi@mi-HP:~/develop/code/JavaCode$ java Hello.java
person1 = Person@5c671d7f, person1.name = String@757277dc
person2 = Person@77f80c04, person2.name = String@1dac5ef
可以看到,不仅 person
是不一样的地址,连 person.name
也是不一样的对象。
Cloneable 接口 和 clone 方法
在上面两个例子中,拷贝是通过覆写 Object.clone
方法来完成的,但如果覆写这个方法,就需要类能够实现 Cloneable
接口,否则在调用 clone
时会出现 CloneNotSupportedException
异常。不过接口仅仅是做个标记,标记这个类是支持 clone
方法的。
无论是写深拷贝还是写浅拷贝,基本都是通过 clone
方法来进行的。因此记住以下几点你就会写 clone
方法了:
- 添加
Cloneable
接口 - 覆写
clone
方法 - 深拷贝需要处理类中的引用类型的拷贝
Objcet.clone
默认是浅拷贝
不过回过头想一下,这种设计好么,我记得接口不是这么用的,你 clone
方法放到 Cloneable
接口里面是不是更合理一些。确实也有很多人说这个 Cloneable
是个比较糟糕的设计,估计也是受限于早期 Java 的不完善,只能各种折中,最终整出来折中设计了。
转载自:https://juejin.cn/post/7386490986434428939