Java浅拷贝和深拷贝

浅拷贝和深拷贝是在编程中很通用的概念。一般来说,对于
    B = A.copy
如果除了程序中的全局共享内存、不变对象,A和B表示的对象还存在内存共享,那么这就是浅拷贝;如果除了程序中的全局共享内存、不变对象,A和B表示的对象占用的内存是完全分开,那么这就是深拷贝。因为浅拷贝存在对象(内存)共享,其中的一个修改了共享对象,另一个也是能看到这种修改的。深拷贝不存在这种对象(内存)共享,双方都可以自己修改自己的,另一方看不到这种修改。

上面所说的全局共享内存,指的就是代码中的常量,方法的代码段,类的元信息等等,这些都是可以被任意对象使用的。不变对象,指的是java.lang.String、java.lang.Integer这种,它们的可访问field都是final的,初始了之后本身就不能再改变,程序中String对象改变都是重新生成一个String并返回,原来的还是不会变。本身程序中一些String对象是可以全局共享的,那些在代码中临时new出来的String对象,也可以看作是一种临时常量。两个对象共享String这种不变对象,双方都不能对这块共享内存进行修改,这不影响浅拷贝还是深拷贝。

另外对于Java中的值类型,它们不同于对象(引用)类型。值类型是存放在对象自己的内存中,对象创建时就已经给值类型分配空间并且赋默认值,所以两个不同对象种的值类型本身就是存放在不同的两个地方,本身就不共享,不会影响浅拷贝深拷贝。

下面通过些简单的demo看下浅拷贝和深拷贝的区别。

1、一般对象的浅复制深复制

package copy;

public class Student implements Cloneable {
    private String no;
    private String name;
    private int age;
    private Grade grade;

    public Student() {
    }

