
第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){ScannerscannernewScanner(System.in);System.out.print(请输入学生人数);intcountscanner.nextInt();int[]scoresnewint[count];for(inti0;icount;i){System.out.print(请输入第(i1)个学生成绩);scores[i]scanner.nextInt();}intmaxscores[0];intminscores[0];intsum0;intpassCount0;for(intscore:scores){if(scoremax){maxscore;}if(scoremin){minscore;}if(score60){passCount;}sumscore;}doubleaveragesum*1.0/count;doublepassRatepassCount*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){ScannerscannernewScanner(System.in);intstudentCountreadStudentCount(scanner);int[]scoresnewint[studentCount];for(inti0;istudentCount;i){scores[i]readScore(scanner,i1);}printReport(scores);}publicstaticintreadStudentCount(Scannerscanner){while(true){System.out.print(请输入学生人数);Stringtextscanner.nextLine();if(!isInteger(text)){System.out.println(学生人数必须是整数请重新输入);continue;}intcountInteger.parseInt(text);if(count0){System.out.println(学生人数必须大于0请重新输入);continue;}returncount;}}publicstaticintreadScore(Scannerscanner,intindex){while(true){System.out.print(请输入第index个学生成绩);Stringtextscanner.nextLine();if(!isInteger(text)){System.out.println(成绩必须是整数请重新输入);continue;}intscoreInteger.parseInt(text);if(score0||score100){System.out.println(成绩必须在0到100之间请重新输入);continue;}returnscore;}}publicstaticbooleanisInteger(Stringtext){if(textnull||text.isEmpty()){returnfalse;}for(inti0;itext.length();i){charchtext.charAt(i);if(ch0||ch9){returnfalse;}}returntrue;}publicstaticintfindMax(int[]scores){intmaxscores[0];for(intscore:scores){if(scoremax){maxscore;}}returnmax;}publicstaticintfindMin(int[]scores){intminscores[0];for(intscore:scores){if(scoremin){minscore;}}returnmin;}publicstaticdoublecalculateAverage(int[]scores){intsum0;for(intscore:scores){sumscore;}returnsum*1.0/scores.length;}publicstaticintcountPassed(int[]scores){intcount0;for(intscore:scores){if(score60){count;}}returncount;}publicstaticvoidprintReport(int[]scores){intmaxfindMax(scores);intminfindMin(scores);doubleaveragecalculateAverage(scores);intpassCountcountPassed(scores);doublepassRatepassCount*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 方法只保留主流程intstudentCountreadStudentCount(scanner);int[]scoresnewint[studentCount];for(inti0;istudentCount;i){scores[i]readScore(scanner,i1);}printReport(scores);main 读起来像流程说明读取学生人数 - 创建成绩数组 - 逐个读取成绩 - 打印报告这就是抽方法的好处。2. readStudentCount 为什么用 while truewhile(true){...if(不合法){continue;}returncount;}这里的逻辑是只要用户没输入合法人数就一直要求重新输入。一旦合法return count直接结束方法。这种循环适合“直到输入合法”的场景。3. isInteger 为什么自己写我们先不用异常处理是因为异常还没正式讲。第一阶段用字符判断更直观for(inti0;itext.length();i){charchtext.charAt(i);if(ch0||ch9){returnfalse;}}它逐个检查字符是否在0到9之间。这个方法当前不支持负数因为学生人数和成绩都不应该是负数。4. findMax 和 findMin 为什么用 scores[0]intmaxscores[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]如果学生人数允许 0findMax会出错。本项目在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 为什么总是强调类和对象。