八、数组

1 什么是数组

在 Java 语言中我们可以定义如 int、char、float 等多种类型的变量,但是这样的变量当中只能存放一个数据,当我们需要存储大量数据时就显得比较麻烦。比如我们要存储全校 1200 名学生的成绩时,用这种方法就得定义 1200 个变量,这个工作量也太大了。

幸好 Java 语言给我们提供了 “数组(array)”,当需要保存大量数据时就可以利用 “数组” 来处理。数组可以存储一组具有相同数据类型的值,使它们形成一个小组,可以把它们作为一个整体处理,同时又可以区分小组内的每一个数值。就好像你把 10 颗糖果放入一个袋子中,你可以把这 10 颗糖果作为一个整体——一袋糖果;也可以从袋子中掏出一颗糖吃掉。

数组实际上是把多个具有相同类型的变量按顺序排列在一起而形成的一个组合。前面我们把变量想象成是单身小房子,那么数组就可以想象成是拥有许多相同小房子的一排房子。数组中存储着的每一个变量,我们称之为 “元素(element)

数组是把相同类型的变量顺序直线排列的结果.gif每个元素都可以通过其数字索引(index)进行访问。如上图所示,编号从 0 开始,因此访问第 1 个元素的索引为 0。

数组中存储的并不是某个具体的数据,而是很多数据的组合,因此,数组是引用数据类型

2 声明变量以引用数组

数组和普通的变量一样,在 Java 语言中必须先定义(称作声明数组)才能使用。

数组的声明和变量的定义是一样的,需要指定数据类型,并取一个唯一的名字(数组名)。那么数组的数据类型是什么呢?很简单,数组是存储确定数量某一确定类型数据的容器,那么数组的数据类型,其实是与其能存储的数据的数据类型强相关的,因此,数组数据类型为其能够存储的数据类型后面跟上方括号 [] 。括号是特殊符号,表明该变量包含一个数组。数组的声明语法如下:

// 语法:能够存储的数据类型[] 数组名;
// 例:
int[] nums;

方括号 [] 也可以放在数组名后,但方括号是用来表明类型的,而不是数组名的一部分,因此,不推荐这样写。

// 不推荐的声明方式
int nums[];

3 创建、初始化和访问数组

3.1 创建数组

对于数组等引用数据类型来说,声明实际上并不创建数组,它只是告诉编译器该变量将保存指定类型的数组。此时,如果我们尝试访问这个数组,我们会得到来自编译器的报错:Variable anArray may not have been initialized.(变量 nums 可能尚未初始化。)

创建数组的一种方法是使用new运算符。下面的语句将会创建一个具有足够空间容纳 3 个整数元素的数组,并将该数组分配给变量 nums

nums = new int[3];

方括号中的数字,确定了数组存储指定类型数据的最大数量,我们称之为 “数组长度(length),如果我们尝试使用更多数据,我们会得到报错:数组索引超出范围。

3.2 声明的同时创建数组

大多数时候,我们在声明数组后马上就需要创建这个数组以使用这个数组, Java 语言考虑到了这一点,声明与创建数组,可以在一条语句中完成。

// 声明的同时创建数组
int[] nums = new int[3];

3.3 初始化数组

数组被创建后,其中的数组元素的值是数组元素数据类型的默认值,我们使用我们希望存储的数据,初始化数组元素的过程,被称为 “初始化数组(元素)” 。

我们可以使用几行代码,为每一个元素逐一初始化:

nums[0] = 1;
nums[1] = 2;
nums[2] = 3;

我们可以输出每个元素的值,来检验我们初始化的成果:

System.out.println("索引 0 处的元素 1: " + nums[0]);
System.out.println("索引 1 处的元素 2: " + nums[1]);
System.out.println("索引 2 处的元素 3: " + nums[2]);

3.4 声明的同时创建和初始化数组

在一些情况下,我们声明数组时,很清楚里面应该存储哪些具体数据,我们就可以把声明、创建和初始化数组的操作,放在一条语句中:

int[] nums = {1, 2, 3};

数组长度为大括号 {} 中数据的个数。

4 数组的复制

4.1 浅复制

数组是引用数据类型,我们说过,引用数据类型的变量中,存储的并不是具体的数据,而是存储这些数据所在的地址。

那现在,让我们思考一下,下面这段代码运行时发生了什么:

int[] nums1 = {1, 2, 3};
int[] nums2 = nums1;
System.out.println("num2 的三个元素:" + nums2[0] + nums2[1] + nums2[2]);

输出:num2 的三个元素:123

从结果我们知道,数组 nums2 中的元素值,与数组 nums1 中的元素值相同,我们似乎完成了对数组 nums1 的复制。但真的是这样吗?

现在,让我们把 num2 的第三个元素变成 6 ,看看这会为两个数组带来怎样的影响:

int[] nums1 = {1, 2, 3};
int[] nums2 = nums1;
nums2[2] = 6;
System.out.println("muns1 的第三个元素: " + nums1[2]);
System.out.println("muns2 的第三个元素: " + nums2[2]);

输出:

mun1 的第三个元素: 6

mun2 的第三个元素: 6

