Java中二维数组的复制(深拷贝与浅拷贝)

最近在做一道编程题的时候出现了一处bug。在这道题需要使用多种方法对同一个二维数组进行更改,找出使第二列数之和最大的更改方法。所以每次修改之前,我对原数组进行拷贝,修改的是数组的拷贝。可是程序写完之后,运行结果却出乎意料的是错误的。看了几遍程序流程,觉得逻辑上没有错误。于是打开debug模式一步步看数值变化。调试时发现在第二次循环中,数组本该为0的位置,值却被改成了1,而这是第一次进行修改的结果。此时顿悟,想起很久之前学过的深拷贝和浅拷贝,绝对是clone()的问题。

我们重现一下当时的问题,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class cloneTeset {
public static void main(String[] args) {
int a[][] = {{0,0,0},{1,1,1},{2,2,2}};
int b[][] = a.clone();
b[1][1]=3;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
System.out.print(a[i][j]+" ");
}
System.out.print(" ");
}
System.out.println();
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
System.out.print(b[i][j]+" ");
}
System.out.print(" ");
}
}
}

输出结果是

1
2
0 0 0  1 3 1  2 2 2  
0 0 0 1 3 1 2 2 2

可以发现,对a的拷贝b进行修改后,原数组a的值也被修改了。这是为什么呢?

首先我们知道,虽然数组的声明方式与其他对象有所不同,但它也是new出来的。在Java中,数组是一个对象。

查看官方文档,在Object的Clone()方法的描述中有这么两段话:

By convention, the object returned by this method should be independent of this object (which is being cloned). To achieve this independence, it may be necessary to modify one or more fields of the object returned by super.clone before returning it. Typically, this means copying any mutable objects that comprise the internal “deep structure” of the object being cloned and replacing the references to these objects with references to the copies. If a class contains only primitive fields or references to immutable objects, then it is usually the case that no fields in the object returned by super.clone need to be modified.
一般来说,我们希望cloen()方法返回的对象相是独立于被克隆对象的,所以我们在返回clone方法前,应该对克隆出的对象的域进行一些修改。这通常意味着,复制被克隆对象所包含的所有内部深层结构的可变对象,并且将域中的引用,指向这些复制出的对象。如果一个类只包含基础类型或者指向不可变对象的引用,那么将不用修改Super.clone()的返回结果。

注:不可变对象有哪些?
String对象都是不可变对象。
原始类型包装类(primitive wrappers)(Integer,Long, Short, Double, Float, Character, Byte, Boolean)也都是不可变的。
关键字final用于声明基本类型和对象引用为不可变对象,但是它不能使对象本身变为不可变对象。

我们再看下一段:

The method clone for class Object performs a specific cloning operation. First, if the class of this object does not implement the interface Cloneable, then a CloneNotSupportedException is thrown. Note that all arrays are considered to implement the interface Cloneable and that the return type of the clone method of an array type T[] is T[] where T is any reference or primitive type. Otherwise, this method creates a new instance of the class of this object and initializes all its fields with exactly the contents of the corresponding fields of this object, as if by assignment; the contents of the fields are not themselves cloned. Thus, this method performs a “shallow copy” of this object, not a “deep copy” operation. Thus, this method performs a “shallow copy” of this object, not a “deep copy” operation.
clone()方法首先会判断一个对象是否实现了Cloneable接口,如果没有实现,则会抛出CloneNotSupportedException。而所有的数组都默认实现了Cloneable接口。如果对数组T[]进行克隆,返回结果是T[],T是引用或者基本类型。如果对对象进行克隆,首先会新建一个这个类的实例,然后根据这个对象中域的内容来对新建的实例进行初始化(赋值操作)。因此clone()这个方法实现了浅拷贝,并不是深拷贝。

从以上两段话我们可以看出:
clone( )方法创建调用它的对象的一个复制副本。只有那些实现Cloneable接口的类能被复制。
如果对数组T[]进行克隆,返回结果是T[],T是引用或者基本类型。
我们克隆的数组是二维数组,即数组的数组。对于clone()方法来说,这里的T[],T是一个对数组的引用。
也就是说:我们克隆出来的数组的第一维度,和原来那个数组的第一维度。同时指向了同一个第二位度的数组。
如下图:

因此当改变数组b的时候,数组a也被改变。此时的引用关系图如下:

所以,为了实现二维数组的克隆,我们需要分别对第二维度的每一个数组进行克隆。
实现如下:

1
2
3
4
int b[][] = new int[a.length][];
for(int i=0;i<a.length;i++){
b[i]=a[i].clone();
}

将之前代码的复制部分改为以上实现,输出结果如下:

1
2
0 0 0  1 1 1  2 2 2  
0 0 0 1 3 1 2 2 2

可以看出使用这种方法,实现了深拷贝,两个数组相互独立,不会互相影响。
同样的,Java提供的Arrays.copyof()方法对于二维数组也是浅拷贝,也需要我们为数组中的每一项进行copy。
此时的引用关系如下图: