6.1 数组变量、对象与元素
前置知识:变量、类型、条件和循环
预计时间:20 分钟
完成标志:能区分数组变量、数组对象和数组元素,并能处理空数组、
null与非法长度
一个 int score 只能保存一个成绩。若要保存一千名学生的成绩,继续声明 score2 到 score1000 既不可维护,也无法用循环统一处理。Java 数组把固定数量、同一组件类型的值放进一个对象,再用从 0 开始的索引访问每个位置。
1. 声明变量不等于创建数组
int[] scores; // 声明一个能引用 int 数组的变量
scores = new int[5]; // 创建长度为 5 的数组对象并保存其引用也可以合写:
int[] scores = new int[5];这里有三个不同概念:
| 名称 | 例子 | 含义 |
|---|---|---|
| 组件类型 | int | 每个位置能保存什么类型 |
| 数组类型 | int[] | 变量能引用哪类数组 |
| 数组对象 | new int[5] | 运行时创建的 5 个组件 |
scores 变量保存的是数组引用,不是把五个整数直接写进变量。数组是对象,但创建后长度固定。需要不同长度时,只能创建另一个数组并复制所需元素。
Java 语言规定了索引行为,却没有要求程序员依赖某种对象物理布局。可以把索引理解为相对第一个元素的偏移:第一个偏移是 0,所以长度为 5 的合法索引是 0、1、2、3、4。
2. 新数组先获得默认值
int[] counts = new int[3];
boolean[] flags = new boolean[2];
String[] names = new String[2];
System.out.println(counts[0]); // 0
System.out.println(flags[0]); // false
System.out.println(names[0]); // null数组组件会被默认初始化:
| 组件类型 | 默认值 |
|---|---|
byte、short、int、long | 对应类型的 0 |
float、double | 对应类型的正零 |
char | \u0000 |
boolean | false |
| 引用类型 | null |
这和方法内的普通局部变量不同。局部变量必须先明确赋值才能读取;新数组的组件已经由运行时初始化。
3. 写入和读取都要通过合法索引
int[] scores = new int[5];
scores[0] = 85;
scores[1] = 92;
scores[2] = 78;
scores[3] = 95;
scores[4] = 88;
System.out.println(scores[2]); // 78
System.out.println(scores.length); // 5length 是数组的只读字段,没有括号。对长度 n 的数组,索引必须满足:
0 <= index && index < n访问 scores[-1] 或 scores[5] 都会抛 ArrayIndexOutOfBoundsException。Java 会在运行时检查每次数组访问,不会自动扩容或把越界写入别处。
4. 初始化器适合已知的少量值
声明时可以同时创建并填值:
int[] scores = {85, 92, 78, 95, 88};需要在声明之后替换整个数组时,要写出数组类型:
int[] scores = new int[0];
scores = new int[]{60, 70, 80};下面不能编译:
// scores = {60, 70, 80}; // 数组初始化器不能单独作为赋值右侧也不能把长度和初始化列表同时写在一个数组创建表达式里:
// int[] scores = new int[3]{60, 70, 80}; // 编译错误5. 空数组、null 和非法长度是三种状态
int[] empty = new int[0];
System.out.println(empty.length); // 0
int[] missing = null;- 空数组是存在的对象,长度为 0,没有任何合法索引。
null表示变量当前没有引用数组;读取missing.length会抛NullPointerException。new int[-1]在运行时抛NegativeArraySizeException。
API 设计中应明确区分“没有提供数组”和“提供了一个没有元素的数组”。若两者含义相同,通常优先接收空数组,调用方更容易直接遍历。
6. 数组变量赋值会产生别名
int[] first = {10, 20, 30};
int[] second = first;
second[0] = 99;
System.out.println(first[0]); // 99这没有创建第二个数组。first 和 second 保存了同一个数组对象的引用,所以通过任一变量修改组件,另一边都能看到。
若需要独立数组,要复制组件。下一课之后会比较 clone、Arrays.copyOf 和 System.arraycopy。对于引用类型数组,这些操作复制的仍是组件中的引用,不会自动复制每个元素对象。
语言差异窗口
- C++ 内建数组的存储期取决于声明位置和分配方式,并不只有“栈数组”和“堆数组”两种。现代 C++ 通常优先使用
std::array表示固定长度、std::vector表示动态长度。 - Python 常用
list,它可以增长,也允许混合类型;类型标注和工程约定可进一步约束元素类型。它不是 Java 数组的等价物。
scores = [85, 92, 78, 95, 88]
print(scores[2]) # 78
scores.append(100)专业速查
| 目的 | Java 写法 | 边界 |
|---|---|---|
| 只声明变量 | int[] values; | 使用前必须赋值 |
| 创建定长数组 | new int[5] | 长度不能为负 |
| 同时填值 | int[] values = {1, 2}; | 简写仅用于声明 |
| 后续替换数组 | values = new int[]{1, 2}; | 原对象是否仍被引用取决于别名 |
| 读取长度 | values.length | values 不能为 null |
| 访问组件 | values[index] | 0 <= index < length |
离开这一页前
把 int[] values、new int[5] 和 values[0] 分别标成数组变量、数组对象和组件访问,再解释空数组、null 与负长度在哪个阶段、以何种方式失败。最后画出 second = first 后的两个变量和一个数组对象。
下一页用循环访问每个元素,并检查正序、倒序和空数组的边界。
规范入口:JLS §10:数组。