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

第三单元 · 数组应用、方法、综合 · 第 7 / 12 节

数组练习:查找、统计与最值

用一次遍历求最值、求和、计数,写线性查找并约定找不到返回 -1,交换两个下标的值,检查空数组/单元素边界。

约 35 分钟 · 7 个步骤 · 进度只存本机

左边网页看步骤,右边终端敲命令。动手前先把四种东西分开:带 $ 的 bash 块是命令,复制到终端;标了文件名的 java 块是源码,写进同名文件;output 块是程序跑完的屏幕输出,只用来对照;input 块是程序的标准输入,只读,本节没有。网页的复制按钮会去掉 $ 提示符,其他内容逐字保留。

这一节只做数组练习,不定义方法——第二单元你已经会 int[]、下标、length、for 遍历和 if,本节把它们组合起来解决真问题:给一份 8 人的成绩单做体检。下一节才学方法,现在每段逻辑都写在 main 里。

固定数据是下面 8 个成绩,后面每一步都用它,方便你核对:

第几个12345678
分数72889561788854100
下标01234567

注意“第几个”和“下标”差 1:人从 1 数,数组从 0 数。本节的输出有的写“下标”,有的写“第几个”,看到时先想清楚是哪一个。

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

已完成 0 / 7 步

第 1 步:把 8 个成绩装进数组,先核对人数

先确认环境,再建本节目录。工作目录固定为 ~/java-practice/lesson07,mkdir -p 的意思是“目录不存在就建,已经存在就什么都不做”,不会清空你以前的东西。

下面有一行 if [ -e ScoreList.java ]; then mv ...; fi:它在文件已经存在时,把旧文件改名成带时间戳的备份,然后才让你写新文件。这样你上次的练习不会被静默覆盖。date +%s 会生成当前时间的秒数,保证备份名不重复。

SSH 服务器终端bash
$ 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 是终端里的文本编辑器,单独放一个命令块,避免复制一整段时后面的编译命令被它吞进编辑界面:

SSH 服务器终端bash
$ nano ScoreList.java

打开后把下面的源码完整粘进去。保存是 Ctrl+O,回车确认文件名;退出是 Ctrl+X。这两个是编辑器的快捷键,和后面程序的 Ctrl+C 不是一回事。

Java 源码ScoreList.java
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,越界。

编译并运行:

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

SSH 服务器终端bash
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSummary.java ]; then mv ScoreSummary.java "ScoreSummary.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano ScoreSummary.java
Java 源码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);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ScoreSummary.java
$ java ScoreSummary
输出示例(只读)output不可复制
总分: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 互不影响。

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

SSH 服务器终端bash
$ cd ~/java-practice/lesson07
$ if [ -e PassCounter.java ]; then mv PassCounter.java "PassCounter.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano PassCounter.java
Java 源码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 + "%");
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 PassCounter.java
$ java PassCounter
输出示例(只读)output不可复制
及格人数: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 是第一个元素的合法下标,调用者分不清“在第一格”还是“没找到”。

SSH 服务器终端bash
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSearch.java ]; then mv ScoreSearch.java "ScoreSearch.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano ScoreSearch.java
Java 源码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);
        }
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ScoreSearch.java
$ java ScoreSearch
输出示例(只读)output不可复制
查找 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 步:交换两个下标的值

交换看起来简单,直接写两行却经常错:

输出示例(只读)text不可复制
scores[1] = scores[6];
scores[6] = scores[1];

第一行已经把 scores[1] 改成 54,原来的 88 丢了;第二行再把 54 写回去,两个位置都变成 54。要交换,必须先用一个临时变量把其中一个值“抄下来”。

SSH 服务器终端bash
$ cd ~/java-practice/lesson07
$ if [ -e ScoreSwap.java ]; then mv ScoreSwap.java "ScoreSwap.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano ScoreSwap.java
Java 源码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);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 ScoreSwap.java
$ java ScoreSwap
输出示例(只读)output不可复制
交换前: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,三行交换代码应该怎么写?

参考答案 对答案
输出示例(只读)text不可复制
int temp = scores[0];
scores[0] = scores[7];
scores[7] = temp;

下标按你的目标替换,但“先存、再覆盖、后写回”的顺序不变。

第 7 步:空数组和单元素数组:先把边界挡住

前面的代码都默认“数组至少有一个元素”。这个假设在真实数据里不一定成立:统计之前先看 length,能避免用 scores[0] 直接撞上越界异常。

SSH 服务器终端bash
$ cd ~/java-practice/lesson07
$ if [ -e BoundaryCheck.java ]; then mv BoundaryCheck.java "BoundaryCheck.java.bak.$(date +%s)"; fi
SSH 服务器终端bash
$ nano BoundaryCheck.java
Java 源码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]);
    }
}
SSH 服务器终端bash
$ javac -encoding UTF-8 BoundaryCheck.java
$ java BoundaryCheck
输出示例(只读)output不可复制
空数组:没有成绩可统计
单元素数组:最高分 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。

练习

  1. 不看代码,用本节固定的 8 个成绩算出:总分、平均分、最高分、最低分、及格人数。
  2. 查找 95 和 59,分别返回什么下标?
  3. 写一段代码:先找到最高分的下标,再把最高分和下标 0 的元素交换。交换后数组第一个值是多少?不用新建文件,可以在 ScoreSwap.java 里改完再运行,确认后改回。

先自己在纸上算,再展开答案。

参考答案 对答案
  1. 总分 636,平均分 79.5,最高分 100(下标 7),最低分 54(下标 6),及格 7 人(只有 54 不及格)。
  2. 95 在下标 2,返回 2;59 不在数组里,返回 -1。
  3. 先用第 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。

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