Java 浅拷贝和深拷贝

Wesley13
• 阅读 994

Java 浅拷贝和深拷贝

前言

Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。

举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a;  B.b=A.b;

在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部 数据。

Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。

先介绍一点铺垫知识:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

浅拷贝(Shallow Copy)

①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

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

具体模型如图所示:可以看到基本数据类型的成员变量,对其值创建了新的拷贝。而引用数据类型的成员变量的实例仍然是只有一份,两个对象的该成员变量都指向同一个实例。

Java 浅拷贝和深拷贝

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

一、通过拷贝构造方法实现浅拷贝:

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

代码参考如下:

  1. /* 拷贝构造方法实现浅拷贝 */

  2. public class CopyConstructor {

  3. public static void main(String[] args) {

  4. Age a=new Age(20);

  5. Person p1=new Person(a,"摇头耶稣");

  6. Person p2=new Person(p1);

  7. System.out.println("p1是"+p1);

  8. System.out.println("p2是"+p2);

  9. //修改p1的各属性值,观察p2的各属性值是否跟随变化

  10. p1.setName("小傻瓜");

  11. a.setAge(99);

  12. System.out.println("修改后的p1是"+p1);

  13. System.out.println("修改后的p2是"+p2);

  14. }

  15. }

  16. class Person{

  17. //两个属性值:分别代表值传递和引用传递

  18. private Age age;

  19. private String name;

  20. public Person(Age age,String name) {

  21. this.age=age;

  22. this.name=name;

  23. }

  24. //拷贝构造方法

  25. public Person(Person p) {

  26. this.name=p.name;

  27. this.age=p.age;

  28. }

  29. public void setName(String name) {

  30. this.name=name;

  31. }

  32. public String toString() {

  33. return this.name+" "+this.age;

  34. }

  35. }

  36. class Age{

  37. private int age;

  38. public Age(int age) {

  39. this.age=age;

  40. }

  41. public void setAge(int age) {

  42. this.age=age;

  43. }

  44. public int getAge() {

  45. return this.age;

  46. }

  47. public String toString() {

  48. return getAge()+"";

  49. }

  50. }

运行结果为:

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

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

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

要注意:如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝。而对于一般的拷贝构造,则一定是浅拷贝。

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

Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。

但是需要注意:

1、Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。

2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

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

  1. /* clone方法实现浅拷贝 */

  2. public class ShallowCopy {

  3. public static void main(String[] args) {

  4. Age a=new Age(20);

  5. Student stu1=new Student("摇头耶稣",a,175);

  6. //通过调用重写后的clone方法进行浅拷贝

  7. Student stu2=(Student)stu1.clone();

  8. System.out.println(stu1.toString());

  9. System.out.println(stu2.toString());

  10. //尝试修改stu1中的各属性,观察stu2的属性有没有变化

  11. stu1.setName("大傻子");

  12. //改变age这个引用类型的成员变量的值

  13. a.setAge(99);

  14. //stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值

  15. stu1.setLength(216);

  16. System.out.println(stu1.toString());

  17. System.out.println(stu2.toString());

  18. }

  19. }

  20. /*

  21. * 创建年龄类

  22. */

  23. class Age{

  24. //年龄类的成员变量(属性)

  25. private int age;

  26. //构造方法

  27. public Age(int age) {

  28. this.age=age;

  29. }

  30. public int getAge() {

  31. return age;

  32. }

  33. public void setAge(int age) {

  34. this.age = age;

  35. }

  36. public String toString() {

  37. return this.age+"";

  38. }

  39. }

  40. /*

  41. * 创建学生类

  42. */

  43. class Student implements Cloneable{

  44. //学生类的成员变量(属性),其中一个属性为类的对象

  45. private String name;

  46. private Age aage;

  47. private int length;

  48. //构造方法,其中一个参数为另一个类的对象

  49. public Student(String name,Age a,int length) {

  50. this.name=name;

  51. this.aage=a;

  52. this.length=length;

  53. }

  54. //eclipe中alt+shift+s自动添加所有的set和get方法

  55. public String getName() {

  56. return name;

  57. }

  58. public void setName(String name) {

  59. this.name = name;

  60. }

  61. public Age getaAge() {

  62. return this.aage;

  63. }

  64. public void setaAge(Age age) {

  65. this.aage=age;

  66. }

  67. public int getLength() {

  68. return this.length;

  69. }

  70. public void setLength(int length) {

  71. this.length=length;

  72. }

  73. //设置输出的字符串形式

  74. public String toString() {

  75. return "姓名是:"+this.getName()+", 年龄为:"+this.getaAge().toString()+", 长度是:"+this.getLength();

  76. }

  77. //重写Object类的clone方法

  78. public Object clone() {

  79. Object obj=null;

  80. //调用Object类的clone方法,返回一个Object实例

  81. try {

  82. obj= super.clone();

  83. } catch (CloneNotSupportedException e) {

  84. e.printStackTrace();

  85. }

  86. return obj;

  87. }

  88. }

