第二单元 · 分支、循环、数组 · 第 6 / 12 节
数组:一排带编号的格子
创建 int[],用下标读写,用 length 控制遍历边界,解释越界异常,浅讲数组变量与元素的关系。
这一节还是“左边网页、右边终端”:网页给完整源码和命令,终端里编译、运行、对照输出。$ 开头是 shell 命令;java 代码块是完整源文件;output 是标准输出(stdout);input 是提前写好的标准输入(stdin,只读)。
前面几节你用变量一次保存一个值,用循环重复做一件事。现在要保存 5 个成绩:写 5 个变量太散,循环也不方便按编号取。数组把同一类型的多个值放进一排“带编号的格子”,用一个名字加下标访问。
本节只讲一维 int[] 的创建、下标、length、遍历、越界和“数组变量与元素的关系”。**不用自定义方法,不用 Arrays 工具类,不用二维数组,也不用增强 for**;遍历统一用上一节的普通 for 加下标。main 仍然只是入口外壳。
新开 SSH 窗口时,先 source ~/java-practice/env.sh,确认 java -version 和 javac -version 都是 17.x。source 可能打印一行环境提示,那是 shell 的输出,不要和程序 output 混。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 6 步
第 1 步:静态初始化,用下标取第一个和最后一个
数组的静态初始化在声明时直接写出元素:
int[] scores = {88, 92, 75, 60, 95};
int[] 表示“一排 int 格子”。scores 是数组变量名。花括号里是 5 个元素,数组长度就是 5。下标从 0 开始:第 1 个元素是 scores[0],第 5 个是 scores[4]。scores.length 得到元素个数 5,最后一个元素的下标是 length - 1。
先预测:scores.length 是几?scores[0] 和 scores[4] 分别是什么?scores[5] 能取到吗?
建目录、写 ArrayBasicsDemo.java。同名文件存在时先备份,不覆盖旧内容:
$ source ~/java-practice/env.sh
$ java -version
$ javac -version
$ mkdir -p ~/java-practice/lesson06
$ cd ~/java-practice/lesson06
$ if [ -e ArrayBasicsDemo.java ]; then mv ArrayBasicsDemo.java "ArrayBasicsDemo.java.bak.$(date +%s)"; fi
$ nano ArrayBasicsDemo.java
把下面这个完整文件粘进 nano,按 Ctrl+O 回车保存,再按 Ctrl+X 退出:
public class ArrayBasicsDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
System.out.println("数组长度:" + scores.length);
System.out.println("第 1 个:" + scores[0]);
System.out.println("第 5 个:" + scores[4]);
System.out.println("最后一个:" + scores[scores.length - 1]);
}
}
$ ls -l ArrayBasicsDemo.java
$ javac -encoding UTF-8 ArrayBasicsDemo.java
$ java ArrayBasicsDemo
数组长度:5
第 1 个:88
第 5 个:95
最后一个:95
scores.length 是 5,不是 4;下标最大是 4。scores[scores.length - 1] 先算 5 - 1 = 4,再取 scores[4],所以也是 95。记住这个写法:**最后一个元素的下标永远是 length - 1**。
length 是数组的属性,不带圆括号;写 scores.length() 会编译错误。先记住数组用 length,不要加括号。
提示 下标不是“第几个”
看到“第 1 个”,脑子里要自动翻译成下标 0;“第 5 个”是下标 4。读代码时先把“第几个”减 1,再去看方括号。
第 2 步:动态初始化,先开格子再写入
如果声明时还不知道每个元素的值,可以先规定长度,让 Java 开好格子:
int[] scores = new int[5];
这叫动态初始化。new int[5] 创建 5 个 int 格子,每个格子先填默认值 0。之后用 scores[0] = 88; 这样的赋值语句逐个写入。数组长度在创建时确定,之后不能改;scores.length 仍然是 5。
new 在这里是“创建数组”的意思。第四单元会用 new 创建对象,那是另一件事;现在只要知道 new int[5] 是按 5 个格子开数组即可。
先预测:刚创建、还没赋值时,scores[0] 是几?写完 5 个值后,scores.length 会变成几?
$ cd ~/java-practice/lesson06
$ if [ -e ArrayFillDemo.java ]; then mv ArrayFillDemo.java "ArrayFillDemo.java.bak.$(date +%s)"; fi
$ nano ArrayFillDemo.java
public class ArrayFillDemo {
public static void main(String[] args) {
int[] scores = new int[5];
System.out.println("默认第 1 个:" + scores[0]);
scores[0] = 88;
scores[1] = 92;
scores[2] = 75;
scores[3] = 60;
scores[4] = 95;
System.out.println("写入后第 1 个:" + scores[0]);
System.out.println("写入后第 5 个:" + scores[4]);
System.out.println("长度仍是:" + scores.length);
}
}
$ javac -encoding UTF-8 ArrayFillDemo.java
$ java ArrayFillDemo
默认第 1 个:0
写入后第 1 个:88
写入后第 5 个:95
长度仍是:5
刚创建时 5 个格子都是 0,所以 scores[0] 打印 0;赋值只改对应格子的值,长度一直是 5。写第 5 个元素要用 scores[4],不是 scores[5]。
第 3 步:用 for 和 length 遍历
遍历就是“从第一个格子走到最后一个格子”。数组的标准遍历写法:
for (int i = 0; i < scores.length; i++) {
// 使用 scores[i]
}
i 是下标,从 0 开始;条件是 i < scores.length,不是 i <= scores.length。因为最后一个合法下标是 length - 1,当 i 等于 length 时已经越界。
先预测:下面的循环会打印几行?如果条件写成 i <= scores.length,第 6 次会发生什么?
$ cd ~/java-practice/lesson06
$ if [ -e ArrayTraverseDemo.java ]; then mv ArrayTraverseDemo.java "ArrayTraverseDemo.java.bak.$(date +%s)"; fi
$ nano ArrayTraverseDemo.java
public class ArrayTraverseDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
for (int i = 0; i < scores.length; i++) {
System.out.println("scores[" + i + "] = " + scores[i]);
}
System.out.println("遍历结束,共 " + scores.length + " 个元素");
}
}
$ javac -encoding UTF-8 ArrayTraverseDemo.java
$ java ArrayTraverseDemo
scores[0] = 88
scores[1] = 92
scores[2] = 75
scores[3] = 60
scores[4] = 95
遍历结束,共 5 个元素
i 依次取 0、1、2、3、4,scores[i] 就依次是 5 个元素;i 变成 5 时,5 < 5 为 false,循环结束。这里把 i 拼进字符串只是为了看清下标,真正取元素的是 scores[i]。
如果写成 i <= scores.length,i 会走到 5,scores[5] 越界,程序在第 6 次循环时抛异常。**遍历边界记成 i < length,不要背成 i <= length。**
第 4 步:越界不是“取到 0”,而是程序停住
数组的合法下标是 0 到 length - 1。访问这个范围之外的格子,Java 会抛出 ArrayIndexOutOfBoundsException,程序不会返回 0,也不会自动扩容,而是当场停止。
先预测:下面的程序会打印几行?最后一行“这一行不会执行”会出现吗?
$ cd ~/java-practice/lesson06
$ if [ -e ArrayBoundsDemo.java ]; then mv ArrayBoundsDemo.java "ArrayBoundsDemo.java.bak.$(date +%s)"; fi
$ nano ArrayBoundsDemo.java
public class ArrayBoundsDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
System.out.println("准备读取下标 5");
System.out.println(scores[5]);
System.out.println("这一行不会执行");
}
}
$ javac -encoding UTF-8 ArrayBoundsDemo.java
$ java ArrayBoundsDemo
这段程序会故意失败。标准输出只有第一行:
准备读取下标 5
异常信息走的是标准错误(stderr),不会出现在 output 块里。终端上会看到类似下面的内容(不同 JDK 补丁的措辞可能略有差异,认类名和“下标 5 超出长度 5”即可):
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
Index 5 out of bounds for length 5 的意思是:长度为 5 的数组,合法下标是 0 到 4,你却要取下标 5。程序在 scores[5] 这一行就停了,后面的“这一行不会执行”没有机会执行。
正确取最后一个元素的写法是先算 length - 1:
$ cd ~/java-practice/lesson06
$ if [ -e ArrayLastDemo.java ]; then mv ArrayLastDemo.java "ArrayLastDemo.java.bak.$(date +%s)"; fi
$ nano ArrayLastDemo.java
public class ArrayLastDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
int lastIndex = scores.length - 1;
System.out.println("最后一个下标:" + lastIndex);
System.out.println("最后一个元素:" + scores[lastIndex]);
}
}
$ javac -encoding UTF-8 ArrayLastDemo.java
$ java ArrayLastDemo
最后一个下标:4
最后一个元素:95
lastIndex 是 4,scores[4] 合法。不要把 lastIndex 写死成 4;如果数组换成 8 个元素,length - 1 会自动变成 7,写死的 4 就错了。
注意 越界是运行时异常,不是编译错误
javac 不会检查 scores[5] 是否越界,因为下标可能是运行时才算出来的。编译能过,运行才报 ArrayIndexOutOfBoundsException。看到这个类名,先数数组长度和最大合法下标。
第 5 步:数组变量保存的是“同一排格子”的引用
先看一个容易误判的例子:
int[] first = {10, 20, 30};
int[] second = first;
second[0] = 99;
second = first 不是把 3 个元素复制一份,而是让 second 和 first 指向同一排格子。通过 second[0] 改值,first[0] 也会看到变化,因为两个名字说的是同一个数组。
先预测:second[0] = 99; 之后,first[0] 是 10 还是 99?
$ cd ~/java-practice/lesson06
$ if [ -e ArrayAliasDemo.java ]; then mv ArrayAliasDemo.java "ArrayAliasDemo.java.bak.$(date +%s)"; fi
$ nano ArrayAliasDemo.java
public class ArrayAliasDemo {
public static void main(String[] args) {
int[] first = {10, 20, 30};
int[] second = first;
System.out.println("修改前 first[0] = " + first[0]);
second[0] = 99;
System.out.println("通过 second 改完后 first[0] = " + first[0]);
System.out.println("second[0] = " + second[0]);
System.out.println("first 的长度:" + first.length);
System.out.println("second 的长度:" + second.length);
}
}
$ javac -encoding UTF-8 ArrayAliasDemo.java
$ java ArrayAliasDemo
修改前 first[0] = 10
通过 second 改完后 first[0] = 99
second[0] = 99
first 的长度:3
second 的长度:3
可以把它想成两个名字挂在同一排格子上:
first ─┐
├──> [10, 20, 30]
second ─┘
改的是格子里的值,不是名字。两个变量看到的长度也都是 3,因为它们指向同一个数组。**数组变量之间用 = 赋值,复制的是“指向同一排格子”的引用,不是元素副本。** 这一点现在先浅讲;第四单元讲对象时还会遇到引用。
如果想让第二个数组独立,需要重新创建,例如 second = new int[]{10, 20, 30};(second 已经声明过时,不能重复写 int[])。这会开一排新格子,改 second 不会影响 first。本阶段不展开复制数组的 API。
第 6 步:用一个 int[] 保存 5 个成绩并遍历输出
把前面的东西合起来:用 new int[5] 开 5 个格子,用 for 循环读 5 次 Scanner.nextInt(),把每次输入写进 scores[i];再用第二个 for 遍历输出。这是本节的目标:**一个 int[] 保存 5 个成绩,完整遍历输出。**
Scanner 还是第一单元那个;文件第一行保留 import java.util.Scanner;,否则 javac 会报 cannot find symbol。
先预测:如果输入依次是 88、92、75、60、95,第二个循环会打印哪 5 行?提示语会打印几次?
$ cd ~/java-practice/lesson06
$ if [ -e ScoreInputDemo.java ]; then mv ScoreInputDemo.java "ScoreInputDemo.java.bak.$(date +%s)"; fi
$ nano ScoreInputDemo.java
import java.util.Scanner;
public class ScoreInputDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = new int[5];
for (int i = 0; i < scores.length; i++) {
System.out.println("请输入第 " + (i + 1) + " 个成绩:");
scores[i] = sc.nextInt();
}
System.out.println("你输入的成绩是:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:" + scores[i]);
}
}
}
编译后,用 printf 把 5 行输入送进程序;\n 表示换行,printf 是 shell 命令,不是 Java 语法:
$ javac -encoding UTF-8 ScoreInputDemo.java
$ printf '88\n92\n75\n60\n95\n' | java ScoreInputDemo
交互输入(示例,只读):
88
92
75
60
95
请输入第 1 个成绩:
请输入第 2 个成绩:
请输入第 3 个成绩:
请输入第 4 个成绩:
请输入第 5 个成绩:
你输入的成绩是:
第 1 个:88
第 2 个:92
第 3 个:75
第 4 个:60
第 5 个:95
第一个循环的 i 从 0 到 4,scores[i] 依次接住 5 个输入;第二个循环再从 0 到 4 读出来。(i + 1) 的括号不能省:"第 " + i + 1 会先拼成“第 0”再拼 1,变成“第 01”,不是“第 1”。字符串 + 的细节第一单元讲过,这里再用一次。
如果只输入 4 个数,程序会等第 5 个;用 printf 固定输入时,第 5 次 nextInt() 没有数据可读,会抛 NoSuchElementException。这不是数组越界,是输入不够。看到“等住”或 NoSuchElementException,先数 input 块是不是 5 行。
说明 为什么不用增强 for
本课程还没教增强 for(那种不用下标的写法),所以这里统一用 for (int i = 0; i < scores.length; i++)。下标遍历虽然多写一点,但能同时拿到下标和元素,后面查找、修改、倒序都要用下标。
常见错误
- 现象: 遍历时多打印一行,然后抛
ArrayIndexOutOfBoundsException。原因: 循环条件写成了i <= scores.length,最后一次i等于长度。先检查: 把条件改成i < scores.length;合法下标最大是length - 1。 - 现象:
javac报cannot find symbol,指向scores.length()。原因: 数组的length是属性,不是方法,不能加圆括号。先检查: 去掉(),写scores.length。 - 现象:
javac报array dimension missing或';' expected,指向new int[];。原因: 动态初始化必须给长度,new int[5];new int[]后面没有长度也没有花括号元素。先检查: 方括号里补上长度,或改成静态初始化{...}。 - 现象: 已经声明过的数组变量再写
scores = {1, 2, 3};编译错误。原因: 静态初始化的花括号只能用在声明那一行;后面赋值要写new int[]{1, 2, 3}或逐个下标赋值。先检查: 看花括号是不是跟int[] scores =在同一条声明语句里。 - 现象: 通过
second[0]改值,first[0]也变了,以为程序串数据。原因:second = first复制的是引用,两个变量指向同一个数组。先检查: 确认有没有把两个数组变量用=连起来;要独立就重新new一个数组。 - 现象:
ScoreInputDemo读不满 5 个成绩,或者抛InputMismatchException/NoSuchElementException。原因:input不足 5 行、某行不是整数,或printf的\n写少了。先检查: 数input块的行数,确认每行都是纯整数,printf字符串里有 5 个\n。
练习
先自己写、先预测,再展开答案。练习答案都在 JDK 17 上实际编译运行过,output 是真实标准输出。
练习 1:倒序遍历 5 个成绩
用 int[] scores = {88, 92, 75, 60, 95};,从最后一个元素开始,倒着打印到第一个。输出要带“第 N 个”,最后打印“倒序输出结束”。提示:i 从 scores.length - 1 开始,条件是 i >= 0,每次 i--。
参考答案 练习 1:倒序遍历
完整文件存成 ArrayReverseDemo.java:
public class ArrayReverseDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
for (int i = scores.length - 1; i >= 0; i--) {
System.out.println("第 " + (i + 1) + " 个:" + scores[i]);
}
System.out.println("倒序输出结束");
}
}
编译运行:
$ javac -encoding UTF-8 ArrayReverseDemo.java
$ java ArrayReverseDemo
第 5 个:95
第 4 个:60
第 3 个:75
第 2 个:92
第 1 个:88
倒序输出结束
i 从 4 开始,依次取 4、3、2、1、0;i 变成 -1 时,-1 >= 0 为 false,循环结束。注意 i >= 0 不能写成 i > 0,否则下标 0 的第 1 个元素会被漏掉。
练习 2:修改一个格子的值
用 int[] scores = {88, 92, 75, 60, 95};,把下标 1 的元素改成 100。先打印修改前的 scores[1],再打印修改后的 scores[1],最后打印数组长度。
参考答案 练习 2:修改元素
完整文件存成 ArrayChangeDemo.java:
public class ArrayChangeDemo {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60, 95};
System.out.println("修改前 scores[1] = " + scores[1]);
scores[1] = 100;
System.out.println("修改后 scores[1] = " + scores[1]);
System.out.println("长度:" + scores.length);
}
}
编译运行:
$ javac -encoding UTF-8 ArrayChangeDemo.java
$ java ArrayChangeDemo
修改前 scores[1] = 92
修改后 scores[1] = 100
长度:5
改的是下标 1 的格子,也就是第 2 个元素;长度不会因为改值而变化。
练习 3:先预测引用赋值
下面三行只是片段,不用单独存成文件;先口算 a[1] 是几,再展开答案。
int[] a = {1, 2, 3};
int[] b = a;
b[1] = 9;
System.out.println(a[1]);
参考答案 练习 3:同一排格子
输出 9。b = a 没有复制元素,b 和 a 指向同一个数组;通过 b[1] 改成 9,a[1] 看到的也是 9。如果写成 int[] b = {1, 2, 3};,那才是另一排新格子,改 b[1] 不会影响 a。
做到这里
做到这里今天可以停。你已经会:
- 用静态初始化
{...}和动态初始化new int[5]创建一维int[]; - 用下标读写元素,知道下标从 0 开始、最大是
length - 1; - 用
for (int i = 0; i < scores.length; i++)遍历; - 认出
ArrayIndexOutOfBoundsException并知道先数长度和下标; - 浅显地理解
second = first复制的是引用,两个变量可能指向同一排格子; - 用一个
int[]保存 5 个成绩并遍历输出。
下一节(第三单元)才把“查找、最值、求和、计数、交换”拆成数组练习;本节没有自定义方法,也没有 Arrays 工具类。不要在练习里写 static 辅助方法或 Arrays.sort。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P054–P057、P062 的数组概述、静态初始化、元素访问、遍历、动态初始化、常见问题与内存图主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。