1. 用一个数组管理一组数据
数组用于保存一组相同类型的数据。例如,保存 5 个学生的成绩,不必分别定义 5 个变量:
int[] scores = {70, 80, 85, 60, 90}; 这里,int[] 是数组类型,scores 是数组变量,数组中有 5 个 int 元素。
可以把数组理解为一排连续编号的格子:每个格子保存一个元素,通过编号直接访问。这个编号叫作下标(索引),从 0 开始。
下标: 0 1 2 3 4 元素: 70 80 85 60 90 一个数组创建后,长度就固定了;元素的值可以修改。
1.1 创建与初始化
动态初始化:指定长度,元素先使用默认值。
int[] numbers = new int[3]; // 3 个 int 元素,初始值都是 0 double[] prices = new double[5]; // 5 个 double 元素 String[] words = new String[3]; // 3 个 String 元素 通用格式是 元素类型[] 数组名 = new 元素类型[长度];。new 在这里用于创建数组。
“动态初始化”并不意味着长度可以随意增长,而是创建时指定长度。
静态初始化:直接给出元素,长度由元素个数决定。
int[] a = new int[]{10, 20, 30}; int[] b = {10, 20, 30}; // 声明时可以简写 String[] words = {"Hello", "Java"}; 元素值必须能赋给数组的元素类型。指定元素时,不再在方括号中填写长度:
// 错误:不能同时指定长度和初始化列表 // int[] a = new int[3]{10, 20, 30}; 声明和赋值可以分开,但分开后不能只写 {...}:
int[] a; a = new int[3]; a = new int[]{10, 20, 30}; // 错误:这种简写只能在声明时使用 // a = {10, 20, 30}; 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 -
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 } 条件写成 i < a.length,不能写成 i <= a.length;使用 a.length 也比把长度写死更方便。
只需要依次读取元素时,可以用增强 for(for-each):
int[] a = {10, 20, 30}; for (int x : a) { System.out.println(x); } 每次循环,x 都取得当前元素的值,它不是下标。对于这里的 int 数组,给 x 赋值只会改变循环变量,不会修改数组元素;要修改元素,应使用 a[i]。
1.5 一次打印整个数组
Arrays 是 java.util 包中的数组工具类,提供了操作数组的方法。使用前在 Java 文件的类定义上方写:
import java.util.Arrays; Arrays.toString(a) 把一维数组的内容转换成便于阅读的字符串:
int[] a = {10, 20, 30}; String text = Arrays.toString(a); System.out.println(text); // [10, 20, 30] 直接 System.out.println(a) 不会按上面的形式输出数组元素。
2. 数组变量保存的是引用
2.1 先分清“变量”和“数组对象”
int number = 10; int[] a = {1, 2, 3}; -
number是基本类型变量,直接保存数值10。 -
a是引用类型变量,保存的是数组对象的引用,不是三个元素本身。
引用可以暂时理解为找到对象的“地址线索”,不必把它当成可直接操作的真实内存地址。
number 中保存:10 a 中保存的引用 ──→ 数组对象 [1, 2, 3] 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 b = a 复制的是引用,因此两个变量指向同一个数组:
a ──┐ ├──→ [99, 2, 3] b ──┘ 通过 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 这里不是原数组长度从 3 变成了 2,而是 b 改为引用另一个数组。
2.3 null:目前没有引用任何数组
int[] a = null; // System.out.println(a[0]); // 空指针异常 // System.out.println(a.length); // 空指针异常 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; } int number = 0; changeNumber(number); System.out.println(number); // 0 形参 x 和变量 number 各自保存一份数值,修改 x 不影响 number。
再看数组:
public static void changeArray(int[] a) { a[0] = 10; } int[] arr = {1, 2, 3}; changeArray(arr); System.out.println(arr[0]); // 10 形参 a 得到了 arr 中引用的副本,二者仍指向同一个数组,所以 a[0] = 10 会改变外部看到的元素。
如果只是让形参指向新数组,外部变量不会跟着改变:
public static void replaceArray(int[] a) { a = new int[]{7, 8, 9}; } int[] arr = {1, 2, 3}; replaceArray(arr); System.out.println(arr[0]); // 1 记住: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; } 调用:
int[] result = fib(6); if (result != null) { System.out.println(Arrays.toString(result)); // [1, 1, 2, 3, 5, 8] } -
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] Arrays.copyOf(原数组, 新长度) 的第二个参数是新数组的长度。新长度较小时截取前面的元素,较大时多出来的 int 元素为 0;原数组的长度不变。
复制部分元素可以使用:
int[] part = Arrays.copyOfRange(a, 1, 3); System.out.println(Arrays.toString(part)); // [20, 30] 范围是 [起始下标, 结束下标),左闭右开。因此 [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; } 关键是先 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; } int[] arr = {1, 2, 3, 10, 5, 6}; System.out.println(find(arr, 10)); // 3 System.out.println(find(arr, 8)); // -1 -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; } 例如,在 {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 |
三个边界要理解:
-
left == right时还有一个元素没检查,所以循环条件是<=。 - 中间元素已经比较过,下一轮必须排除它,因此使用
mid - 1或mid + 1。 -
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] 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; } } } } -
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] 冒泡排序便于理解,但效率较低。需要排序时,可以使用工具方法:
int[] arr = {9, 5, 2, 7}; Arrays.sort(arr); // 直接把原 int 数组按升序排列 System.out.println(Arrays.toString(arr)); // [2, 5, 7, 9] 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--; } } 例如,{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 也可以直接给出数据:
int[][] a = { {1, 2, 3}, {4, 5, 6} }; 完整写法可以使用 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 行的长度 a[row][col] 可以分两步看:先由 a[row] 找到这一行的数组,再用 [col] 取出该行中的元素。行下标和列下标都从 0 开始,都不能越界。
6.2 遍历二维数组
外层循环遍历行,内层循环遍历当前行的元素:
Related posts
Why map() inside map() is how grouped lists work
Cómo vemos venir un posible cierre del Cristo Redentor antes de que cambie el estado oficial (y por qué mostramos “sin confirmar”)
Your UI should tell you what's going on
Pharo in the Browser with WebAssembly
Meet Lenso UI. Bringing the design language and interactions of iOS and iPadOS to the web. Glass materials. Elastic feedback. Fluid menus. Built with Base UI + StyleX. Try it: https://ui.lenso.dev
