java对象---深/浅拷贝

继原型模式的续,本文分享的是浅拷贝和深拷贝

深入了解浅拷贝与深拷贝

在学习深拷贝和浅拷贝之前,咱们先来一个例子;

  1. import java.util.ArrayList;
  2. public class MyBaby implements Cloneable {
  3.    /**
  4.     * 私有变量
  5.     */
  6.    private ArrayList<String> list = new ArrayList<>();
  7.    @Override
  8.    protected Object clone() throws CloneNotSupportedException {
  9.        MyBaby myBaby = null;
  10.        try {
  11.            myBaby = (MyBaby) super.clone();
  12.       } catch (CloneNotSupportedException ex) {
  13.            ex.printStackTrace();
  14.       }
  15.        return myBaby;
  16.   }
  17.    /**
  18.     * 给List设置
  19.     *
  20.     * @param value 值
  21.     */
  22.    public void setValue(String value) {
  23.        this.list.add(value);
  24.   }
  25.    /**
  26.     * 获取list
  27.     *
  28.     * @return list
  29.     */
  30.    public ArrayList<String> getValue() {
  31.        return this.list;
  32.   }
  33. }

在MyBaby类中有一个私有变量list,类似为List,然后咱们使用setValue对其进行设值,使用getValue进行取值。接下来咱们来看看他是如何拷贝的。

  1. public class TestMyBaby {
  2.    public static void main(String[] args) {
  3.        MyBaby baby = new MyBaby();
  4.        baby.setValue("Java后端技术栈");
  5.        try {
  6.            MyBaby myBabyClone = (MyBaby) baby.clone();
  7.            myBabyClone.setValue("咖啡");
  8.            
  9.            System.out.println(baby.getValue());
  10.       } catch (CloneNotSupportedException e) {
  11.            e.printStackTrace();
  12.       }
  13.   }
  14. }

猜想运行结果会是神马?

[Java后端技术栈, 咖啡]

怎么会这样呢?怎么会有“咖啡”呢?

是因为Java给我们做了一个偷懒性的拷贝动作,Object类原本就提供一个方法clone用来拷贝对象,因为其对象内部的数组、引用对象等都不拷贝,还是指向了原生对象的内部元素地址,这种拷贝就叫做浅拷贝

浅拷贝

上面这个拷贝也太浅了吧,两个对象引用都boby、myBaby共享一个私有变量list,都可以对list进行改变,是一种非常不安全的方式。

 

再看一个例子;

  1. public class Person implements Cloneable {
  2.    private int age;
  3.    private String name;
  4.    public int getAge() {
  5.        return age;
  6.   }
  7.    public void setAge(int age) {
  8.        this.age = age;
  9.   }
  10.    public String getName() {
  11.        return name;
  12.   }
  13.    public void setName(String name) {
  14.        this.name = name;
  15.   }
  16.    @Override
  17.    protected Object clone() throws CloneNotSupportedException {
  18.        Person person=null;
  19.        try{
  20.            person=(Person)super.clone();
  21.       }catch (CloneNotSupportedException ex){
  22.            ex.printStackTrace();
  23.       }
  24.        return person;
  25.   }
  26. }

来写一个测试类,对其clone方法进行测试:

  1. public class TestPerson {
  2.    public static void main(String[] args) throws CloneNotSupportedException {
  3.        Person person = new Person();
  4.        person.setAge(22);
  5.        person.setName("Java后端技术栈");
  6.        //克隆一个对象
  7.        Person clone = (Person) person.clone();
  8.        //对person的age重新赋值为25
  9.        person.setAge(25);
  10.        //对person的age重新赋值为25
  11.        person.setName("咖啡");
  12.        System.out.print(clone.getAge()+","+clone.getName());
  13.   }
  14. }

运行后将输出什么呢?先猜想一下。

具体运行结果如下:

22,Java后端技术栈






是不是觉得很神奇呢?为什么没有变化呢?

