第三单元 · 数组应用、方法、综合 · 第 7 / 12 节
数组练习:查找、统计与最值
用一次遍历求最值、求和、计数,写线性查找并约定找不到返回 -1,交换两个下标的值,检查空数组/单元素边界。
左边网页看步骤,右边终端敲命令。动手前先把四种东西分开:带 $ 的 bash 块是命令,复制到终端;标了文件名的 java 块是源码,写进同名文件;output 块是程序跑完的屏幕输出,只用来对照;input 块是程序的标准输入,只读,本节没有。网页的复制按钮会去掉 $ 提示符,其他内容逐字保留。
这一节只做数组练习,不定义方法——第二单元你已经会 int[]、下标、length、for 遍历和 if,本节把它们组合起来解决真问题:给一份 8 人的成绩单做体检。下一节才学方法,现在每段逻辑都写在 main 里。
固定数据是下面 8 个成绩,后面每一步都用它,方便你核对:
| 第几个 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 分数 | 72 | 88 | 95 | 61 | 78 | 88 | 54 | 100 |
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
注意“第几个”和“下标”差 1:人从 1 数,数组从 0 数。本节的输出有的写“下标”,有的写“第几个”,看到时先想清楚是哪一个。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 7 步
第 1 步:把 8 个成绩装进数组,先核对人数
先确认环境,再建本节目录。工作目录固定为 ~/java-practice/lesson07,mkdir -p 的意思是“目录不存在就建,已经存在就什么都不做”,不会清空你以前的东西。
下面有一行 if [ -e ScoreList.java ]; then mv ...; fi:它在文件已经存在时,把旧文件改名成带时间戳的备份,然后才让你写新文件。这样你上次的练习不会被静默覆盖。date +%s 会生成当前时间的秒数,保证备份名不重复。
$ source ~/java-practice/env.sh
$ java -version
$ mkdir -p ~/java-practice/lesson07
$ cd ~/java-practice/lesson07
$ if [ -e ScoreList.java ]; then mv ScoreList.java "ScoreList.java.bak.$(date +%s)"; fi
java -version 应显示 17.x。看不到 17.x 就先回 setup.html 检查环境,不要继续。
nano 是终端里的文本编辑器,单独放一个命令块,避免复制一整段时后面的编译命令被它吞进编辑界面:
$ nano ScoreList.java
打开后把下面的源码完整粘进去。保存是 Ctrl+O,回车确认文件名;退出是 Ctrl+X。这两个是编辑器的快捷键,和后面程序的 Ctrl+C 不是一回事。
public class ScoreList {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
System.out.println("成绩人数:" + scores.length);
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:" + scores[i]);
}
}
}
int[] scores = {...} 是静态初始化:元素个数由大括号里的值决定,这里正好 8 个,所以 scores.length 是 8,最大下标是 7。循环条件必须写 i < scores.length,不能写 i <= scores.length——后者会走到下标 8,越界。
编译并运行:
$ javac -encoding UTF-8 ScoreList.java
$ java ScoreList
成绩人数:8
第 1 个:72
第 2 个:88
第 3 个:95
第 4 个:61
第 5 个:78
第 6 个:88
第 7 个:54
第 8 个:100
javac 成功时不打印任何东西是正常的;它生成了 ScoreList.class。运行写类名 ScoreList,不要写文件名或 .class。
改一改:把数组里的 54 改成 59,重新编译运行,先猜第 7 行输出什么,再对答案。
参考答案 对答案
第 7 行会变成 第 7 个:59。只改元素值,length 仍是 8,其他行不变。看完记得改回 54,本节后面的输出都按原数据计算。
第 2 步:一次遍历求总分和平均分
求和的套路叫“累加器”:准备一个 total 从 0 开始,每读一个元素就加进去。循环结束时,total 就是所有元素的和。这个变量必须在循环外面声明,否则每轮都会被重新赋成 0。
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSummary.java ]; then mv ScoreSummary.java "ScoreSummary.java.bak.$(date +%s)"; fi
$ nano ScoreSummary.java
public class ScoreSummary {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
int total = 0;
for (int i = 0; i < scores.length; i++) {
total = total + scores[i];
}
double average = (double) total / scores.length;
System.out.println("总分:" + total);
System.out.println("平均分:" + average);
}
}
$ javac -encoding UTF-8 ScoreSummary.java
$ java ScoreSummary
总分:636
平均分:79.5
关键在 (double) total / scores.length。total 和 scores.length 都是 int,如果直接相除,Java 会做整数除法,636 / 8 得到 79,小数部分被丢掉。先把 total 强制转换成 double,除法才按小数进行,结果是 79.5。
改一改:把 (double) 删掉,重新编译运行,先写下你认为的平均分。
参考答案 对答案
会输出 平均分:79。两个 int 相除只保留整数部分,不是四舍五入。要小数结果,至少让参与除法的一个值是 double;本节用强制转换,最直白。
第 3 步:用第一个元素当起点,找最高分和最低分
找最值的常见错误是把 max 初始化成 0。如果所有成绩都是负数(比如记录的是相对上次的增减分),答案会永远停在 0。正确做法是拿第一个元素当擂主,从第二个元素开始挑战:谁更大,谁就成为新的 max。
同时记录下标:分数变了,位置也要跟着变。最高分和最低分可以在同一个循环里完成,两个 if 互不影响。
$ cd ~/java-practice/lesson07
$ if [ -e ScoreExtremes.java ]; then mv ScoreExtremes.java "ScoreExtremes.java.bak.$(date +%s)"; fi
$ nano ScoreExtremes.java
public class ScoreExtremes {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
int max = scores[0];
int maxIndex = 0;
int min = scores[0];
int minIndex = 0;
for (int i = 1; i < scores.length; i++) {
if (scores[i] > max) {
max = scores[i];
maxIndex = i;
}
if (scores[i] < min) {
min = scores[i];
minIndex = i;
}
}
System.out.println("最高分:" + max + ",下标:" + maxIndex);
System.out.println("最低分:" + min + ",下标:" + minIndex);
}
}
$ javac -encoding UTF-8 ScoreExtremes.java
$ java ScoreExtremes
最高分:100,下标:7
最低分:54,下标:6
循环从 i = 1 开始,因为下标 0 已经是初始擂主,没必要再和自己比。maxIndex 和 minIndex 也必须用 scores[0] 对应的 0 初始化,否则分数和下标会脱节。
改一改:如果数组是 {-5, -9, -2},把 max 初始化成 0 会输出什么?现在的写法又会输出什么?
参考答案 对答案
初始化成 0 会输出最高分 0,但 0 根本不在数组里;现在的写法输出最高分 -2、下标 2。最值的起点必须来自数组本身,不能靠“0 看起来很小”这种假设。
第 4 步:计数:及格人数与及格率
计数器和累加器长得像,但做的事不同:累加器把值加起来,计数器只问“满足条件的有几个”。套路是 count = 0,遇到一个满足条件的就 count++。
$ cd ~/java-practice/lesson07
$ if [ -e PassCounter.java ]; then mv PassCounter.java "PassCounter.java.bak.$(date +%s)"; fi
$ nano PassCounter.java
public class PassCounter {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
int passCount = 0;
for (int i = 0; i < scores.length; i++) {
if (scores[i] >= 60) {
passCount++;
}
}
double passRate = (double) passCount / scores.length * 100;
System.out.println("及格人数:" + passCount);
System.out.println("及格率:" + passRate + "%");
}
}
$ javac -encoding UTF-8 PassCounter.java
$ java PassCounter
及格人数:7
及格率:87.5%
8 人里只有 54 不及格,所以及格 7 人;7 / 8 = 0.875,乘 100 得 87.5。条件写 >= 60 而不是 > 60:60 分整是及格,这个边界在输入 60 时最容易暴露。
改一改:把及格线改成 70,先算一遍再改代码运行。
参考答案 对答案
72、88、95、78、88、100 共 6 人及格,及格率 6 / 8 × 100 = 75.0,输出 及格人数:6、及格率:75.0%。61 和 54 都不到 70。
第 5 步:线性查找:找到返回下标,找不到返回 -1
“线性查找”就是从第一个元素开始挨个比,找到就停。它的返回值要能表达两种结果:找到了,返回下标;没找到,返回一个不可能的下标。这里约定返回 -1,因为合法下标从 0 开始,-1 永远不会是真实位置。
不要用 0 表示“没找到”——0 是第一个元素的合法下标,调用者分不清“在第一格”还是“没找到”。
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSearch.java ]; then mv ScoreSearch.java "ScoreSearch.java.bak.$(date +%s)"; fi
$ nano ScoreSearch.java
public class ScoreSearch {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
int[] targets = {88, 100, 90};
for (int t = 0; t < targets.length; t++) {
int found = -1;
for (int i = 0; i < scores.length; i++) {
if (scores[i] == targets[t]) {
found = i;
break;
}
}
System.out.println("查找 " + targets[t] + ":下标 " + found);
}
}
}
$ javac -encoding UTF-8 ScoreSearch.java
$ java ScoreSearch
查找 88:下标 1
查找 100:下标 7
查找 90:下标 -1
found 每查一个目标都要重置成 -1。如果把它写在最外层循环外面,第一次查到 1 之后,后面查 90 还会保留 1,输出就错了。break 只跳出内层循环,外层继续查下一个目标。
88 在数组里出现两次(下标 1 和 5),这里返回 1:找到第一个就停。这是“查首次出现位置”的约定,不是 bug。
改一改:查找 61 和 59,分别应输出什么?
参考答案 对答案
61 在下标 3,输出 查找 61:下标 3;59 不在数组里,输出 查找 59:下标 -1。找不到时不要编造一个 0。
第 6 步:交换两个下标的值
交换看起来简单,直接写两行却经常错:
scores[1] = scores[6];
scores[6] = scores[1];
第一行已经把 scores[1] 改成 54,原来的 88 丢了;第二行再把 54 写回去,两个位置都变成 54。要交换,必须先用一个临时变量把其中一个值“抄下来”。
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSwap.java ]; then mv ScoreSwap.java "ScoreSwap.java.bak.$(date +%s)"; fi
$ nano ScoreSwap.java
public class ScoreSwap {
public static void main(String[] args) {
int[] scores = {72, 88, 95, 61, 78, 88, 54, 100};
String before = "";
for (int i = 0; i < scores.length; i++) {
if (i > 0) {
before = before + ",";
}
before = before + scores[i];
}
System.out.println("交换前:" + before);
int temp = scores[1];
scores[1] = scores[6];
scores[6] = temp;
String after = "";
for (int i = 0; i < scores.length; i++) {
if (i > 0) {
after = after + ",";
}
after = after + scores[i];
}
System.out.println("交换后:" + after);
}
}
$ javac -encoding UTF-8 ScoreSwap.java
$ java ScoreSwap
交换前:72,88,95,61,78,88,54,100
交换后:72,54,95,61,78,88,88,100
三段顺序不能换:temp 先保存 scores[1] 的 88;scores[1] 被 54 覆盖;最后把 temp 里的 88 放进 scores[6]。打印时先用循环把 8 个元素拼成一个 String,元素之间加中文逗号,再整体 println,所以输出没有多余的空格;这一段也顺便复习了字符串拼接。
这里的打印循环写了两遍。下一节学了方法,就能把“打印数组”抽成一个方法,只写一次。
改一改:如果要交换下标 0 和 7,三行交换代码应该怎么写?
参考答案 对答案
int temp = scores[0];
scores[0] = scores[7];
scores[7] = temp;
下标按你的目标替换,但“先存、再覆盖、后写回”的顺序不变。
第 7 步:空数组和单元素数组:先把边界挡住
前面的代码都默认“数组至少有一个元素”。这个假设在真实数据里不一定成立:统计之前先看 length,能避免用 scores[0] 直接撞上越界异常。
$ cd ~/java-practice/lesson07
$ if [ -e BoundaryCheck.java ]; then mv BoundaryCheck.java "BoundaryCheck.java.bak.$(date +%s)"; fi
$ nano BoundaryCheck.java
public class BoundaryCheck {
public static void main(String[] args) {
int[] empty = new int[0];
int[] single = {85};
if (empty.length == 0) {
System.out.println("空数组:没有成绩可统计");
} else {
int max = empty[0];
for (int i = 1; i < empty.length; i++) {
if (empty[i] > max) {
max = empty[i];
}
}
System.out.println("空数组最高分:" + max);
}
int max = single[0];
for (int i = 1; i < single.length; i++) {
if (single[i] > max) {
max = single[i];
}
}
System.out.println("单元素数组:最高分 " + max + ",最低分 " + single[0]);
}
}
$ javac -encoding UTF-8 BoundaryCheck.java
$ java BoundaryCheck
空数组:没有成绩可统计
单元素数组:最高分 85,最低分 85
new int[0] 创建的是长度为 0 的数组,不是 null;empty.length 可以安全读取,值是 0,但 empty[0] 会越界。所以先判断 length == 0,再决定要不要进求最值的分支。
单元素数组走另一条路:max 从 single[0] 开始,循环 i = 1 不满足 i < 1,一次都不执行,直接输出 85。这不是巧合,而是“第一个元素当起点”写法的好处。
改一改:单元素数组求最低分,需要额外加判断吗?
参考答案 对答案
不需要。min 用 single[0] 初始化,循环体一次都不进,最低分就是 85,和最高分相同。需要判断的只有长度为 0 的情况,因为那时根本没有 [0] 可读。
常见错误
下面按“现象 → 常见原因 → 先检查什么”列出;报错文字会随 JDK 补丁和系统语言略有差异,认关键词和原因即可,不必逐字一致。
- **
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8**:循环写成了i <= scores.length,最后一次访问下标 8。现象是前面几行都正常,最后一行崩。先检查循环条件,必须是i < scores.length;再检查手动写的下标有没有超过 7。 - 平均分输出 79 而不是 79.5:
total / scores.length是两个int相除,小数被截断。先看除号两边是不是都还是int,把其中一个转成double,例如(double) total / scores.length。 - 最高分总是 0,或者最小值总是 0:
max/min初始化成了 0,而数据可能全是负数。先看初始化那一行;改成scores[0],循环从 1 开始。 - 查找明明没有这个分数,却返回 0:
found初始化成了 0,0 是合法下标,无法表达“没找到”。检查found的初值是不是-1,以及每次查新目标前有没有重置。 - 交换后两个位置变成同一个值:少了临时变量,第一行赋值已经把原值覆盖。按“
temp保存 → 覆盖 → 写回”三行检查顺序,不要图省事写成两行。 - **空数组一跑就崩在
scores[0]**:代码默认至少有一个元素。求最值、取第一个元素之前先判断length == 0,空的时候直接输出提示或跳过统计。 - **
Error: Could not find or load main class ScoreList.class**:java后面跟类名,不要加.class后缀,也不要在别的目录运行。先ls看ScoreList.class是否在当前目录,再写java ScoreList。
练习
- 不看代码,用本节固定的 8 个成绩算出:总分、平均分、最高分、最低分、及格人数。
- 查找 95 和 59,分别返回什么下标?
- 写一段代码:先找到最高分的下标,再把最高分和下标 0 的元素交换。交换后数组第一个值是多少?不用新建文件,可以在
ScoreSwap.java里改完再运行,确认后改回。
先自己在纸上算,再展开答案。
参考答案 对答案
- 总分 636,平均分 79.5,最高分 100(下标 7),最低分 54(下标 6),及格 7 人(只有 54 不及格)。
- 95 在下标 2,返回 2;59 不在数组里,返回 -1。
- 先用第 3 步的写法找到
maxIndex(这里是 7),再交换scores[0]和scores[maxIndex]:int temp = scores[0]; scores[0] = scores[7]; scores[7] = temp;交换后第一个值是 100,最后一个值是 72。注意这是“先查下标再交换”,不是把 100 写死。
做到这里
做到这里今天可以停。你已经能用一次遍历完成求和、求最值、计数,能写“找不到返回 -1”的线性查找,能用临时变量交换两个下标,也知道空数组和单元素数组要先检查 length。
本节所有逻辑都挤在 main 里,ScoreSwap 的打印循环甚至写了两遍。下一节学方法,把这些重复段落装进有名字的方法里;现在不要提前用方法,先把循环和边界写熟。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P056、P058–P061 的数组遍历、求最值、求和并统计、交换数据主题;P061 的打乱数据依赖随机数,本阶段不采用。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。