第8章 第一阶段项目:命令行成绩统计器
第8章 第一阶段项目:命令行成绩统计器
第一阶段学了很多基础知识:
- Java 程序运行。
- 变量和类型。
- 输入输出。
- 条件判断。
- 循环。
- 方法。
如果这些知识只停留在小片段里,很容易学完就忘。项目的意义是把它们串起来。本章我们做一个命令行成绩统计器。
一、项目目标
程序运行后:
- 询问要输入多少个学生成绩。
- 逐个输入成绩。
- 每个成绩必须是 0 到 100 的整数。
- 输入非法时提示错误,并要求重新输入。
- 输入完成后统计:
- 最高分。
- 最低分。
- 平均分。
- 及格人数。
- 及格率。
- 程序结构要拆成方法,不把所有逻辑堆在 main 里。
最终效果类似:
请输入学生人数:3 请输入第1个学生成绩:90 请输入第2个学生成绩:abc 成绩必须是整数,请重新输入 请输入第2个学生成绩:80 请输入第3个学生成绩:59 ===== 成绩统计 ===== 最高分:90 最低分:59 平均分:76.33 及格人数:2 及格率:66.67%二、先做最小版本
第一版不处理非法输入,只跑通流程。
importjava.util.Scanner;publicclassScoreStatisticsAppV1{publicstaticvoidmain(String[]args){Scannerscanner=newScanner(System.in);System.out.print("请输入学生人数:");intcount=scanner.nextInt();int[]scores=newint[count];for(inti=0;i<count;i++){System.out.print("请输入第"+(i+1)+"个学生成绩:");scores[i]=scanner.nextInt();}intmax=scores[0];intmin=scores[0];intsum=0;intpassCount=0;for(intscore:scores){if(score>max){max=score;}if(score<min){min=score;}if(score>=60){passCount++;}sum+=score;}doubleaverage=sum*1.0/count;doublepassRate=passCount*100.0/count;System.out.println("最高分:"+max);System.out.println("最低分:"+min);System.out.println("平均分:"+average);System.out.println("及格人数:"+passCount);System.out.println("及格率:"+passRate+"%");}}这个版本能跑,但有问题:
- 学生人数如果输入 0,会访问
scores[0]出错。 - 成绩如果输入 abc,会直接异常。
- 成绩如果输入 200,也会被接受。
- main 太长。
- 统计逻辑没有拆方法。
项目开发通常就是这样:先跑通,再逐步加校验和结构。
三、设计程序结构
我们把程序拆成几类方法:
读取学生人数 读取一个合法成绩 判断字符串是否整数 查找最高分 查找最低分 计算平均分 统计及格人数 打印统计结果对应方法:
readStudentCount readScore isInteger findMax findMin calculateAverage countPassed printReport拆方法不是为了显得高级,而是为了让每段逻辑有名字、有边界。
四、完整版本代码
importjava.util.Scanner;publicclassScoreStatisticsApp{publicstaticvoidmain(String[]args){Scannerscanner=newScanner(System.in);intstudentCount=readStudentCount(scanner);int[]scores=newint[studentCount];for(inti=0;i<studentCount;i++){scores[i]=readScore(scanner,i+1);}printReport(scores);}publicstaticintreadStudentCount(Scannerscanner){while(true){System.out.print("请输入学生人数:");Stringtext=scanner.nextLine();if(!isInteger(text)){System.out.println("学生人数必须是整数,请重新输入");continue;}intcount=Integer.parseInt(text);if(count<=0){System.out.println("学生人数必须大于0,请重新输入");continue;}returncount;}}publicstaticintreadScore(Scannerscanner,intindex){while(true){System.out.print("请输入第"+index+"个学生成绩:");Stringtext=scanner.nextLine();if(!isInteger(text)){System.out.println("成绩必须是整数,请重新输入");continue;}intscore=Integer.parseInt(text);if(score<0||score>100){System.out.println("成绩必须在0到100之间,请重新输入");continue;}returnscore;}}publicstaticbooleanisInteger(Stringtext){if(text==null||text.isEmpty()){returnfalse;}for(inti=0;i<text.length();i++){charch=text.charAt(i);if(ch<'0'||ch>'9'){returnfalse;}}returntrue;}publicstaticintfindMax(int[]scores){intmax=scores[0];for(intscore:scores){if(score>max){max=score;}}returnmax;}publicstaticintfindMin(int[]scores){intmin=scores[0];for(intscore:scores){if(score<min){min=score;}}returnmin;}publicstaticdoublecalculateAverage(int[]scores){intsum=0;for(intscore:scores){sum+=score;}returnsum*1.0/scores.length;}publicstaticintcountPassed(int[]scores){intcount=0;for(intscore:scores){if(score>=60){count++;}}returncount;}publicstaticvoidprintReport(int[]scores){intmax=findMax(scores);intmin=findMin(scores);doubleaverage=calculateAverage(scores);intpassCount=countPassed(scores);doublepassRate=passCount*100.0/scores.length;System.out.println();System.out.println("===== 成绩统计 =====");System.out.println("最高分:"+max);System.out.println("最低分:"+min);System.out.printf("平均分:%.2f%n",average);System.out.println("及格人数:"+passCount);System.out.printf("及格率:%.2f%%%n",passRate);}}五、逐段解释
1. main 方法只保留主流程
intstudentCount=readStudentCount(scanner);int[]scores=newint[studentCount];for(inti=0;i<studentCount;i++){scores[i]=readScore(scanner,i+1);}printReport(scores);main 读起来像流程说明:
读取学生人数 -> 创建成绩数组 -> 逐个读取成绩 -> 打印报告这就是抽方法的好处。
2. readStudentCount 为什么用 while true
while(true){...if(不合法){continue;}returncount;}这里的逻辑是:只要用户没输入合法人数,就一直要求重新输入。一旦合法,return count直接结束方法。
这种循环适合“直到输入合法”的场景。
3. isInteger 为什么自己写
我们先不用异常处理,是因为异常还没正式讲。第一阶段用字符判断更直观:
for(inti=0;i<text.length();i++){charch=text.charAt(i);if(ch<'0'||ch>'9'){returnfalse;}}它逐个检查字符是否在'0'到'9'之间。
这个方法当前不支持负数,因为学生人数和成绩都不应该是负数。
4. findMax 和 findMin 为什么用 scores[0]
intmax=scores[0];因为如果分数范围以后变了,初始值写死可能不可靠。用数组第一个元素作为初始值更通用。
本项目里学生人数已经保证大于 0,所以 scores 一定不是空数组。
5. printf 中的%%
System.out.printf("及格率:%.2f%%%n",passRate);%.2f表示保留两位小数。%%表示输出一个百分号。%n表示换行。
所以这里会输出:
及格率:66.67%六、这个项目用到了哪些知识
| 知识 | 在项目中的体现 |
|---|---|
| 变量 | studentCount、scores、max、min |
| 类型 | int、double、String、boolean、int[] |
| 输入 | Scanner |
| 输出 | print、println、printf |
| 条件 | 校验人数、校验成绩 |
| 循环 | 重复输入、遍历数组 |
| continue | 输入不合法时重新循环 |
| return | 输入合法后返回结果 |
| 方法 | 拆分读取、统计、输出 |
| 数组 | 保存多个成绩 |
这就是阶段项目的意义:把分散知识串起来。
七、可以继续改进的地方
这个项目还不是最终工程。它还有很多可以改进的地方:
- 支持输入学生姓名。
- 每个学生不仅有成绩,还有学号。
- 统计优秀人数。
- 保存到文件。
- 下次启动读取历史成绩。
- 用类表示 Student。
- 用 List 替代数组。
- 用异常处理输入转换。
这些改进会在后续阶段逐步实现。现在不要急着一次做完。学习编程最重要的是每一步都知道自己为什么这么写。
八、常见错误
1.scanner.nextLine()读到空字符串
如果你混用nextInt()和nextLine(),容易出现换行残留。本项目统一使用nextLine(),避免这个问题。
2. 空数组访问 scores[0]
如果学生人数允许 0,findMax会出错。本项目在readStudentCount保证人数大于 0。
3. 平均分整数除法
错误:
returnsum/scores.length;正确:
returnsum*1.0/scores.length;4. 忘记 return 导致循环停不下来
合法输入后必须 return,否则 while true 会继续。
5. 方法拆太碎或不拆
所有逻辑堆 main 里难读;但每一行都拆方法也没必要。拆方法的原则是:这段逻辑有明确名字,并且能独立理解。
九、本章练习
增加优秀人数统计:分数大于等于 90 算优秀。
增加不及格人数统计。
修改输出,让平均分和及格率都保留 1 位小数。
增加输入班级名称,并在报告中输出。
尝试把
printReport再拆成:
printBasicReport printPassReport- 思考:如果要保存学生姓名和成绩,数组还够不够用?
十、第一阶段总结
完成这个项目,说明你已经能把第一阶段的基础知识串起来。
你应该已经具备:
- 编写 main 程序。
- 使用变量和类型。
- 从命令行读取输入。
- 使用条件校验数据。
- 使用循环重复处理。
- 使用数组保存一组数据。
- 使用方法拆分逻辑。
- 阅读基础错误并修复。
这还不是 Java 的全部,但它是继续学习面向对象的前提。下一阶段会把“学生成绩”这种散落的数据升级成对象,比如Student类。到那时,你会看到 Java 为什么总是强调类和对象。