博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java浅拷贝和深拷贝
阅读量:4297 次
发布时间:2019-05-27

本文共 11887 字,大约阅读时间需要 39 分钟。

浅拷贝和深拷贝是在编程中很通用的概念。一般来说,对于
   
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 ArrayList instance.  (The * elements themselves are not copied.) * * @return a clone of this ArrayList 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有自动垃圾回收机制,已经很大程度避免了这种问题。如果是为了多线程、并发而考虑使用深拷贝让两个对象互不影响,我觉得这种情况下,设计一个好的不变类,比提供一个深拷贝,要更有用。不变类能够实现读操作共享,写操作按需创建新对象,比无条件的深拷贝一个对象要好很多。

你可能感兴趣的文章
Lock重入锁
查看>>
docker安装 rabbitMq
查看>>
git 常用命令 入门
查看>>
linux安装docker
查看>>
关闭selinx nginx无法使用代理
查看>>
shell 脚本部署项目
查看>>
spring cloud zuul网关上传大文件
查看>>
springboot+mybatis日志显示SQL
查看>>
工作流中文乱码问题解决
查看>>
maven打包本地依赖包
查看>>
spring boot jpa 实现拦截器
查看>>
jenkins + maven+ gitlab 自动化部署
查看>>
Pull Request流程
查看>>
Lambda 表达式
查看>>
函数式数据处理(一)--流
查看>>
java 流使用
查看>>
java 用流收集数据
查看>>
java并行流
查看>>
CompletableFuture 组合式异步编程
查看>>
mysql查询某一个字段是否包含中文字符
查看>>