Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。
在讨论主题之前我们先了解一个基本知识点,Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时是有差别的。对于基本数据类型是值传递,对于引用数据类型则是引用传递(可以理解为存放在堆内存中对象的地址值)。
一、浅拷贝:
我们看以下代码:
public class Teacher {
public Teacher(String name) {
super();
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Student {
private int stuNo;
private Teacher teacher;
public Student() {
super();
}
public Student (Student s) {
this.stuNo = s.getStuNo();
this.teacher = s.getTeacher();
}
public int getStuNo() {
return stuNo;
}
public void setStuNo(int stuNo) {
this.stuNo = stuNo;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student [stuNo=" + stuNo + ", teacher=" + teacher.getName() + "]";
}
}
public class Client {
public static void main(String[] args) {
Student s1 = new Student();
s1.setStuNo(10);
Teacher t = new Teacher("张老师");
s1.setTeacher(t);
Student s2 = new Student(s1);
s1.getTeacher().setName("李老师");
s1.setStuNo(20);
System.out.println("s1 :" + s1.toString());
System.out.println("s2 :" + s2.toString());
}
}
分析以上代码:
Student类有两个成员变量,一个int类型age、一个引用类型Teacher。在他的构造方法中传入一个Student对象,然后使用这个Student对象的值给当前对象赋值。
Teacher类很简单只有一个String类型的变量name。
Client类 1,创建了s1对象并赋值。2,使用s1对象创建s2对象。3重新设置s1对象的老师姓名和学号
运行结果:
s1 :Student [stuNo=20, teacher=李老师]
s2 :Student [stuNo=10, teacher=李老师]
我们发现我们改变了s1对象的老师姓名,s2的跟随着也变化了,这就是java中的浅拷贝。
①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。②对于数据类型是引用数据类型的成员变量,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
具体模型如下图所示:可以看到基本数据类型的成员变量,对其值创建了新的拷贝。而引用数据类型的成员变量的实例仍然是只有一份,两个对象的该成员变量都指向同一个实例。
对于上面的浅拷贝功能我们也可以通过重写clone()方法来实现:
Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。但是需要注意:1、Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。
参考代码如下:我们让Student实现Cloneable接口,重写clone()方法。
public class Student implements Cloneable{
private int stuNo;
private Teacher teacher;
public Student() {
super();
}
public int getStuNo() {
return stuNo;
}
public void setStuNo(int stuNo) {
this.stuNo = stuNo;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student [stuNo=" + stuNo + ", teacher=" + teacher.getName() + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setStuNo(10);
Teacher t = new Teacher("张老师");
s1.setTeacher(t);
Student s2 = (Student) s1.clone();
s1.getTeacher().setName("李老师");
s1.setStuNo(20);
System.out.println("s1 :" + s1.toString());
System.out.println("s2 :" + s2.toString());
}
}
运行结果如下:
s1 :Student [stuNo=20, teacher=李老师]
s2 :Student [stuNo=10, teacher=李老师]
分析结果可以验证:
基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。
我们再给Student类增加一个成员变量String类型的stuName,看看浅拷贝后的结果如何。
public class Student implements Cloneable{
private int stuNo;
private Teacher teacher;
private String stuName;
public Student() {
super();
}
public int getStuNo() {
return stuNo;
}
public void setStuNo(int stuNo) {
this.stuNo = stuNo;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
@Override
public String toString() {
return "Student [stuNo=" + stuNo + ", teacher=" + teacher.getName() + ", stuName=" + stuName + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setStuNo(10);
s1.setStuName("Lucy");
Teacher t = new Teacher("张老师");
s1.setTeacher(t);
Student s2 = (Student) s1.clone();
//改变s1的成员变量值
s1.getTeacher().setName("李老师");
s1.setStuNo(20);
s1.setStuName("Tom");
System.out.println("s1 :" + s1.toString());
System.out.println("s2 :" + s2.toString());
}
}
运行结果如下:
s1 :Student [stuNo=20, teacher=李老师, stuName=Tom]
s2 :Student [stuNo=10, teacher=李老师, stuName=Lucy]
我们发现我们改变了s1的学生名字但s2并没用改变,String为引用类型,这个结果显然不符合我们前面讲的引用类型赋值是地址值传递的知识点。
String类型虽然属于引用数据类型,但是String类型的数据是存放在常量池中的,是无法修改的!也就是说,当我将name属性从“Lucy”改为“Tom"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”Lucy“这个常量改为了指向”Tom“这个常量。在这种情况下,另一个对象的name属性值仍然指向”Lucy“不会受到影响。
二、深拷贝:
深拷贝与浅拷贝的区别是在引用数据类型的成员变量的拷贝上:
对于引用数据类型的赋值,浅拷贝只是地址值的拷贝,并没有创建出新的对象。深拷贝是重新创建出一个对象。
因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。
深拷贝的实现方法主要有两种:
一、通过重写clone方法来实现深拷贝
与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
参考代码如下:
public class Teacher implements Cloneable{
public Teacher(String name) {
super();
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Student implements Cloneable{
private int stuNo;
private Teacher teacher;
private String stuName;
public Student() {
super();
}
public int getStuNo() {
return stuNo;
}
public void setStuNo(int stuNo) {
this.stuNo = stuNo;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
@Override
public String toString() {
return "Student [stuNo=" + stuNo + ", teacher=" + teacher.getName() + ", stuName=" + stuName + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
Student stu = ((Student)obj);
//对引用数据类型的变量进行浅拷贝
stu.setTeacher((Teacher)stu.getTeacher().clone());
return obj;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setStuNo(10);
s1.setStuName("Lucy");
Teacher t = new Teacher("张老师");
s1.setTeacher(t);
Student s2 = (Student) s1.clone();
//改变s1的成员变量值
s1.getTeacher().setName("李老师");
s1.setStuNo(20);
s1.setStuName("Tom");
System.out.println("s1 :" + s1.toString());
System.out.println("s2 :" + s2.toString());
}
}
运行结果:
s1 :Student [stuNo=20, teacher=李老师, stuName=Tom]
s2 :Student [stuNo=10, teacher=张老师, stuName=Lucy]
分析结果可以验证:进行了深拷贝之后,无论是什么类型的属性值的修改,都不会影响另一个对象的属性值。
二、通过对象序列化实现深拷贝
虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。
将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝,只需要将参与拷贝的所有类都实现Serializable接口,然后通过io流实现拷贝。
参考代码如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
// 写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
// 分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
// 返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setStuNo(10);
s1.setStuName("Lucy");
Teacher t = new Teacher("张老师");
s1.setTeacher(t);
Student s2 = (Student) CloneUtils.clone(s1);
//改变s1的成员变量值
s1.getTeacher().setName("李老师");
s1.setStuNo(20);
s1.setStuName("Tom");
System.out.println("s1 :" + s1.toString());
System.out.println("s2 :" + s2.toString());
}
}
运行结果为:
s1 :Student [stuNo=20, teacher=李老师, stuName=Tom]
s2 :Student [stuNo=10, teacher=张老师, stuName=Lucy]
可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。
以上是浅拷贝的深拷贝的区别和实现方式。