运行结果如下:

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

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

分析结果可以验证:

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

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

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

深拷贝

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

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

深拷贝模型如图所示:可以看到所有的成员变量都进行了复制。

Java 浅拷贝和深拷贝

因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。

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

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

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

参考代码如下:

  1. package linearList;

  2. /* 层次调用clone方法实现深拷贝 */

  3. public class DeepCopy {

  4. public static void main(String[] args) {

  5. Age a=new Age(20);

  6. Student stu1=new Student("摇头耶稣",a,175);

  7. //通过调用重写后的clone方法进行浅拷贝

  8. Student stu2=(Student)stu1.clone();

  9. System.out.println(stu1.toString());

  10. System.out.println(stu2.toString());

  11. System.out.println();

  12. //尝试修改stu1中的各属性,观察stu2的属性有没有变化

  13. stu1.setName("大傻子");

  14. //改变age这个引用类型的成员变量的值

  15. a.setAge(99);

  16. //stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值

  17. stu1.setLength(216);

  18. System.out.println(stu1.toString());

  19. System.out.println(stu2.toString());

  20. }

  21. }

  22. /*

  23. * 创建年龄类

  24. */

  25. class Age implements Cloneable{

  26. //年龄类的成员变量(属性)

  27. private int age;

  28. //构造方法

  29. public Age(int age) {

  30. this.age=age;

  31. }

  32. public int getAge() {

  33. return age;

  34. }

  35. public void setAge(int age) {

  36. this.age = age;

  37. }

  38. public String toString() {

  39. return this.age+"";

  40. }

  41. //重写Object的clone方法

  42. public Object clone() {

  43. Object obj=null;

  44. try {

  45. obj=super.clone();

  46. } catch (CloneNotSupportedException e) {

  47. e.printStackTrace();

  48. }

  49. return obj;

  50. }

  51. }

  52. /*

  53. * 创建学生类

  54. */

  55. class Student implements Cloneable{

  56. //学生类的成员变量(属性),其中一个属性为类的对象

  57. private String name;

  58. private Age aage;

  59. private int length;

  60. //构造方法,其中一个参数为另一个类的对象

  61. public Student(String name,Age a,int length) {

  62. this.name=name;

  63. this.aage=a;

  64. this.length=length;

  65. }

  66. public String getName() {

  67. return name;

  68. }

  69. public void setName(String name) {

  70. this.name = name;

  71. }

  72. public Age getaAge() {

  73. return this.aage;

  74. }

  75. public void setaAge(Age age) {

  76. this.aage=age;

  77. }

  78. public int getLength() {

  79. return this.length;

  80. }

  81. public void setLength(int length) {

  82. this.length=length;

  83. }

  84. public String toString() {

  85. return "姓名是:"+this.getName()+", 年龄为:"+this.getaAge().toString()+", 长度是:"+this.getLength();

  86. }

  87. //重写Object类的clone方法

  88. public Object clone() {

  89. Object obj=null;

  90. //调用Object类的clone方法——浅拷贝

  91. try {

  92. obj= super.clone();

  93. } catch (CloneNotSupportedException e) {

  94. e.printStackTrace();

  95. }

  96. //调用Age类的clone方法进行深拷贝

  97. //先将obj转化为学生类实例

  98. Student stu=(Student)obj;

  99. //学生类实例的Age对象属性,调用其clone方法进行拷贝

  100. stu.aage=(Age)stu.getaAge().clone();

  101. return obj;

  102. }

  103. }

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

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

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

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

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

