Java从环境到类与对象 · SSH 实操

第二单元 · 分支、循环、数组 · 第 6 / 12 节

数组:一排带编号的格子

创建 int[],用下标读写,用 length 控制遍历边界,解释越界异常,浅讲数组变量与元素的关系。

约 30 分钟 · 6 个步骤 · 进度只存本机

这一节还是“左边网页、右边终端”:网页给完整源码和命令,终端里编译、运行、对照输出。$ 开头是 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 混。

SSH 服务器终端Java 源码程序交互输入输出示例(只读)复制 Java 源码不带文件名;复制命令不带 $ 提示符;输出和输入示例没有复制按钮。
这是教程网页,不是在线运行环境。Java 代码和 SSH 命令都要在你的服务器上执行;网页不会执行代码、不会连接服务器、也不会接收密码。

已完成 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。同名文件存在时先备份,不覆盖旧内容:

SSH 服务器终端bash
$ 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 退出:

Java 源码ArrayBasicsDemo.java
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]);
    }
}
SSH 服务器终端bash
$ ls -l ArrayBasicsDemo.java
$ javac -encoding UTF-8 ArrayBasicsDemo.java
$ java ArrayBasicsDemo
输出示例(只读)output不可复制
数组长度: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 会变成几?

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ArrayFillDemo.java ]; then mv ArrayFillDemo.java "ArrayFillDemo.java.bak.$(date +%s)"; fi
$ nano ArrayFillDemo.java
Java 源码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);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayFillDemo.java
$ java ArrayFillDemo
输出示例(只读)output不可复制
默认第 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 次会发生什么?

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ArrayTraverseDemo.java ]; then mv ArrayTraverseDemo.java "ArrayTraverseDemo.java.bak.$(date +%s)"; fi
$ nano ArrayTraverseDemo.java
Java 源码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 + " 个元素");
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayTraverseDemo.java
$ java ArrayTraverseDemo
输出示例(只读)output不可复制
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,也不会自动扩容,而是当场停止。

先预测:下面的程序会打印几行?最后一行“这一行不会执行”会出现吗?

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ArrayBoundsDemo.java ]; then mv ArrayBoundsDemo.java "ArrayBoundsDemo.java.bak.$(date +%s)"; fi
$ nano ArrayBoundsDemo.java
Java 源码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("这一行不会执行");
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayBoundsDemo.java
$ java ArrayBoundsDemo

这段程序会故意失败。标准输出只有第一行:

输出示例(只读)output不可复制
准备读取下标 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:

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ArrayLastDemo.java ]; then mv ArrayLastDemo.java "ArrayLastDemo.java.bak.$(date +%s)"; fi
$ nano ArrayLastDemo.java
Java 源码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]);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayLastDemo.java
$ java ArrayLastDemo
输出示例(只读)output不可复制
最后一个下标: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?

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ArrayAliasDemo.java ]; then mv ArrayAliasDemo.java "ArrayAliasDemo.java.bak.$(date +%s)"; fi
$ nano ArrayAliasDemo.java
Java 源码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);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayAliasDemo.java
$ java ArrayAliasDemo
输出示例(只读)output不可复制
修改前 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 行?提示语会打印几次?

SSH 服务器终端bash
$ cd ~/java-practice/lesson06
$ if [ -e ScoreInputDemo.java ]; then mv ScoreInputDemo.java "ScoreInputDemo.java.bak.$(date +%s)"; fi
$ nano ScoreInputDemo.java
Java 源码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 语法:

SSH 服务器终端bash
$ javac -encoding UTF-8 ScoreInputDemo.java
$ printf '88\n92\n75\n60\n95\n' | java ScoreInputDemo

交互输入(示例,只读):

交互输入(只读)input不可复制
88
92
75
60
95
输出示例(只读)output不可复制
请输入第 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:

Java 源码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("倒序输出结束");
    }
}

编译运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayReverseDemo.java
$ java ArrayReverseDemo
输出示例(只读)output不可复制
第 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:

Java 源码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);
    }
}

编译运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 ArrayChangeDemo.java
$ java ArrayChangeDemo
输出示例(只读)output不可复制
修改前 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。

说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。