第三单元 · 数组应用、方法、综合 · 第 9 / 12 节
综合练习:控制台成绩统计
把需求拆成输入、统计、输出三个方法,用数组保存固定数量成绩,计算平均分/最高分/及格人数,用固定输入核对输出。
左边网页看步骤,右边终端敲命令。bash 块是命令,java 块写进同名文件,output 块只读、用来逐行对照,input 块是这次运行的标准输入,也只读——程序跑起来后,你在终端里按行键入这些数字,不要试图把 input 块整块粘给 shell。
这一节做一个完整的小程序:从控制台读 5 个成绩,输出人数、平均分、最高分、及格人数、及格率。只用已经学过的数组、for、if、方法和 Scanner;不引入类与对象,不用 ArrayList,不处理异常,也不做输入校验——先把“输入 → 统计 → 输出”这条主线走通。
下面按“输入 → 统计 → 输出”三类职责来拆。统计里平均分和及格人数各写成一个小方法,所以最后会看到四个辅助方法;每个方法只做一件事。
固定输入有两组,后面会反复用:
| 组 | 5 个成绩 | 总分 | 平均分 | 最高分 | 及格人数 |
|---|---|---|---|---|---|
| A | 78、92、65、88、50 | 373 | 74.6 | 92 | 4 |
| B | 0、100、59、60、100 | 319 | 63.8 | 100 | 3 |
B 组专门用来踩边界:0 分、100 分、59 和 60 这一对及格线两侧的值都在里面。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 6 步
第 1 步:先定输入:读 5 个成绩,别的先不做
写程序最怕一口气把所有功能堆进去,出错时不知道是哪一段。先把“输入”做对:main 建 Scanner,调用 readScores 拿到数组,只打印读到的个数;平均分、最高分先不碰。
$ source ~/java-practice/env.sh
$ java -version
$ mkdir -p ~/java-practice/lesson09
$ cd ~/java-practice/lesson09
$ if [ -e ScoreApp.java ]; then mv ScoreApp.java "ScoreApp.java.bak.$(date +%s%N)"; fi
if [ -e ... ] 还是“已存在就改名备份”,不会覆盖你之前写的版本。这一节反复重写同一个 ScoreApp.java,所以后缀用 date +%s%N(秒 + 纳秒)而不是只到秒,避免几分钟内两次备份撞名。
$ nano ScoreApp.java
import java.util.Scanner;
public class ScoreApp {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = readScores(sc);
System.out.println("已读入 " + scores.length + " 个成绩。");
}
static int[] readScores(Scanner sc) {
int[] scores = new int[5];
System.out.println("请输入 5 个学生的成绩,每行一个:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:");
scores[i] = sc.nextInt();
}
return scores;
}
}
new int[5] 是动态初始化:先建 5 个格子,元素默认是 0;循环再用 nextInt() 一个个填。方法返回 int[],main 用 int[] scores = readScores(sc); 接住。
Scanner 作为参数传给方法,方法里就能继续读同一个输入流。不要在每个方法里各自 new Scanner(System.in),那会绕开课程约定,也容易把输入顺序弄乱。
这次运行的固定输入是 A 组;下面是交互输入(示例,只读),你在终端里按行键入:
78
92
65
88
50
$ javac -encoding UTF-8 ScoreApp.java
$ java ScoreApp
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
已读入 5 个成绩。
提示语也是 System.out 输出的一部分,所以 output 块里会看到它们。输入的数字本身不回显在程序输出里,终端上看到的数字是你敲进去的,不属于 stdout。
改一改:如果第 3 个成绩输入 101,程序会拒绝吗?
参考答案 对答案
不会。nextInt() 只负责把整数读进来,readScores 没有任何范围判断,101 会原样存进数组并参加后面的统计。本节按“输入由使用者保证在 0–100”来写;要做校验,得在读取时加 if 和提示,这是后面可以自己扩展的方向,不属于本节必需项。
第 2 步:加一个 average 方法,把平均分算对
现在只加“统计平均分”这一件事。方法接收整个数组,内部累加总分,再除以长度。返回类型必须是 double,除法前先转换,否则 373 / 5 会得到 74。
$ cd ~/java-practice/lesson09
$ if [ -e ScoreApp.java ]; then mv ScoreApp.java "ScoreApp.java.bak.$(date +%s%N)"; fi
$ nano ScoreApp.java
import java.util.Scanner;
public class ScoreApp {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = readScores(sc);
System.out.println("平均分:" + average(scores));
}
static int[] readScores(Scanner sc) {
int[] scores = new int[5];
System.out.println("请输入 5 个学生的成绩,每行一个:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:");
scores[i] = sc.nextInt();
}
return scores;
}
static double average(int[] scores) {
int total = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
return (double) total / scores.length;
}
}
输入仍然是 A 组;下面是交互输入(示例,只读):
78
92
65
88
50
$ javac -encoding UTF-8 ScoreApp.java
$ java ScoreApp
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
平均分:74.6
A 组总分 373,373 / 5 = 74.6。(double) 不能省;省掉就是整数除法,输出 74。
改一改:如果只输入 4 个数就停住,程序在等什么?
参考答案 对答案
在等第 5 次 nextInt()。for 循环要读满 5 个元素才结束,少一个就停在“第 5 个:”后面继续等,不是死机。补上第 5 个数并回车即可。如果输入流已经结束(例如文件只有 4 行),nextInt() 读不到数据会抛 NoSuchElementException;本节不展开异常处理。
第 3 步:加一个 passCount 方法,数及格人数
计数逻辑和上一节一样,只是搬进方法:count 从 0 开始,遇到 >= 60 就加 1,最后返回 count。方法名用动词开头,读代码时一眼能看出它“数什么”。
$ cd ~/java-practice/lesson09
$ if [ -e ScoreApp.java ]; then mv ScoreApp.java "ScoreApp.java.bak.$(date +%s%N)"; fi
$ nano ScoreApp.java
import java.util.Scanner;
public class ScoreApp {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = readScores(sc);
System.out.println("平均分:" + average(scores));
System.out.println("及格人数:" + passCount(scores));
}
static int[] readScores(Scanner sc) {
int[] scores = new int[5];
System.out.println("请输入 5 个学生的成绩,每行一个:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:");
scores[i] = sc.nextInt();
}
return scores;
}
static double average(int[] scores) {
int total = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
return (double) total / scores.length;
}
static int passCount(int[] scores) {
int count = 0;
for (int i = 0; i < scores.length; i++) {
if (scores[i] >= 60) {
count++;
}
}
return count;
}
}
这次运行的交互输入(示例,只读)仍是 A 组:
78
92
65
88
50
$ javac -encoding UTF-8 ScoreApp.java
$ java ScoreApp
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
平均分:74.6
及格人数:4
A 组里 50 不及格,其余 4 个都 ≥ 60。>= 的边界在 B 组会真正被检验:59 不及格,60 及格。
改一改:把 >= 60 改成 > 60,A 组的及格人数会变吗?B 组呢?
参考答案 对答案
A 组不变,仍是 4——A 组没有正好 60 分的成绩。B 组会从 3 变成 2:60 分的那个人被 > 60 排除了。这就是为什么边界数据要单独准备一组。
第 4 步:写 printReport,把 main 缩成三行
输入、平均分、及格人数都单独验证过了,现在把它们组装成报告。printReport 是 void:它只负责打印,不需要把结果交回给 main。最高分在这一个方法里用一个短循环求出来;平均分和及格人数直接调用已经验证过的方法,不重复写循环。
$ cd ~/java-practice/lesson09
$ if [ -e ScoreApp.java ]; then mv ScoreApp.java "ScoreApp.java.bak.$(date +%s%N)"; fi
$ nano ScoreApp.java
import java.util.Scanner;
public class ScoreApp {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = readScores(sc);
printReport(scores);
}
static int[] readScores(Scanner sc) {
int[] scores = new int[5];
System.out.println("请输入 5 个学生的成绩,每行一个:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:");
scores[i] = sc.nextInt();
}
return scores;
}
static double average(int[] scores) {
int total = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
return (double) total / scores.length;
}
static int passCount(int[] scores) {
int count = 0;
for (int i = 0; i < scores.length; i++) {
if (scores[i] >= 60) {
count++;
}
}
return count;
}
static void printReport(int[] scores) {
int max = scores[0];
for (int i = 1; i < scores.length; i++) {
if (scores[i] > max) {
max = scores[i];
}
}
double rate = (double) passCount(scores) / scores.length * 100;
System.out.println("==== 成绩统计 ====");
System.out.println("人数:" + scores.length);
System.out.println("平均分:" + average(scores));
System.out.println("最高分:" + max);
System.out.println("及格人数:" + passCount(scores));
System.out.println("及格率:" + rate + "%");
}
}
这次运行的交互输入(示例,只读)仍是 A 组:
78
92
65
88
50
$ javac -encoding UTF-8 ScoreApp.java
$ java ScoreApp
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
==== 成绩统计 ====
人数:5
平均分:74.6
最高分:92
及格人数:4
及格率:80.0%
main 现在只有三行有效代码:建 Scanner、读数组、打印报告。输入、统计、输出各自有名字,改报告格式只动 printReport,改读取方式只动 readScores。
printReport 里又调用 average 和 passCount,这叫方法组合。注意不要在 printReport 里再写一遍求和循环——那会出现两份“怎么算平均分”的代码,改了一处忘另一处。
改一改:为什么 printReport 的返回类型是 void,而 average 不是?
参考答案 对答案
printReport 的任务是“把报告打到屏幕”,做完就结束,没有结果需要交给调用者,所以用 void。average 的任务是“算出一个数”,main 还要拿这个数去拼接输出,所以必须返回 double。判断标准是:调用者需不需要这个方法的结果。
第 5 步:换一组边界数据,核对 0、59、60、100
代码没变,只换输入。把 B 组数据敲进去,先按上面的表手算一遍,再和程序输出对照。固定输入的意义就在这里:每次运行的条件完全一样,输出对不上时,问题一定在代码,不在“这次手气不好”。
$ cd ~/java-practice/lesson09
$ java ScoreApp
这次运行的交互输入(示例,只读)是 B 组:
0
100
59
60
100
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
==== 成绩统计 ====
人数:5
平均分:63.8
最高分:100
及格人数:3
及格率:60.0%
核对:总分 0 + 100 + 59 + 60 + 100 = 319,319 / 5 = 63.8;最高分 100;及格的是 100、60、100 共 3 人;3 / 5 = 60.0%。59 被排除在外,说明 >= 60 写对了。
如果这一组输出和手算不一致,先查三处:平均分有没有 (double)、及格判断是 > 还是 >=、最高分初始值是不是 scores[0]。
改一改:如果把 B 组的 59 改成 60,及格人数和及格率会变成多少?
参考答案 对答案
及格人数从 3 变成 4,及格率从 60.0% 变成 80.0%。总分从 319 变成 320,平均分变成 64.0。60 分整算及格,这是 >= 的约定。
第 6 步:加最高分下标,让报告能指出“第几个”
老师看到“最高分 92”还会问“是谁”。数组里没有姓名,只有位置,那就把下标带出来。lesson07 里我们用 maxIndex 变量记过最高分的位置;这一节的报告只在 printReport 里用一次,不必再抽一个方法:直接在求最高分的循环里多记一个 maxIndex 变量,循环结束就能同时拿到分数和位置。
$ cd ~/java-practice/lesson09
$ if [ -e ScoreApp.java ]; then mv ScoreApp.java "ScoreApp.java.bak.$(date +%s%N)"; fi
$ nano ScoreApp.java
import java.util.Scanner;
public class ScoreApp {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] scores = readScores(sc);
printReport(scores);
}
static int[] readScores(Scanner sc) {
int[] scores = new int[5];
System.out.println("请输入 5 个学生的成绩,每行一个:");
for (int i = 0; i < scores.length; i++) {
System.out.println("第 " + (i + 1) + " 个:");
scores[i] = sc.nextInt();
}
return scores;
}
static double average(int[] scores) {
int total = 0;
for (int i = 0; i < scores.length; i++) {
total += scores[i];
}
return (double) total / scores.length;
}
static int passCount(int[] scores) {
int count = 0;
for (int i = 0; i < scores.length; i++) {
if (scores[i] >= 60) {
count++;
}
}
return count;
}
static void printReport(int[] scores) {
int max = scores[0];
int maxIndex = 0;
for (int i = 1; i < scores.length; i++) {
if (scores[i] > max) {
max = scores[i];
maxIndex = i;
}
}
double rate = (double) passCount(scores) / scores.length * 100;
System.out.println("==== 成绩统计 ====");
System.out.println("人数:" + scores.length);
System.out.println("平均分:" + average(scores));
System.out.println("最高分:" + max + "(第 " + (maxIndex + 1) + " 个)");
System.out.println("及格人数:" + passCount(scores));
System.out.println("及格率:" + rate + "%");
}
}
这次运行的交互输入(示例,只读)仍是 A 组:
78
92
65
88
50
$ javac -encoding UTF-8 ScoreApp.java
$ java ScoreApp
请输入 5 个学生的成绩,每行一个:
第 1 个:
第 2 个:
第 3 个:
第 4 个:
第 5 个:
==== 成绩统计 ====
人数:5
平均分:74.6
最高分:92(第 2 个)
及格人数:4
及格率:80.0%
maxIndex 是 printReport 的局部变量,和 max 一起在同一个循环里更新:谁刷新了最高分,位置就跟着刷新。报告里写“第 2 个”,因为人从 1 数、数组从 0 数。不要把 maxIndex + 1 和数组访问混在一起:scores[maxIndex] 取分数,maxIndex + 1 只用于显示。
如果最高分出现两次,maxIndex 记录第一次出现的下标,因为它只在 > 时更新,相等不更新。B 组有两个 100,会报“第 2 个”;这是约定,不是错误。
改一改:如果“最高分是第几个”这个信息要在三个地方使用,还应该把下标留在 printReport 里吗?
参考答案 对答案
不应该。只在报告里用一次,局部变量最省事;要在多处复用,就把它抽成独立方法,把求下标的循环搬进去:
static int maxIndex(int[] scores) {
int index = 0;
for (int i = 1; i < scores.length; i++) {
if (scores[i] > scores[index]) {
index = i;
}
}
return index;
}
printReport 里改成 int index = maxIndex(scores);,再用 scores[index] 和 index + 1。判断标准和上一节一样:看这段逻辑有没有独立职责、会不会被重复使用。
常见错误
下面按“现象 → 常见原因 → 先检查什么”列出;报错文字会随 JDK 补丁和系统语言略有差异,认关键词和原因即可,不必逐字一致。
- **
cannot find symbol: class Scanner**:忘了import java.util.Scanner;,或者拼成了scanner。现象是Scanner下面有红色波浪线、编译报找不到符号。先看第一行 import,大小写必须一致。 - **
cannot find symbol: method readScores(Scanner)**:方法名拼错、大小写不对,或者定义时漏了Scanner参数。先核对调用处和定义处的名字、参数个数、参数类型是否完全一致。 - **
incompatible types: int[] cannot be converted to int**:readScores的返回类型写成了int,或者main用int去接数组。返回数组必须是int[],接收变量也必须是int[]。 - 平均分输出 74.0 或 63.0:
total / scores.length是两个int相除。先找除号两边,把total转成double,再除。 - 程序停在“第 5 个:”不动:它在等第 5 个
nextInt(),不是死机。补输入即可。想安全结束自己正在运行的程序,按Ctrl+C;Ctrl+D是给终端发送输入结束符(EOF),程序还在等nextInt()时会因为读不到数据而报错,不要把它当“退出键”。 - **
Exception in thread "main" java.util.NoSuchElementException**:输入提前用完了,nextInt()没有数据可读。检查input是不是只有 4 行,或者复制固定输入时漏了一行。 - 及格人数差 1:边界写成了
> 60,把 60 分整排除了。先找passCount里的比较符号,必须是>= 60。 - **输出和
output块对不上**:提示语也是 stdout 的一部分,output块包含“第 1 个:”这些行;如果你只对照统计结果,会以为多了几行。逐行从第一行比起,别只看最后几行。 - **
class ScoreApp is public, should be declared in a file named ScoreApp.java**:文件名和 public 类名不一致。用ls -l看文件名,大小写也要一致。
练习
- 给报告加“最低分”,写一个
min或minIndex方法,输出类似最低分:50。 - 给报告加“优秀人数”(≥ 90 分),A 组应输出 1,B 组应输出 2。
- 如果班级人数不是固定 5 人,而是先输入人数
n,再读n个成绩,readScores应该怎么改?只写改动部分即可,不用处理n为负数的异常情况。
先自己改、自己跑,再展开答案。
参考答案 对答案
- 用返回下标的方式最省事:
static int minIndex(int[] scores) {
int index = 0;
for (int i = 1; i < scores.length; i++) {
if (scores[i] < scores[index]) {
index = i;
}
}
return index;
}
在 printReport 里加一行 System.out.println("最低分:" + scores[minIndex(scores)]);,A 组输出 最低分:50。
static int excellentCount(int[] scores) {
int count = 0;
for (int i = 0; i < scores.length; i++) {
if (scores[i] >= 90) {
count++;
}
}
return count;
}
在报告里加 System.out.println("优秀人数:" + excellentCount(scores));。A 组只有 92 一个,输出 1;B 组两个 100,输出 2。
- 在
main里先读人数,再把它传给方法:
int n = sc.nextInt();
int[] scores = readScores(sc, n);
readScores 改成两个参数,用 n 建数组:
static int[] readScores(Scanner sc, int n) {
int[] scores = new int[n];
for (int i = 0; i < scores.length; i++) {
scores[i] = sc.nextInt();
}
return scores;
}
提示语可以按 n 拼接。new int[n] 的长度来自运行时读到的值,但 n 为负数会抛异常;本节不处理异常,实际使用时要在读入后先判断 n > 0。
做到这里
做到这里今天可以停。你已经把一个“读 5 个成绩 → 算平均分/最高分/及格人数 → 打印报告”的程序拆成 readScores、average、passCount、printReport 四个方法,最高分的下标作为 printReport 的局部变量;main 只剩建 Scanner、读数组、打印报告三步;也知道换一组固定输入就能核对边界。
本节仍然没有创建对象,所有方法都带 static。下一单元开始写 Student 类、用 new 创建对象,方法会逐步改成通过对象调用;static 的完整规则不在本阶段展开。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P074–P080 的综合练习主题范围(买飞机票、找质数、验证码、复制数组、评委打分等)。本站没有采用原课的具体题目、随机数或二维数组,改为原创的“控制台成绩统计”;讲解、代码、错误清单与练习均为原创补充,非官方站点/教材,未获授权,不逐字复刻视频或讲义。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。