本文共 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有自动垃圾回收机制,已经很大程度避免了这种问题。如果是为了多线程、并发而考虑使用深拷贝让两个对象互不影响,我觉得这种情况下,设计一个好的不变类,比提供一个深拷贝,要更有用。不变类能够实现读操作共享,写操作按需创建新对象,比无条件的深拷贝一个对象要好很多。