浅拷贝和深拷贝的区别及实现

    技术2022-07-10  164

    一、拷贝的引入

    (1)引用拷贝

    创建一个指向对象的引用变量的拷贝。

    Teacher teacher = new Teacher("Taylor",26); Teacher otherteacher = teacher; System.out.println(teacher); System.out.println(otherteacher);

    输出结果:

    blog.Teacher@355da254 blog.Teacher@355da254

    结果分析:由输出结果可以看出,它们的地址值是相同的,那么它们肯定是同一个对象。teacher和otherteacher的只是引用而已,他们都指向了一个相同的对象Teacher(“Taylor”,26)。 这就叫做引用拷贝。

    (2)对象拷贝

    创建对象本身的一个副本。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部数据。对象拷贝又分为浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。

    Teacher teacher = new Teacher("Swift",26); Teacher otherteacher = (Teacher)teacher.clone(); System.out.println(teacher); System.out.println(otherteacher);

    输出结果:

    blog.Teacher@355da254 blog.Teacher@4dc63996

    结果分析:由输出结果可以看出,它们的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量,这就叫做对象拷贝。 注意:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

    二、浅拷贝

    (1)定义
    对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

      简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象

    (2)浅拷贝实例

    浅拷贝的实现方式主要有两种:

    1)通过拷贝构造方法实现浅拷贝:

      拷贝构造方法指的是该类的构造方法参数为该类的对象。使用拷贝构造方法可以很好地完成浅拷贝,直接通过一个现有的对象创建出与该对象属性相同的新的对象。

    代码参考如下:

    /* 拷贝构造方法实现浅拷贝 */ public class CopyConstructor { public static void main(String[] args) { Age a=new Age(20); Person p1=new Person(a,"摇头耶稣"); Person p2=new Person(p1); System.out.println("p1是"+p1); System.out.println("p2是"+p2); //修改p1的各属性值,观察p2的各属性值是否跟随变化 p1.setName("小傻瓜"); a.setAge(99); System.out.println("修改后的p1是"+p1); System.out.println("修改后的p2是"+p2); } } class Person{ //两个属性值:分别代表值传递和引用传递 private Age age; private String name; public Person(Age age,String name) { this.age=age; this.name=name; } //拷贝构造方法 public Person(Person p) { this.name=p.name; this.age=p.age; } public void setName(String name) { this.name=name; } public String toString() { return this.name+" "+this.age; } } class Age{ private int age; public Age(int age) { this.age=age; } public void setAge(int age) { this.age=age; } public int getAge() { return this.age; } public String toString() { return getAge()+""; } }

    运行结果为:

    p1是摇头耶稣 20 p2是摇头耶稣 20 修改后的p1是小傻瓜 99 修改后的p2是摇头耶稣 99

    结果分析:这里对Person类选择了两个具有代表性的属性值:一个是引用传递类型;另一个是字符串类型(属于常量)。

      通过拷贝构造方法进行了浅拷贝,各属性值成功复制。其中,p1值传递部分的属性值发生变化时,p2不会随之改变;而引用传递部分属性值发生变化时,p2也随之改变。

    注意:如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝(例如,this.age=new Age(p.age.getAge());)。而对于一般的拷贝构造,则一定是浅拷贝。

    2)通过重写clone()方法进行浅拷贝:

      Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。但是需要注意:1、Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

    参考代码如下:对Student类的对象进行拷贝,直接重写clone()方法,通过调用clone方法即可完成浅拷贝。

    /* clone方法实现浅拷贝 */ public class ShallowCopy { public static void main(String[] args) { Age a=new Age(20); Student stu1=new Student("摇头耶稣",a,175); //通过调用重写后的clone方法进行浅拷贝 Student stu2=(Student)stu1.clone(); System.out.println(stu1.toString()); System.out.println(stu2.toString()); //尝试修改stu1中的各属性,观察stu2的属性有没有变化 stu1.setName("大傻子"); //改变age这个引用类型的成员变量的值 a.setAge(99); //stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值 stu1.setLength(216); System.out.println(stu1.toString()); System.out.println(stu2.toString()); } } /* * 创建年龄类 */ class Age{ //年龄类的成员变量(属性) private int age; //构造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } } /* * 创建学生类 */ class Student implements Cloneable{ //学生类的成员变量(属性),其中一个属性为类的对象 private String name; private Age aage; private int length; //构造方法,其中一个参数为另一个类的对象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } //eclipe中alt+shift+s自动添加所有的set和get方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } //设置输出的字符串形式 public String toString() { return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength(); } //重写Object类的clone方法 public Object clone() { Object obj=null; //调用Object类的clone方法,返回一个Object实例 try { obj= super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return obj; } }

    运行结果如下:

    姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 大傻子, 年龄为: 99, 长度是: 216 姓名是: 摇头耶稣, 年龄为: 99, 长度是: 175

    其中:Student类的成员变量我有代表性地设置了三种:基本数据类型的成员变量length,引用数据类型的成员变量age和字符串String类型的name.

    分析结果可以验证:

      基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;

      引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。

      String类型非常特殊,所以额外设置了一个字符串类型的成员变量来进行说明。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!也就是说,当我将name属性从“摇头耶稣”改为“大傻子"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”摇头耶稣“这个常量改为了指向”大傻子“这个常量。在这种情况下,另一个对象的name属性值仍然指向”摇头耶稣“不会受到影响。

    三、深拷贝

    (1)定义

      首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!深拷贝相比于浅拷贝速度较慢并且花销较大。

      简而言之,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,拷贝对象并没有对引用数据类型创建内存空间。

    (2)深拷贝实例

    深拷贝的实现方法主要有两种:

    一、通过重写clone方法来实现深拷贝

      与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。

    参考代码如下:

    /* 层次调用clone方法实现深拷贝 */ public class DeepCopy { public static void main(String[] args) { Age a=new Age(20); Student stu1=new Student("摇头耶稣",a,175); //通过调用重写后的clone方法进行浅拷贝 Student stu2=(Student)stu1.clone(); System.out.println(stu1.toString()); System.out.println(stu2.toString()); System.out.println(); //尝试修改stu1中的各属性,观察stu2的属性有没有变化 stu1.setName("大傻子"); //改变age这个引用类型的成员变量的值 a.setAge(99); //stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值 stu1.setLength(216); System.out.println(stu1.toString()); System.out.println(stu2.toString()); } } /* * 创建年龄类 */ class Age implements Cloneable{ //年龄类的成员变量(属性) private int age; //构造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } //重写Object的clone方法 public Object clone() { Object obj=null; try { obj=super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return obj; } } /* * 创建学生类 */ class Student implements Cloneable{ //学生类的成员变量(属性),其中一个属性为类的对象 private String name; private Age aage; private int length; //构造方法,其中一个参数为另一个类的对象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } public String toString() { return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength(); } //重写Object类的clone方法 public Object clone() { Object obj=null; //调用Object类的clone方法——浅拷贝 try { obj= super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } //调用Age类的clone方法进行深拷贝 //先将obj转化为学生类实例 Student stu=(Student)obj; //学生类实例的Age对象属性,调用其clone方法进行拷贝 stu.aage=(Age)stu.getaAge().clone(); return obj; } } 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 大傻子, 年龄为: 99, 长度是: 216 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175

    分析结果可以验证:进行了深拷贝之后,无论是什么类型的属性值的修改,都不会影响另一个对象的属性值。

    二、通过对象序列化实现深拷贝

      虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。

      将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

    java的序列化机制,这一篇文章就够了(关于序列化机制的介绍,可以参考这一篇文章)

    参考代码如下:

    import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /* 通过序列化实现深拷贝 */ public class DeepCopyBySerialization { public static void main(String[] args) throws IOException, ClassNotFoundException { Age a=new Age(20); Student stu1=new Student("摇头耶稣",a,175); //通过序列化方法实现深拷贝 ByteArrayOutputStream bos=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(stu1); oos.flush(); ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); Student stu2=(Student)ois.readObject(); System.out.println(stu1.toString()); System.out.println(stu2.toString()); System.out.println(); //尝试修改stu1中的各属性,观察stu2的属性有没有变化 stu1.setName("大傻子"); //改变age这个引用类型的成员变量的值 a.setAge(99); stu1.setLength(216); System.out.println(stu1.toString()); System.out.println(stu2.toString()); } } /* * 创建年龄类 */ class Age implements Serializable{ //年龄类的成员变量(属性) private int age; //构造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } } /* * 创建学生类 */ class Student implements Serializable{ //学生类的成员变量(属性),其中一个属性为类的对象 private String name; private Age aage; private int length; //构造方法,其中一个参数为另一个类的对象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } //eclipe中alt+shift+s自动添加所有的set和get方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } //设置输出的字符串形式 public String toString() { return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength(); } }

    运行结果为:

    姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175 姓名是: 大傻子, 年龄为: 99, 长度是: 216 姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175

      可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient(:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化)修饰,那么该属性就无法被拷贝了。

    参考 1、Java深入理解深拷贝和浅拷贝区别 2、Java 浅拷贝和深拷贝的理解和实现方式

    Processed: 0.018, SQL: 8