DEV Community

Homlander
Homlander

Posted on

Java 数组的定义与使用

1. 用一个数组管理一组数据

数组用于保存一组相同类型的数据。例如,保存 5 个学生的成绩,不必分别定义 5 个变量:

int[] scores = {70, 80, 85, 60, 90};
Enter fullscreen mode Exit fullscreen mode

这里,int[] 是数组类型,scores 是数组变量,数组中有 5 个 int 元素。

可以把数组理解为一排连续编号的格子:每个格子保存一个元素,通过编号直接访问。这个编号叫作下标(索引),从 0 开始。

下标:  0   1   2   3   4
元素: 70  80  85  60  90
Enter fullscreen mode Exit fullscreen mode

一个数组创建后,长度就固定了;元素的值可以修改。

1.1 创建与初始化

动态初始化:指定长度,元素先使用默认值。

int[] numbers = new int[3];        // 3 个 int 元素,初始值都是 0
double[] prices = new double[5];   // 5 个 double 元素
String[] words = new String[3];    // 3 个 String 元素
Enter fullscreen mode Exit fullscreen mode

通用格式是 元素类型[] 数组名 = new 元素类型[长度];。new 在这里用于创建数组。

“动态初始化”并不意味着长度可以随意增长,而是创建时指定长度。

静态初始化:直接给出元素,长度由元素个数决定。

int[] a = new int[]{10, 20, 30};
int[] b = {10, 20, 30};            // 声明时可以简写
String[] words = {"Hello", "Java"};
Enter fullscreen mode Exit fullscreen mode

元素值必须能赋给数组的元素类型。指定元素时,不再在方括号中填写长度:

// 错误:不能同时指定长度和初始化列表
// int[] a = new int[3]{10, 20, 30};
Enter fullscreen mode Exit fullscreen mode

声明和赋值可以分开,但分开后不能只写 {...}:

int[] a;
a = new int[3];
a = new int[]{10, 20, 30};

// 错误:这种简写只能在声明时使用
// a = {10, 20, 30};
Enter fullscreen mode Exit fullscreen mode

int a[] 也是合法写法,但推荐 int[] a,更容易看出变量的类型是 int[]。

1.2 数组元素的默认值

使用 new 类型[长度] 创建数组后,即使没有逐个赋值,元素也已经有默认值:

元素类型 默认值
byte、short、int、long 0
float 0.0f
double 0.0
char '\u0000',不是字符 '0',也不是空格
boolean false
引用类型,例如 String null,表示没有引用对象

注意区分:int[] a; 只是声明了局部变量,没有创建数组,也不能直接使用这个尚未赋值的变量;int[] a = new int[3]; 才创建了包含 3 个零的数组。

1.3 访问元素与获取长度

int[] a = {10, 20, 30};
System.out.println(a[0]);      // 10:读取第一个元素
a[1] = 99;                    // 修改第二个元素
System.out.println(a[1]);      // 99
System.out.println(a.length); // 3
Enter fullscreen mode Exit fullscreen mode
  • a.length 是元素个数,没有括号。
  • 合法下标为 0 到 a.length - 1。
  • 最后一个元素是 a[a.length - 1],不是 a[a.length]。

对于上面的数组,访问 a[3] 或 a[-1] 都会发生 ArrayIndexOutOfBoundsException(数组下标越界异常)。异常可以先理解为程序运行时报告的错误。

1.4 遍历数组

遍历就是依次访问数组中的所有元素。

需要下标或需要修改元素时,用普通 for 循环:

int[] a = {10, 20, 30};
for (int i = 0; i < a.length; i++) {
    a[i] += 1;
    System.out.println(a[i]); // 依次输出 11、21、31
}
Enter fullscreen mode Exit fullscreen mode

条件写成 i < a.length,不能写成 i <= a.length;使用 a.length 也比把长度写死更方便。

只需要依次读取元素时,可以用增强 for(for-each):

