这两者都算是java基础中的基础,平常写代码可能并没有过多的去深究它,但这样容易引发一些不可预知的BUG。
这里有一个简单的类,文章中会提到多次。
一个学生类,它有两个属性,String类型的name与Integer类型的age。
public class Student { private String name; private Integer age; public Student() { } public Student(String name, Integer age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}
首先我们有一个List。
ArrayListoriginalList = new ArrayList(); list1.add(new Student("五更琉璃", 15)); list1.add(new Student("高坂桐乃", 14));
1、先说说最简单粗暴的复制。
ArrayListcopyList = new ArrayList<>(); copyList = originalList; copyList.set(1,new Student("土间埋",16));
copyList直接获得originalList的引用
originalList ==>Student{name='五更琉璃', age=15}Student{name='土间埋', age=16}copyList ==>Student{name='五更琉璃', age=15}Student{name='土间埋', age=16}
结果如下,我们发现,即使只改变了copyList的元素element,原来的ArrayList也跟着变了。
originalList = {ArrayList@531} size = 2 0 = {Student@533} "Student{name='五更琉璃', age=15}" 1 = {Student@534} "Student{name='土间埋', age=16}"copyList = {ArrayList@531} size = 2 0 = {Student@533} "Student{name='五更琉璃', age=15}" 1 = {Student@534} "Student{name='土间埋', age=16}"
打个断点我们可以发现,无论是ArrayList,还是里面的引用,它们的内存地址是完全一样的。
也就是说,直接赋值 copyList = originalList;的这种方法,很难称得上是一种复制。
2、使用clone方法进行复制
ArrayListcopyList = (ArrayList ) originalList.clone(); copyList.set(1,new Student("土间埋",16));
结果如下:看起来正常了,我们的originalList并没有因为copyList的set方法而改变。
originalList ==>Student{name='五更琉璃', age=15}Student{name='高坂桐乃', age=14}copyList ==>Student{name='五更琉璃', age=15}Student{name='土间埋', age=16}
然而打开断点会发现,不是那么一回事。
我们发现,originalList和copyList的指向确实不同了。 然而List中的元素,指向的还是同一块地址。
originalList = {ArrayList@531} size = 2 0 = {Student@534} "Student{name='五更琉璃', age=15}" 1 = {Student@535} "Student{name='高坂桐乃', age=14}"copyList = {ArrayList@532} size = 2 0 = {Student@534} "Student{name='五更琉璃', age=15}" 1 = {Student@539} "Student{name='土间埋', age=16}"
也就是说,如果我们不进行set重新修改元素的指向,而是直接改变元素内的属性。
ArrayListcopyList = (ArrayList ) originalList.clone(); copyList.get(1).setName("土间埋"); copyList.get(1).setAge(16);
从断点中我们可以得到我们的预期。originalList 中的第一个元素,也会被改变。
originalList = {ArrayList@463} size = 2 0 = {Student@466} "Student{name='五更琉璃', age=15}" 1 = {Student@467} "Student{name='土间埋', age=16}"copyList = {ArrayList@464} size = 2 0 = {Student@466} "Student{name='五更琉璃', age=15}" 1 = {Student@467} "Student{name='土间埋', age=16}"
从这里可以看出,ArrayList提供的clone方法,实际上是一种浅复制。
也就是它不是一种递归复制。
它只是改变了顶层,copyList 的引用。
我们看看源码
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); } }
源码中,先是clone了一个新的ArrayList:v
然后将原ArrayList中的数据直接复制到了新的ArrayList中。
再看看ArrayList中的add与addAll方法,实际上也是如出一辙。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public boolean addAll(Collection c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }
它们只是把元素的引用复制了一遍。也就是说调用clone、add、addAll方法,当改变元素中的属性时,新List中的元素也会跟着改变。
java的参数传递也是如此,java中,参数的传递都是值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。
当这个副本的属性在方法内部被改变时,这个副本的正本也会被改变。
那么就针对当前业务来说,如何实现深克隆?
ArrayListcopyList = new ArrayList<>(); for (Student student : originalList) { copyList.add(student.clone()); } copyList.get(1).setName("土间埋"); copyList.get(1).setAge(16);
粗暴实现:我们发现它们的地址已经完全不同了。
originalList = {ArrayList@464} size = 2 0 = {Student@467} "Student{name='五更琉璃', age=15}" 1 = {Student@468} "Student{name='高坂桐乃', age=14}"copyList = {ArrayList@465} size = 2 0 = {Student@472} "Student{name='五更琉璃', age=15}" 1 = {Student@473} "Student{name='土间埋', age=16}"