从结果可以看出,我们对 nums2 的元素的修改,也对 nums1 的对应元素产生了影响。实际上,int[] nums2 = nums1; 所进行的操作,是将 nums1 中存储的地址复制给了 nums2 ,两个数组变量所代表的实际上是同一个数组。这也是使用赋值符号 = 连接两个引用数据类型变量时会发生的事,这被称为 “浅复制”。

4.2 深复制

既然有 “浅复制” ,那应该会有 “深复制” ,实际上也是如此。

数组的 “深复制” 就是创建一个新的数组,把原数组的元素,一个个复制到新数组中。我们可以手动完成这项操作:

int[] nums1 = {1, 2, 3};
int[] nums2 = new int[3];
nums2[0] = nums1[0];
nums2[1] = nums1[1];
nums2[2] = nums1[2];
System.out.println("num2 的三个元素:" + nums2[0] + nums2[1] + nums2[2]);
nums2[2] = 6;
System.out.println("mun1 的第三个元素: " + nums1[2]);
System.out.println("mun2 的第三个元素: " + nums2[2]);

输出:

num2 的三个元素:123

mun1 的第三个元素: 3

mun2 的第三个元素: 6

“深复制” 在编程中是常用操作,Java 为我们提供了一个 arraycopy 方法,可以用来将数据从一个数组有效地复制到另一个数组中:

public static void arraycopy(Object src, int srcPos,
                             Object dest, int destPos, int length)

两个 Object 参数指定了要从哪个数组复制,以及要复制到哪个数组。三个 int 参数分别指定源数组的起始位置、目标数组的起始位置以及要复制的数组元素个数。

下面的程序使用 System.arraycopy 方法将数组 nums1 中的,从索引为 2 的元素开始的,3 个元素子序列,复制到数组 nums2 的,从索引为 0 开始的位置中:

public static void main(String[] args) {
    int[] nums1 = {1, 2, 3, 4, 5};
    int[] nums2 = new int[3];
    System.arraycopy(nums1, 2, nums2, 0, 3);
    System.out.println("num2 的三个元素:" + nums2[0] + nums2[1] + nums2[2]);
}

输出:num2 的三个元素:345

从这个方法的作用,可以看出这个方法适合在目标数组原本就有数据时,用源数组的数据进行覆盖。

Java 还提供了另一个数组复制方法 java.util.Arrays.copyOfRange ,用于直接创建一个新数组。下面的代码功能与上面的代码相同:

public static void main(String[] args) {
    int[] nums1 = {1, 2, 3, 4, 5};
    // 语法:目标数组名 = java.util.Arrays.copyOfRange(源数组, 复制起始的索引(含), 复制终止的索引(不含));
    int[] nums2 = java.util.Arrays.copyOfRange(nums1,2,5);
    System.out.println("num2 的三个元素:" + nums2[0] + nums2[1] + nums2[2]);
}

5 判断数组相同

我们首先来看一段代码:

int[] nums1 = {1, 2, 3, 4, 5};
int[] nums2 = {1, 2, 3, 4, 5};
System.out.println(nums1 == nums2);

输出:false

这是怎么回事?明明两个数组的内容完全相同。

这是因为数组是引用数据类型,数组变量中直接存储的是数组元素们的地址,而这两个数组的地址是不同的。

如果我们需要判断两个数组的内容是否相同,我们可以手动比较两个数组中的每个元素,然后使用 && 进行综合判断:

nums1[0] == nums2[0] && nums1[1] == nums2[1] && ... &&  nums1[4] == nums2[4] 

很明显,这非常麻烦,如果数组长度是几十个呢?麻烦程度简直不可想象。

Java 中提供了 java.util.Arrays.equals 方法,来完成判断数组内容是否相同:

int[] nums1 = {1, 2, 3, 4, 5};
int[] nums2 = {1, 2, 3, 4, 5};
System.out.println(Arrays.equals(nums1, nums2));

输出:true

6 展示所有数组元素

java.util.Arrays 类中的方法还提供了其他一些有用的操作。比如toString 方法,将数组的每个元素转换为字符串,用逗号分隔,然后用括号包围:

System.out.println(java.util.Arrays.toString(nums1));

输出:[1, 2, 3, 4, 5]

7 二维数组

我们想这样一种情境:我们可以使用一个数组存储一名学生的几门课程的成绩,而一个班中有很多学生,我们如何存储这些信息呢?

答案很明显,我们需要一个以数组为元素的数组,也可以叫做 “数组的数组” ,这就是 “二维数组” 。

下面的代码,可以创建一个二维数组:

int[][] ns = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
System.out.println(ns.length); // 3

它的结构如图:

二维数组1.jpg因为存储的元素是数组,所以我们可以把二维数组的元素,赋值给一维数组。

int[][] ns = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
int[] arr0 = ns[0];
System.out.println(arr0.length); // 4

二维数组的每个数组元素的长度并不要求相同。

int[][] ns = {
    { 1, 2, 3, 4 },
    { 5, 6 },
    { 7, 8, 9 }
};

它的结构如图:

二维数组2.jpg至此,我们知道了,二维数组,只是元素为数组的数组而已,没什么难的。继续推广,就可以得到 “三维数组”、“四维数组”等多维数组。