int[] a = {10, 20, 30};
for (int x : a) {
    System.out.println(x);
}
Enter fullscreen mode Exit fullscreen mode

每次循环,x 都取得当前元素的值,它不是下标。对于这里的 int 数组,给 x 赋值只会改变循环变量,不会修改数组元素;要修改元素,应使用 a[i]。

1.5 一次打印整个数组

Arrays 是 java.util 包中的数组工具类,提供了操作数组的方法。使用前在 Java 文件的类定义上方写:

import java.util.Arrays;
Enter fullscreen mode Exit fullscreen mode

Arrays.toString(a) 把一维数组的内容转换成便于阅读的字符串:

int[] a = {10, 20, 30};
String text = Arrays.toString(a);
System.out.println(text); // [10, 20, 30]
Enter fullscreen mode Exit fullscreen mode

直接 System.out.println(a) 不会按上面的形式输出数组元素。

2. 数组变量保存的是引用

2.1 先分清“变量”和“数组对象”

int number = 10;
int[] a = {1, 2, 3};
Enter fullscreen mode Exit fullscreen mode
  • number 是基本类型变量,直接保存数值 10。
  • a 是引用类型变量,保存的是数组对象的引用,不是三个元素本身。

引用可以暂时理解为找到对象的“地址线索”,不必把它当成可直接操作的真实内存地址。