原始数据类型会被拷贝,如果从原始数据类型考虑,因为age是int类型,int是原始数据类型,所以上述场景没变,那也就无话可说,但是String并不是原始数据类型,那又是为什么呢?因为String是一个特殊类型,因为这种场景下Java希望String也看成原始类型。所以String并没有clone方法。

  1. String aa="aa";
  2. aa.clone();

String定义

  1. public final class String
  2.    implements java.io.Serializable, Comparable<String>, CharSequence {
  3.    //...省略其他
  4. }

这段代码编译通不过,提示无法访问,为什么呢?请看Object源码中对clone方法的定义

 protected native Object clone() throws CloneNotSupportedException;

String处理机制比较特殊,通过字符串池在需要的时候再内存中创建新的字符串,以后大家就在使用(clone)的时候就直接把String当做原始数据类型就行了。

深拷贝

浅拷贝是有风险的,那么如何才能深拷贝呢?我们对前面的Mybaby程序进行修改一下就成了深拷贝了;

 

  1. import java.util.ArrayList;
  2. /**
  3. * @author tianweichang
  4. * @date 2019/7/13
  5. */
  6. public class MyBaby implements Cloneable {
  7.    /**
  8.     * 私有变量
  9.     */
  10.    private ArrayList<String> list = new ArrayList<>();
  11.    @SuppressWarnings("unchecked")
  12.    @Override
  13.    protected Object clone() throws CloneNotSupportedException {
  14.        MyBaby myBaby = null;
  15.        try {
  16.            myBaby = (MyBaby) super.clone();
  17.            //增加了一个list.clone();
  18.            this.list = (ArrayList<String>) this.list.clone();
  19.       } catch (CloneNotSupportedException ex) {
  20.            ex.printStackTrace();
  21.       }
  22.        return myBaby;
  23.   }
  24.    /**
  25.     * 给List设置
  26.     *
  27.     * @param value 值
  28.     */
  29.    public void setValue(String value) {
  30.        this.list.add(value);
  31.   }
  32.    /**
  33.     * 获取list
  34.     *
  35.     * @return list
  36.     */
  37.    public ArrayList<String> getValue() {
  38.        return this.list;
  39.   }
  40. }

再次运行TestMyBaby,结果:

[Java后端技术栈]

改短代码就实现了完全的拷贝,两个对象引用指向的就不再是同一个地址了。相互之间没有什么关系了,你修改你的,我修改我的,完全不会有什么安全问题。这就是深拷贝

深拷贝还有一种实现方式:通过写自己的二进制流来操作对象,然后实现对象的深拷贝。

建议

深拷贝和浅拷贝不要混合使用,特别是在涉及到类的继承时候,父类中有多个引用的情况下就会非常复杂,建议方案是深拷贝和浅拷贝分开实现。

 

clone与final两个冤家

对象的clone与对象内的final关键字是有冲突,前者是要重新赋值,后者是赋值了就不能变了。

咱们继续帮上忙的代码进行改造:

  1. public class MyBaby implements Cloneable {
  2.    /**
  3.     * 私有变量
  4.     */
  5.    private final ArrayList<String> list = new ArrayList<>();
  6.    @SuppressWarnings("unchecked")
  7.    @Override
  8.    protected Object clone() throws CloneNotSupportedException {
  9.        MyBaby myBaby = null;
  10.        try {
  11.            myBaby = (MyBaby) super.clone();
  12.            //下面的this.list编译通不过,提示不能给final修饰的变量重新赋值
  13.            this.list = (ArrayList<String>) this.list.clone();
  14.       } catch (CloneNotSupportedException ex) {
  15.            ex.printStackTrace();
  16.       }
  17.        return myBaby;
  18.   }
  19.    /**
  20.     * 给List设置
  21.     *
  22.     * @param value 值
  23.     */
  24.    public void setValue(String value) {
  25.        this.list.add(value);
  26.   }
  27.    /**
  28.     * 获取list
  29.     *
  30.     * @return list
  31.     */
  32.    public ArrayList<String> getValue() {
  33.        return this.list;
  34.   }
  35. }

所以请注意,要使用clone方法的时候,类的成员变量上不要加final修饰



欢迎关注公众号:Java后端技术全栈