    public Student(String no, String name, int age, Grade grade) {
        this.no = no;
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
    
    // 深拷贝构造,field中可变的对象类型会重新new一个
    // 后面这个boolean没什么用,只是为了能重载构造方法
    public Student(Student s, boolean depth) {
        this.no = s.no; // String由于是一个不变类,修改String变量会把引用重新执行新的String对象,实际上不会受浅拷贝的影响
        this.no = new String(s.no); // 这句在深拷贝是表达意思更直接的一种,但是因为String类的不变特性,这句可以用上面一句替换,效率高,省空间
        this.name = s.name;
        this.age = s.age; // 基本类型的浅拷贝、深拷贝都是值复制,行为一样
        this.grade = new Grade(s.grade.getGradeId(), s.grade.getGradeName());
    }

    // 浅拷贝构造,field中可变的对象类型仅仅是引用赋值成一样的
    public Student(Student s) {
        this.no = s.no;
        this.name = s.name;
        this.age = s.age;
        this.grade = s.grade; // 可变的引用类型(可变的对象类型),浅拷贝只是把引用赋值成一样的,两个引用相等,指向同一块内存,被复制出的对象共享
    }

    public String getNo() {
        return no;
    }

    public void setNo(String no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    @Override
    protected Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

class Grade implements Cloneable {
    private int gradeId;
    private String gradeName;

    public Grade() {
    }

    public Grade(int gradeId, String gradeName) {
        this.gradeId = gradeId;
        this.gradeName = gradeName;
    }

    public int getGradeId() {
        return gradeId;
    }

    public void setGradeId(int gradeId) {
        this.gradeId = gradeId;
    }

    public String getGradeName() {
        return gradeName;
    }

    public void setGradeName(String gradeName) {
        this.gradeName = gradeName;
    }

    @Override
    protected Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}


package copy;

public class Main {
    public static void main(String[] args) {
        Student s1 = new Student();
        s1.setNo("1");
        s1.setName("first");
        s1.setAge(11);
        Grade g1 = new Grade(101, "一年级");
        s1.setGrade(g1);

        Student s2 = (Student) s1.clone();
        System.err.println("s1 和 s2");
        compare(s1, s2);

        Student s3 = new Student(s1);
        System.err.println("s1 和 s3");
        compare(s1, s3);

        Student s4 = new Student(s1, true);
        System.err.println("s1 和 s4");
        compare(s1, s4);

        System.err.println("-------------------------");
        // 修改原对象中某个引用类型自身的内存,看下影响
        s1.getGrade().setGradeId(202);
        System.err.println("s1.grade.gradeId: " + s1.getGrade().getGradeId());
        System.err.println("s2.grade.gradeId: " + s2.getGrade().getGradeId());
        System.err.println("s3.grade.gradeId: " + s3.getGrade().getGradeId());
        System.err.println("s4.grade.gradeId: " + s4.getGrade().getGradeId());
    }

    /**
     * 打印原对象和拷贝对象的各个field
     */
    private static void compare(Student s, Student sCopy) {
        System.err.println("s == sCopy: " + (s == sCopy));
        System.err.println("s1.no == sCopy.no: " + (s.getNo() == sCopy.getNo()));
        System.err.println("s1.name == sCopy.name: " + (s.getName() == sCopy.getName()));
        System.err.println("s1.age == sCopy.age: " + (s.getAge() == sCopy.getAge()));
        System.err.println("s1.grade == sCopy.grade: " + (s.getGrade() == sCopy.getGrade()));
        System.err.println("s1.grade.gradeId == sCopy.grade.gradeId: "
                + (s.getGrade().getGradeId() == sCopy.getGrade().getGradeId()));
        System.err.println("s1.grade.gradeName == sCopy.grade.gradeName: "
                + (s.getGrade().getGradeName() == sCopy.getGrade().getGradeName()));
        System.err.println();
    }
}

这个demo很简单,就是通过不同的方式分别拷贝出一个对象,先比较下拷贝前后对象各个field,再修改下原对象,看下对拷贝出来的对象的影响。运行结果如下:



代码的前面部分是通过不同的方法进行对象拷贝,这些拷贝的执行结果如下图。


运行结果很简单明了,这里也简单说下。

s2是s1通过Object.clone()拷贝出来的对象,s1和s2的grade这个属性的对象是共享的,用的是同一块内存区域,s2是s1的一个浅拷贝,所以s1.grade == s2.grade 为 true;

s3是s1通过常见的一种构造方法生成的对象,grade属性直接用引用赋值,s3和s1的grade属性也是共享一块内存,s3是s1的一个浅拷贝,所以s1.grade == s3.grade 为 true;

s4是s1通过另一个构造方法生成的对象,grade属性是重新new一个然后赋值为同s1.grade中对应属性一样的值(String对象前面说了,不影响浅拷贝深拷贝),s4和s1的grade属性的对象是不共享的,用的是不同的内存,s4是s1的一个深拷贝,所以s1.grade == s4.grade 为 false。

代码最后面把s1.grade.gradeId修改了,这个修改影响到了s2和s3,对s4没影响,这一点也验证了浅拷贝和深拷贝的性质。


2、对象包含数组

下面这个demo测试对象包含数组的情况

package copy;

public class A {
    private String uin;
    private int[] intArray; // 默认null
    private Student[] students; // 默认null

    public A() {
    }

    // 通过System.arraycopy复制数组
    public A(A a) {
        this.uin = a.uin;
        this.intArray = new int[a.intArray.length];
        System.arraycopy(a.intArray, 0, this.intArray, 0, a.intArray.length);
        this.students = new Student[a.students.length];
        System.arraycopy(a.students, 0, this.students, 0, a.students.length);

        // System.err.println(Arrays.toString(this.intArray));
        // System.err.println(Arrays.toString(this.students));
    }

    // 通过数组的clone方法复制数组,int参数没什么用
    public A(A a, int clone) {
        this.uin = a.uin;
        this.intArray = a.intArray.clone();
        this.students = a.students.clone();

        // System.err.println(Arrays.toString(this.intArray));
        // System.err.println(Arrays.toString(this.students));
    }

    // for循环一个个new,boolean参数没什么用
    public A(A a, boolean another) {
        this.uin = a.uin;
        intArray = new int[a.intArray.length];
        // 因为是基本类型的数组,也可以使用上面的方法的那种方式
        for (int i = 0; i < a.intArray.length; i++) {
            intArray[i] = a.intArray[i];
        }
        students = new Student[a.students.length];
        // 对象类型(引用类型)的数组,跟上面的方法的那种方式有区别
        for (int i = 0; i < a.students.length; i++) {
            this.students[i] = new Student(a.students[i], true); // 这个方法前面说了,是个深拷贝
        }

        // System.err.println(Arrays.toString(this.intArray));
        // System.err.println(Arrays.toString(this.students));
    }

    public String getUin() {
        return uin;
    }

    public void setUin(String uin) {
        this.uin = uin;
    }

    public int[] getIntArray() {
        return intArray;
    }

    public void setIntArray(int[] intArray) {
        this.intArray = intArray;
    }

    public Student[] getStudents() {
        return students;
    }

    public void setStudents(Student[] students) {
        this.students = students;
    }
}
package copy;

import java.util.Arrays;

public class ArrayDemoMain {
    public static void main(String[] args) {
        A a1 = new A();
        a1.setUin("aaa");
        int[] ints = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        a1.setIntArray(ints);
        Grade[] grades = { new Grade(101, "一年级"), new Grade(102, "二年级"), new Grade(103, "三年级") };
        Student[] students = { new Student("1", "first", 11, grades[0]), new Student("2", "second", 22, grades[1]),
                new Student("3", "second", 33, grades[2]) };
        a1.setStudents(students);

        A a2 = new A(a1); // system.arraycopy
        System.err.println("a1 和 a2");
        compare(a1, a2);

        A a3 = new A(a1, 1); // Array.arraycopy
        System.err.println("a1 和 a3");
        compare(a1, a3);

        A a4 = new A(a1, true); // for循环一个个new
        System.err.println("a1 和 a4");
        compare(a1, a4);

        System.err.println("-------------------------");
        a1.getIntArray()[0] = 2222;
        System.err.println("a1.intArray: " + Arrays.toString(a1.getIntArray()));
        System.err.println("a2.intArray: " + Arrays.toString(a2.getIntArray()));
        System.err.println("a3.intArray: " + Arrays.toString(a3.getIntArray()));
        System.err.println("a4.intArray: " + Arrays.toString(a4.getIntArray()));

        System.err.println("-------------------------");
        a1.getStudents()[0].setAge(3333);
        System.err.println("a1.students.0.age: " + a1.getStudents()[0].getAge());
        System.err.println("a2.students.0.age: " + a2.getStudents()[0].getAge());
        System.err.println("a3.students.0.age: " + a3.getStudents()[0].getAge());
        System.err.println("a4.students.0.age: " + a4.getStudents()[0].getAge());
    }

    private static void compare(A a, A aCopy) {
        System.err.println("a == aCopy: " + (a == aCopy));
        System.err.println("a.uin == aCopy.uin:" + (a.getUin() == aCopy.getUin()));
        System.err.println("a.intArray == aCopy.intArray: " + (a.getIntArray() == aCopy.getIntArray()));
        boolean isSame = true;
        for (int i = 0; i < a.getIntArray().length; i++) {
            if (a.getIntArray()[i] != aCopy.getIntArray()[i]) {
                // 数组内容对应不相同
                isSame = false;
                System.err.printf("\ta.intArray.%d != aCopy.intArray.%d\n", i);
            }
        }
        if (isSame) {
            // 数组内容对应相同
            System.err.println("\ta.intArray's real content is the same as aCopy.intArray's real content");
        }
        isSame = true;
        System.err.println("a.students == aCopy.students: " + (a.getStudents() == aCopy.getStudents()));
        for (int i = 0; i < a.getStudents().length; i++) {
            if (a.getStudents()[i] != aCopy.getStudents()[i]) {
                isSame = false;
                System.err.printf("\ta.students.%d != aCopy.students.%d\n", i, i);
            }
        }
        if (isSame) {
            // 数组内容对应相同
            System.err.println("\ta.students's real content is the same as aCopy.students's real content");
        }
        System.err.println();
    }
}

上面代码运行结果如下:


Java中数组也是对象,并且Java中对象都是引用类型所以对象数组,数组里面存的是个引用地址,实际对象存储在别的地方。java里面的clone方法以及Systemarraycopy方法,都只是用值复制的方式把数组存的引用地址复制一遍,实际指向的对象并不复制,会处于共享状态,这两种用于拷贝对象数组都无法达到深拷贝一个数组的目的。要深拷贝一个数组,只能用a4的方式,一个个for循环,并且很重要的一点,对象本身要提供一个深拷贝方法。如果对象本身没有方法深拷贝,那么它的数组也基本上无法真正深拷贝。


关于Array.clone和System.arraycopy到底用哪个?

在由整个数组复制出一个新数组时,两个基本没区别,速度也差不多。System.arraycopy最大的优势就是能够进行数组的部分复制,并且可以自己的一部分复制到自己上面,它能够控制的东西更多,功能更强大,更灵活,所以大多数数组复制都是用System.arraycopy或者它的封装方法Arrays.copyOf等等。


ArrayList是使用数组实现的List它的,clone方法源码如下:

/**
 * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
 * elements themselves are not copied.)
 *
 * @return a clone of this <tt>ArrayList</tt> instance
 */
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}
注释上写了,此为浅拷贝,数组有两份,但是不拷贝实际的元素,实际的元素还是共享的。

作为一个泛用的集合类,没必要提供深拷贝方法,ArrayList根本不知道如何深拷贝它的某一个元素。Object.clone方法是一个实际中可选的方法(子类要实现Cloneable接口并且覆盖此方法才能给外面用),不是一个类的必要行为,而且clone是浅拷贝的,要如何深拷贝一个类,实际上往往只有真正写代码的人才知道。在C/C++中区分浅拷贝深拷贝,很大一部分是为了避免释放掉了和别的对象共享的内存造成的问题。Java有自动垃圾回收机制,已经很大程度避免了这种问题。如果是为了多线程、并发而考虑使用深拷贝让两个对象互不影响,我觉得这种情况下,设计一个好的不变类,比提供一个深拷贝,要更有用。不变类能够实现读操作共享,写操作按需创建新对象,比无条件的深拷贝一个对象要好很多。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值