number 中保存:10
a 中保存的引用 ──→ 数组对象 [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

JVM(Java 虚拟机)负责运行 Java 程序,其运行时内存按用途划分:

区域 本章需要知道的作用
虚拟机栈 方法调用时创建栈帧,保存局部变量等执行信息;方法结束时对应栈帧出栈
堆 存放数组等对象
程序计数器 记录当前线程执行字节码的位置
本地方法栈 为本地方法服务;本地方法是由 Java 以外的语言实现的方法
方法区 保存类信息、方法字节码等数据

本章重点是栈和堆。 按这里的入门模型,方法内的局部变量 number、a 位于该方法的栈帧中,而 a 引用的数组对象位于堆中。其他区域的内部细节暂不展开。

2.2 数组赋值不会复制元素

int[] a = {1, 2, 3};
int[] b = a;
b[0] = 99;

System.out.println(a[0]); // 99
Enter fullscreen mode Exit fullscreen mode

b = a 复制的是引用,因此两个变量指向同一个数组:

a ──┐
    ├──→ [99, 2, 3]
b ──┘
Enter fullscreen mode Exit fullscreen mode

通过 a 或 b 修改元素,改的都是同一个数组对象。

但重新给引用变量赋值,只会改变这个变量的指向:

b = new int[]{7, 8};
System.out.println(a[0]);     // 99:a 仍指向原数组
System.out.println(b[0]);     // 7:b 指向新数组
System.out.println(a.length); // 3
System.out.println(b.length); // 2
Enter fullscreen mode Exit fullscreen mode

这里不是原数组长度从 3 变成了 2,而是 b 改为引用另一个数组。

2.3 null:目前没有引用任何数组

int[] a = null;
// System.out.println(a[0]);     // 空指针异常
// System.out.println(a.length); // 空指针异常
Enter fullscreen mode Exit fullscreen mode

null 表示空引用。此时没有可访问的数组对象,读取元素或长度会发生 NullPointerException(空指针异常)。null 也不等于数值 0。

要区分下面两种情况:

写法 含义
int[] a = null; 没有引用数组,不能读取 a.length
int[] a = new int[0]; 引用了长度为 0 的数组,a.length 为 0,但没有可访问的元素

3. 数组与方法配合使用

下面的 public static 方法定义在类中,与 main 并列;调用语句放在 main 等方法内部。

3.1 作为参数:传入的是引用的副本

Java 的参数传递都是值传递:基本类型复制数值,引用类型复制引用值。

先看基本类型:

public static void changeNumber(int x) {
    x = 10;
}
Enter fullscreen mode Exit fullscreen mode
int number = 0;
changeNumber(number);
System.out.println(number); // 0
Enter fullscreen mode Exit fullscreen mode

形参 x 和变量 number 各自保存一份数值,修改 x 不影响 number。

再看数组:

public static void changeArray(int[] a) {
    a[0] = 10;
}
Enter fullscreen mode Exit fullscreen mode
int[] arr = {1, 2, 3};
changeArray(arr);
System.out.println(arr[0]); // 10
Enter fullscreen mode Exit fullscreen mode

形参 a 得到了 arr 中引用的副本,二者仍指向同一个数组,所以 a[0] = 10 会改变外部看到的元素。

如果只是让形参指向新数组,外部变量不会跟着改变:

public static void replaceArray(int[] a) {
    a = new int[]{7, 8, 9};
}
Enter fullscreen mode Exit fullscreen mode
int[] arr = {1, 2, 3};
replaceArray(arr);
System.out.println(arr[0]); // 1
Enter fullscreen mode Exit fullscreen mode

记住:a[i] = ... 修改数组元素;a = ... 修改形参自己的引用值。 传递数组时也不需要复制整组元素。

3.2 作为返回值:返回一组计算结果

方法的返回类型写成 int[],就可以返回一个整型数组的引用。

例如,生成斐波那契数列的前 n 项:前两项为 1、1,从第三项开始,每项等于前两项之和。

public static int[] fib(int n) {
    if (n <= 0) {
        return null;
    }

    int[] result = new int[n];
    result[0] = 1;
    if (n >= 2) {
        result[1] = 1;
    }

    for (int i = 2; i < n; i++) {
        result[i] = result[i - 1] + result[i - 2];
    }
    return result;
}
Enter fullscreen mode Exit fullscreen mode

调用:

int[] result = fib(6);
if (result != null) {
    System.out.println(Arrays.toString(result)); // [1, 1, 2, 3, 5, 8]
}
Enter fullscreen mode Exit fullscreen mode
  • n == 1 时只能访问 result[0],不能直接给 result[1] 赋值。
  • 本例约定 n <= 0 返回 null,调用方使用前先判断。
  • 方法结束后,局部变量会离开作用域,但返回的数组仍被调用方引用,因此还能使用。
  • 本例使用 int,只适合结果未超出 int 范围的项数;从第 47 项开始会溢出。

4. 数组拷贝:创建一份独立的数组

4.1 Arrays.copyOf 与 Arrays.copyOfRange

b = a 只复制引用。如果需要独立修改一个 int 数组,可以创建新数组并复制元素:

int[] a = {10, 20, 30, 40};
int[] b = Arrays.copyOf(a, a.length);
b[0] = 99;

System.out.println(Arrays.toString(a)); // [10, 20, 30, 40]
System.out.println(Arrays.toString(b)); // [99, 20, 30, 40]
Enter fullscreen mode Exit fullscreen mode

Arrays.copyOf(原数组, 新长度) 的第二个参数是新数组的长度。新长度较小时截取前面的元素,较大时多出来的 int 元素为 0;原数组的长度不变。

复制部分元素可以使用:

int[] part = Arrays.copyOfRange(a, 1, 3);
System.out.println(Arrays.toString(part)); // [20, 30]
Enter fullscreen mode Exit fullscreen mode

范围是 [起始下标, 结束下标),左闭右开。因此 [1, 3) 复制下标 1、2,不包含下标 3。

4.2 用循环实现拷贝

public static int[] copyOf(int[] arr) {
    int[] result = new int[arr.length];
    for (int i = 0; i < arr.length; i++) {
        result[i] = arr[i];
    }
    return result;
}
Enter fullscreen mode Exit fullscreen mode

关键是先 new 一个数组,再逐个复制元素;如果直接 return arr,返回的仍是原数组的引用。

以上“修改互不影响”针对 int[] 这样的基本类型数组。若元素本身是引用,复制元素时复制的仍是引用,涉及的对象可能继续共享;这属于后续的深浅拷贝问题,本章不展开。

5. 用数组练习查找、排序与逆序

以下方法均假定传入的数组不是 null;长度为 0 的数组可以正常处理。

5.1 顺序查找:从头逐个比较

不要求数组有序。找到目标就返回下标,全部检查后仍没找到则返回 -1。

public static int find(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}
Enter fullscreen mode Exit fullscreen mode
int[] arr = {1, 2, 3, 10, 5, 6};
System.out.println(find(arr, 10)); // 3
System.out.println(find(arr, 8));  // -1
Enter fullscreen mode Exit fullscreen mode

-1 不是合法下标,适合表示未找到。它必须放在循环结束之后:当前元素不相等,不代表后面也没有。若目标出现多次,这个方法返回第一次出现的位置。

5.2 二分查找:每次排除一半范围

前提:数组已经有序。下面的代码只适用于升序数组。 升序指元素从小到大排列,允许相等;降序则相反。

用 left、right 表示待查找范围的左右边界,两端都包含在范围内。取中间下标 mid:

  • 目标小于 arr[mid]:目标只可能在左半边,令 right = mid - 1。
  • 目标大于 arr[mid]:目标只可能在右半边,令 left = mid + 1。
  • 相等:返回 mid。
public static int binarySearch(int[] arr, int target) {
    int left = 0;
    int right = arr.length - 1;

    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (target < arr[mid]) {
            right = mid - 1;
        } else if (target > arr[mid]) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}
Enter fullscreen mode Exit fullscreen mode

例如,在 {1, 2, 3, 4, 5, 6} 中查找 6:

轮次 left right mid 比较与下一步
1 0 5 2 6 > 3,令 left = 3
2 3 5 4 6 > 5,令 left = 5
3 5 5 5 6 == 6,返回下标 5

三个边界要理解:

  1. left == right 时还有一个元素没检查,所以循环条件是 <=。
  2. 中间元素已经比较过,下一轮必须排除它,因此使用 mid - 1 或 mid + 1。
  3. left > right 表示范围为空,才确定未找到。

left + (right - left) / 2 能避免直接计算 left + right 可能造成的整数溢出。每轮约缩小一半范围,对 10000 个元素最多约需比较 14 次。若有重复目标值,这个实现不保证返回第一次出现的位置。

5.3 冒泡排序:把较大的元素逐步换到右边

按升序排序时,从左到右比较相邻元素;前面的更大就交换。一趟结束后,尚未排好部分的最大值会到达该部分最右端。

以 {9, 5, 2, 7} 的第一趟为例:

[9, 5, 2, 7]
 交换 9 和 5 → [5, 9, 2, 7]
 交换 9 和 2 → [5, 2, 9, 7]
 交换 9 和 7 → [5, 2, 7, 9]
Enter fullscreen mode Exit fullscreen mode

9 已到最终位置,下一趟只需处理前 3 个元素;第二趟后得到 [2, 5, 7, 9]。

public static void bubbleSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • i 表示已经完成的趟数,最多需要 长度 - 1 趟。
  • 每完成一趟,右侧多一个已就位的元素,因此内层范围减去 i。
  • 内层要访问 arr[j + 1],所以条件为 j < arr.length - 1 - i。
  • 交换时用 temp 暂存原值,避免赋值覆盖后丢失数据。
  • 若要降序,将比较条件改为 arr[j] < arr[j + 1]。

调用后,原数组会直接改变:

int[] arr = {9, 5, 2, 7};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [2, 5, 7, 9]
Enter fullscreen mode Exit fullscreen mode

冒泡排序便于理解,但效率较低。需要排序时,可以使用工具方法:

int[] arr = {9, 5, 2, 7};
Arrays.sort(arr); // 直接把原 int 数组按升序排列
System.out.println(Arrays.toString(arr)); // [2, 5, 7, 9]
Enter fullscreen mode Exit fullscreen mode

Arrays.sort 的内部排序算法留到后续学习,本章掌握调用方式即可。

5.4 数组逆序:两端向中间交换

逆序是把原来的先后顺序颠倒,不等于降序排序。例如,{3, 1, 2} 逆序后是 {2, 1, 3}。

public static void reverse(int[] arr) {
    int left = 0;
    int right = arr.length - 1;

    while (left < right) {
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}
Enter fullscreen mode Exit fullscreen mode

例如,{1, 2, 3, 4} 先交换首尾,得到 {4, 2, 3, 1},再交换中间两个元素,得到 {4, 3, 2, 1}。

每次交换后两端各向中间移动一步。相遇时是同一个元素,无需交换,因此条件为 left < right。

6. 二维数组:数组的元素也是数组引用

6.1 创建、访问与长度

二维数组可以按“行、列”理解,但本质是:外层数组的每个元素,保存着一个一维数组的引用。

int[][] a = new int[2][3]; // 2 行,每行 3 个 int,初始值均为 0
Enter fullscreen mode Exit fullscreen mode

也可以直接给出数据:

int[][] a = {
    {1, 2, 3},
    {4, 5, 6}
};
Enter fullscreen mode Exit fullscreen mode

完整写法可以使用 new int[][]{{1, 2, 3}, {4, 5, 6}},但不能写成 new int[2][3]{{...}, {...}}。

System.out.println(a[0][1]); // 2:第 1 行、第 2 列
a[1][2] = 99;
System.out.println(a[1][2]); // 99
System.out.println(a.length);    // 2:外层长度,即行数
System.out.println(a[0].length); // 3:第 0 行的长度
Enter fullscreen mode Exit fullscreen mode

a[row][col] 可以分两步看:先由 a[row] 找到这一行的数组,再用 [col] 取出该行中的元素。行下标和列下标都从 0 开始,都不能越界。

6.2 遍历二维数组

外层循环遍历行,内层循环遍历当前行的元素:

int[][] a = {
    {1, 2, 3},
    {4, 5, 6}
};

for (int row = 0; row < a.length; row++) {
    for (int col = 0; col < a[row].length; col++) {
        System.out.print(a[row][col] + " ");
    }
    System.out.println(); // 一行输出完后再换行
}
Enter fullscreen mode Exit fullscreen mode

输出:

1 2 3
4 5 6
Enter fullscreen mode Exit fullscreen mode

内层条件必须看 a[row].length,它表示当前行的列数;a.length 表示的是行数。

6.3 不规则二维数组

每一行都是独立的一维数组,所以各行长度可以不同:

int[][] a = new int[2][];
a[0] = new int[3];
a[1] = new int[5];
Enter fullscreen mode Exit fullscreen mode
a[0] ──→ [0, 0, 0]
a[1] ──→ [0, 0, 0, 0, 0]
Enter fullscreen mode Exit fullscreen mode

也可以直接初始化:

int[][] a = {
    {1, 2, 3},
    {4, 5, 6, 7, 8}
};
Enter fullscreen mode Exit fullscreen mode

需要注意:

  • 按长度创建时,可以写 new int[2][],但不能写 new int[][3]。
  • 只执行 new int[2][] 时,外层数组已创建,但两个元素都是 null;此时不能访问 a[0][0] 或 a[0].length。
  • 为每行创建数组后,前面的双重循环同样适用,因为它使用的是各行自己的长度。

7. 复习时抓住这几组区别

容易混淆的操作 实际含义
a.length / a.length - 1 元素个数 / 最后一个元素的下标(非空数组)
b = a / Arrays.copyOf(a, a.length) 共享同一个数组 / 创建新数组并复制元素
a[i] = value / a = new int[...] 修改原数组元素 / 让变量引用另一个数组
数组传参时修改元素 / 给形参重新赋值 外部可见元素变化 / 外部变量的指向不变
顺序查找 / 二分查找 不要求有序 / 本文实现要求升序
排序 / 逆序 按大小排列 / 颠倒原有先后顺序
a.length / a[row].length(二维数组) 行数 / 当前行的列数

Top comments (0)