这是今天我们在技术群里面讨论的一个知识点,讨论的相当激烈,由于对这一块使用的比较少,所以对这一块多少有些盲区。这篇文章总结了所讨论的内容,希望这篇文章对你有所帮助。

在 Java 开发中,对象拷贝或者说对象克隆是常有的事,对象克隆最终都离不开直接赋值浅拷贝深拷贝 这三种方式,其中直接赋值应该是我们最常用的一种方式吧,对于浅拷贝和深拷贝可能用的少,所以或多或少存在一些误区,这篇文章会详细的介绍这三种对象克隆方式。

  1. 值类型:Java 的基本数据类型,例如 intfloat
  2. 引用类型:自定义类和 Java 包装类(stringinteger

直接赋值是我们最常用的方式,在我们代码中的体现是Persona = new Person();Person b = a,是一种简单明了的方式,但是它只是拷贝了对象引用地址而已,并没有在内存中生成新的对象,我们可以通过下面这个例子来证明这一点

  1. // person 对象
  2. public class Person {
  3. // 姓名
  4. private String name;
  5. // 年龄
  6. private int age;
  7. // 邮件
  8. private String email;
  9. // 描述
  10. private String desc;
  11. ...省略get/set...
  12. }
  13. // main 方法
  14. public class PersonApp {
  15. public static void main(String[] args) {
  16. // 初始化一个对象
  17. Person person = new Person("张三",20,"123456@qq.com","我是张三");
  18. // 复制对象
  19. Person person1 = person;
  20. // 改变 person1 的属性值
  21. person1.setName("我不是张三了");
  22. System.out.println("person对象:"+person);
  23. System.out.println("person1对象:"+person1);
  24. }
  25. }

运行上面代码,你会得到如下结果:

  1. person对象:Person{name=\'我不是张三了\', age=20, email=\'123456@qq.com\', desc=\'我是张三\'}
  2. person1对象:Person{name=\'我不是张三了\', age=20, email=\'123456@qq.com\', desc=\'我是张三\'}

我们将 person 对象复制给了 person1 对象,我们对 person1 对象的 name 属性进行了修改,并未修改 person 对象的name 属性值,但是我们最后发现 person 对象的 name 属性也发生了变化,其实不止这一个值,对于其他值也是一样的,所以这结果证明了我们上面的结论:直接赋值的方式没有生产新的对象,只是生新增了一个对象引用,直接赋值在 Java 内存中的模型大概是这样的

浅拷贝也可以实现对象克隆,从这名字你或许可以知道,这种拷贝一定存在某种缺陷,是的,它就是存在一定的缺陷,先来看看浅拷贝的定义:如果原型对象的成员变量是值类型,将复制一份给克隆对象,也就是说在堆中拥有独立的空间;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。换句话说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。 可能你没太理解这段话,那么我们在来看看浅拷贝的通用模型:

浅拷贝通用模型

要实现对象浅拷贝还是比较简单的,只需要被复制类需要实现 Cloneable 接口,重写 clone 方法即可,对 person 类进行改造,使其可以支持浅拷贝。

  1. public class Person implements Cloneable {
  2. // 姓名
  3. private String name;
  4. // 年龄
  5. private int age;
  6. // 邮件
  7. private String email;
  8. // 描述
  9. private String desc;
  10. /*
  11. * 重写 clone 方法,需要将权限改成 public ,直接调用父类的 clone 方法就好了
  12. */
  13. @Override
  14. public Object clone() throws CloneNotSupportedException {
  15. return super.clone();
  16. }
  17. ...省略...
  18. }

改造很简单只需要让 person 继承 Cloneable 接口,并且重写 clone 方法即可,clone 也非常简单只需要调用 object 的 clone 方法就好,唯一需要注意的地方就是 clone 方法需要用 public 来修饰,在简单的修改 main 方法

  1. public class PersonApp {
  2. public static void main(String[] args) throws Exception {
  3. // 初始化一个对象
  4. Person person = new Person("张三",20,"123456@qq.com","我是张三");
  5. // 复制对象
  6. Person person1 = (Person) person.clone();
  7. // 改变 person1 的属性值
  8. person1.setName("我是张三的克隆对象");
  9. // 修改 person age 的值
  10. person1.setAge(22);
  11. System.out.println("person对象:"+person);
  12. System.out.println();
  13. System.out.println("person1对象:"+person1);
  14. }
  15. }

重新运行 main 方法,结果如下:

  1. person对象:Person{name=\'张三\', age=20, email=\'123456@qq.com\', desc=\'我是张三\'}
  2. person1对象:Person{name=\'我是张三的克隆对象\', age=22, email=\'123456@qq.com\', desc=\'我是张三\'}

看到这个结果,你是否有所质疑呢?说好的引用对象只是拷贝了地址,为啥修改了 person1 对象的 name 属性值,person 对象没有改变?这里就是一个非常重要的知识点了,,原因在于:String、Integer 等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址,所以在这里我们修改了 person1 对象的 name 属性值,person1 对象的 name 字段指向了内存中新的 name 对象,但是我们并没有改变 person 对象的 name 字段的指向,所以 person 对象的 name 还是指向内存中原来的 name 地址,也就没有变化

这种引用是一种特列,因为这些引用具有不可变性,并不具备通用性,所以我们就自定义一个类,来演示浅拷贝,我们定义一个 PersonDesc 类用来存放person 对象中的 desc 字段,,然后在 person 对象中引用 PersonDesc 类,具体代码如下:

  1. // 新增 PersonDesc
  2. public class PersonDesc {
  3. // 描述
  4. private String desc;
  5. }
  6. public class Person implements Cloneable {
  7. // 姓名
  8. private String name;
  9. // 年龄
  10. private int age;
  11. // 邮件
  12. private String email;
  13. // 将原来的 string desc 变成了 PersonDesc 对象,这样 personDesc 就是引用类型
  14. private PersonDesc personDesc;
  15. @Override
  16. public Object clone() throws CloneNotSupportedException {
  17. return super.clone();
  18. }
  19. public void setDesc(String desc) {
  20. this.personDesc.setDesc(desc);
  21. }
  22. public Person(String name, int age, String email, String desc) {
  23. this.name = name;
  24. this.age = age;
  25. this.email = email;
  26. this.personDesc = new PersonDesc();
  27. this.personDesc.setDesc(desc);
  28. }
  29. ...省略...
  30. }

修改 main 方法

  1. public class PersonApp {
  2. public static void main(String[] args) throws Exception {
  3. // 初始化一个对象
  4. Person person = new Person("平头哥",20,"123456@qq.com","我的公众号是:平头哥的技术博文");
  5. // 复制对象
  6. Person person1 = (Person) person.clone();
  7. // 改变 person1 的属性值
  8. person1.setName("我是平头哥的克隆对象");
  9. // 修改 person age 的值
  10. person1.setAge(22);
  11. person1.setDesc("我已经关注了平头哥的技术博文公众号");
  12. System.out.println("person对象:"+person);
  13. System.out.println();
  14. System.out.println("person1对象:"+person1);
  15. }
  16. }

运行 main 方法,得到如下结果:

  1. person对象:Person{name=\'平头哥\', age=20, email=\'123456@qq.com\', desc=\'我已经关注了平头哥的技术博文公众号\'}
  2. person1对象:Person{name=\'我是平头哥的克隆对象\', age=22, email=\'123456@qq.com\', desc=\'我已经关注了平头哥的技术博文公众号\'}

我们修改 person1 的 desc 字段之后,person 的 desc 也发生了改变,这说明 person 对象和 person1 对象指向是同一个 PersonDesc 对象地址,这也符合浅拷贝引用对象只拷贝引用地址并未创建新对象的定义,到这你应该知道浅拷贝了吧。

深拷贝也是对象克隆的一种方式,相对于浅拷贝,深拷贝是一种完全拷贝,无论是值类型还是引用类型都会完完全全的拷贝一份,在内存中生成一个新的对象,简单点说就是拷贝对象和被拷贝对象没有任何关系,互不影响。深拷贝的通用模型如下:

深拷贝通用模型

深拷贝有两种方式,一种是跟浅拷贝一样实现 Cloneable 接口,另一种是实现 Serializable 接口,用序列化的方式来实现深拷贝,我们分别用这两种方式来实现深拷贝

实现 Cloneable 接口的方式跟浅拷贝相差不大,我们需要引用对象也实现 Cloneable 接口,具体代码改造如下:

  1. public class PersonDesc implements Cloneable{
  2. // 描述
  3. private String desc;
  4. ...省略...
  5. @Override
  6. public Object clone() throws CloneNotSupportedException {
  7. return super.clone();
  8. }
  9. }
  10. public class Person implements Cloneable {
  11. // 姓名
  12. private String name;
  13. // 年龄
  14. private int age;
  15. // 邮件
  16. private String email;
  17. private PersonDesc personDesc;
  18. /**
  19. * clone 方法不是简单的调用super的clone 就好,
  20. */
  21. @Override
  22. public Object clone() throws CloneNotSupportedException {
  23. Person person = (Person)super.clone();
  24. // 需要将引用对象也克隆一次
  25. person.personDesc = (PersonDesc) personDesc.clone();
  26. return person;
  27. }
  28. ...省略...
  29. }

main 方法不需要任何改动,我们再次运行 main 方法,得到如下结果:

  1. person对象:Person{name=\'平头哥\', age=20, email=\'123456@qq.com\', desc=\'我的公众号是:平头哥的技术博文\'}
  2. person1对象:Person{name=\'我是平头哥的克隆对象\', age=22, email=\'123456@qq.com\', desc=\'我已经关注了平头哥的技术博文公众号\'}

可以看出,修改 person1 的 desc 时对 person 的 desc 已经没有影响了,说明进行了深拷贝,在内存中重新生成了一个新的对象。

实现 Serializable 接口方式也可以实现深拷贝,而且这种方式还可以解决多层克隆的问题,多层克隆就是引用类型里面又有引用类型,层层嵌套下去,用 Cloneable 方式实现还是比较麻烦的,一不小心写错了就不能实现深拷贝了,使用 Serializable 序列化的方式就需要所有的对象对实现 Serializable 接口,我们对代码进行改造,改造成序列化的方式

  1. public class Person implements Serializable {
  2. private static final long serialVersionUID = 369285298572941L;
  3. // 姓名
  4. private String name;
  5. // 年龄
  6. private int age;
  7. // 邮件
  8. private String email;
  9. private PersonDesc personDesc;
  10. public Person clone() {
  11. Person person = null;
  12. try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
  13. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  14. ObjectOutputStream oos = new ObjectOutputStream(baos);
  15. oos.writeObject(this);
  16. // 将流序列化成对象
  17. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
  18. ObjectInputStream ois = new ObjectInputStream(bais);
  19. person = (Person) ois.readObject();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. } catch (ClassNotFoundException e) {
  23. e.printStackTrace();
  24. }
  25. return person;
  26. }
  27. public void setDesc(String desc) {
  28. this.personDesc.setDesc(desc);
  29. }
  30. ...省略...
  31. }
  32. public class PersonDesc implements Serializable {
  33. private static final long serialVersionUID = 872390113109L;
  34. // 描述
  35. private String desc;
  36. public String getDesc() {
  37. return desc;
  38. }
  39. public void setDesc(String desc) {
  40. this.desc = desc;
  41. }
  42. }
  43. public class PersonApp {
  44. public static void main(String[] args) throws Exception {
  45. // 初始化一个对象
  46. Person person = new Person("平头哥",20,"123456@qq.com","我的公众号是:平头哥的技术博文");
  47. // 复制对象
  48. Person person1 = (Person) person.clone();
  49. // 改变 person1 的属性值
  50. person1.setName("我是平头哥的克隆对象");
  51. // 修改 person age 的值
  52. person1.setAge(22);
  53. person1.setDesc("我已经关注了平头哥的技术博文公众号");
  54. System.out.println("person对象:"+person);
  55. System.out.println();
  56. System.out.println("person1对象:"+person1);
  57. }
  58. }

运行 main 方法,我们可以得到跟 Cloneable 方式一样的结果,序列化的方式也实现了深拷贝。到此关于 Java 浅拷贝和深拷贝的相关内容就介绍完了,希望你有所收获。

目前互联网上很多大佬都有 Java 对象克隆文章,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

欢迎扫码关注微信公众号:「平头哥的技术博文」,和平头哥一起学习,一起进步。

平头哥的技术博文

版权声明:本文为jamaler原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/jamaler/p/11981645.html