参考代码如下:

  1. import java.io.ByteArrayInputStream;

  2. import java.io.ByteArrayOutputStream;

  3. import java.io.IOException;

  4. import java.io.ObjectInputStream;

  5. import java.io.ObjectOutputStream;

  6. import java.io.Serializable;

  7. /* 通过序列化实现深拷贝 */

  8. public class DeepCopyBySerialization {

  9. public static void main(String[] args) throws IOException, ClassNotFoundException {

  10. Age a=new Age(20);

  11. Student stu1=new Student("摇头耶稣",a,175);

  12. //通过序列化方法实现深拷贝

  13. ByteArrayOutputStream bos=new ByteArrayOutputStream();

  14. ObjectOutputStream oos=new ObjectOutputStream(bos);

  15. oos.writeObject(stu1);

  16. oos.flush();

  17. ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));

  18. Student stu2=(Student)ois.readObject();

  19. System.out.println(stu1.toString());

  20. System.out.println(stu2.toString());

  21. System.out.println();

  22. //尝试修改stu1中的各属性,观察stu2的属性有没有变化

  23. stu1.setName("大傻子");

  24. //改变age这个引用类型的成员变量的值

  25. a.setAge(99);

  26. stu1.setLength(216);

  27. System.out.println(stu1.toString());

  28. System.out.println(stu2.toString());

  29. }

  30. }

  31. /*

  32. * 创建年龄类

  33. */

  34. class Age implements Serializable{

  35. //年龄类的成员变量(属性)

  36. private int age;

  37. //构造方法

  38. public Age(int age) {

  39. this.age=age;

  40. }

  41. public int getAge() {

  42. return age;

  43. }

  44. public void setAge(int age) {

  45. this.age = age;

  46. }

  47. public String toString() {

  48. return this.age+"";

  49. }

  50. }

  51. /*

  52. * 创建学生类

  53. */

  54. class Student implements Serializable{

  55. //学生类的成员变量(属性),其中一个属性为类的对象

  56. private String name;

  57. private Age aage;

  58. private int length;

  59. //构造方法,其中一个参数为另一个类的对象

  60. public Student(String name,Age a,int length) {

  61. this.name=name;

  62. this.aage=a;

  63. this.length=length;

  64. }

  65. //eclipe中alt+shift+s自动添加所有的set和get方法

  66. public String getName() {

  67. return name;

  68. }

  69. public void setName(String name) {

  70. this.name = name;

  71. }

  72. public Age getaAge() {

  73. return this.aage;

  74. }

  75. public void setaAge(Age age) {

  76. this.aage=age;

  77. }

  78. public int getLength() {

  79. return this.length;

  80. }

  81. public void setLength(int length) {

  82. this.length=length;

  83. }

  84. //设置输出的字符串形式

  85. public String toString() {

  86. return "姓名是:"+this.getName()+", 年龄为:"+this.getaAge().toString()+", 长度是:"+this.getLength();

  87. }

  88. }

运行结果为:

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

可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。

以上是浅拷贝的深拷贝的区别和实现方式。

原文:https://www.cnblogs.com/shakinghead/p/7651502.html 作者:摇头耶稣

本文分享自微信公众号 - 爱编码(ilovecode)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java 复制Map对象(深拷贝与浅拷贝)
java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这