
1. 项目概述与核心需求解析“评委打分”这个场景无论是校园里的演讲比赛、公司内部的技能评比还是电视上的选秀节目都太常见了。核心逻辑也出奇的一致去掉一个最高分去掉一个最低分然后计算剩余分数的平均值。这个逻辑看似简单但对于Java初学者或者正在准备面试的朋友来说它却是一个绝佳的综合性练手项目。它几乎串联了Java SE阶段最核心的几个知识点控制台输入Scanner、数组或集合的操作、循环遍历、条件判断以及基本的算术运算。很多人拿到这个题目第一反应可能就是“这太简单了”然后草草写几行代码了事。但如果你愿意稍微深入一点就会发现里面藏着不少“坑”和可以优化的点。比如评委人数是固定的还是可变的分数是整数还是小数如果出现并列的最高分或最低分是只去掉一个还是全部去掉这些边界情况恰恰是面试官最喜欢追问的地方也是区分“能写代码”和“能写好代码”的关键。所以我们今天不单单是完成一个功能而是要把它当作一个完整的微型项目来剖析。我会从最基础的版本开始一步步迭代加入异常处理、考虑更复杂的情况并分享一些我在实际开发中总结出来的心得和容易踩的坑。无论你是刚学完Java基础想找项目练手还是在为面试刷题做准备相信这篇内容都能给你带来一些实实在在的收获。2. 基础版本实现从零开始构建打分系统我们先从最直观、最易理解的版本开始。这个版本假设评委人数固定分数为整数并且我们只使用最基础的数组和循环。2.1 环境准备与核心工具Scanner在Java中要从控制台获取用户的输入最常用的就是java.util.Scanner类。它就像是一个“扫描仪”可以从各种输入源如系统标准输入System.in中读取数据。import java.util.Scanner; public class JudgeScoreBasic { public static void main(String[] args) { // 创建Scanner对象绑定到标准输入流 Scanner scanner new Scanner(System.in); System.out.print(请输入评委人数); int judgeCount scanner.nextInt(); // 读取下一个整数 // ... 后续逻辑 scanner.close(); // 使用完毕后关闭释放资源 } }注意Scanner的nextInt()、nextDouble()等方法在读取数据时不会消耗输入流中的换行符\n。如果你在读取数字后紧接着要读取字符串比如nextLine()这个残留的换行符会被立刻读取导致字符串读取为空。一个常见的处理技巧是在nextInt()后多加一句scanner.nextLine()来“吞掉”这个换行符。2.2 数据存储与收集数组的运用确定了评委人数后我们需要一个容器来存储每一位评委给出的分数。数组Array是最简单、最直接的顺序存储结构。// 根据评委人数创建分数数组 int[] scores new int[judgeCount]; System.out.println(请依次输入 judgeCount 位评委的分数); for (int i 0; i judgeCount; i) { System.out.print(第 (i 1) 位评委分数); scores[i] scanner.nextInt(); }这里我们用了一个for循环来依次填充数组。数组下标从0开始但给用户提示时我们从1开始更符合日常习惯。2.3 核心逻辑实现查找极值与计算均值这是整个程序的心脏部分。我们需要遍历数组找出最大值和最小值同时计算所有分数的总和。// 初始化最大值和最小值。通常将最大值设为一个非常小的数最小值设为一个非常大的数。 // 也可以直接使用数组的第一个元素进行初始化这样更安全。 int maxScore scores[0]; int minScore scores[0]; int sum scores[0]; // 总和也从第一个元素开始累加 // 从数组的第二个元素开始遍历下标1 for (int i 1; i scores.length; i) { int currentScore scores[i]; sum currentScore; // 累加总分 // 更新最高分 if (currentScore maxScore) { maxScore currentScore; } // 更新最低分 if (currentScore minScore) { minScore currentScore; } }找到极值并有了总分后计算平均分的公式就很简单了(总分 - 最高分 - 最低分) / (评委人数 - 2)。这里有一个细节除数可能是0吗如果评委人数小于3去掉两个分数后就没有分数可平均了。这是一个重要的边界情况必须处理。// 计算有效评委人数去掉最高最低分后 int validJudgeCount scores.length - 2; // 边界情况处理如果评委人数不足3人则无法进行去掉最高最低分的操作 if (validJudgeCount 0) { System.out.println(评委人数不足无法计算去掉最高最低分后的平均分。); // 这里可以选择直接计算平均分或者结束程序 double average (double) sum / scores.length; System.out.println(所有分数的平均分为 average); } else { // 计算最终平均分 double finalAverage (double)(sum - maxScore - minScore) / validJudgeCount; // 格式化输出保留两位小数 System.out.printf(去掉一个最高分%d去掉一个最低分%d%n, maxScore, minScore); System.out.printf(最终平均分是%.2f%n, finalAverage); }注意类型转换(double)这是为了进行浮点数除法得到带小数的精确结果。如果使用整数除法结果会被截断。2.4 基础版本完整代码与测试将以上部分组合起来就是一个完整的基础版本。import java.util.Scanner; public class JudgeScoreBasic { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.print(请输入评委人数); int judgeCount scanner.nextInt(); if (judgeCount 0) { System.out.println(评委人数必须大于0。); scanner.close(); return; // 结束程序 } int[] scores new int[judgeCount]; System.out.println(请依次输入 judgeCount 位评委的分数); for (int i 0; i judgeCount; i) { System.out.print(第 (i 1) 位评委分数); scores[i] scanner.nextInt(); } // 查找最大值、最小值并求和 int maxScore scores[0]; int minScore scores[0]; int sum scores[0]; for (int i 1; i scores.length; i) { int currentScore scores[i]; sum currentScore; if (currentScore maxScore) { maxScore currentScore; } if (currentScore minScore) { minScore currentScore; } } // 计算并输出结果 int validCount scores.length - 2; if (validCount 0) { System.out.println(评委人数不足3人无法去掉最高最低分。); double average (double) sum / scores.length; System.out.println(所有分数的平均分为 average); } else { double finalAverage (double)(sum - maxScore - minScore) / validCount; System.out.println(最高分 maxScore); System.out.println(最低分 minScore); System.out.println(最终平均分 finalAverage); // 使用printf格式化输出 System.out.printf(最终平均分保留两位小数%.2f%n, finalAverage); } scanner.close(); } }测试一下 输入评委人数5分数依次为90, 85, 92, 88, 95。 程序会输出最高分95最低分85总和450。去掉极值后有效分数为90, 92, 88总和270平均分90.0。逻辑正确。3. 进阶优化处理边界与提升健壮性基础版本能跑通但离“健壮”还差得远。用户可能输入非数字评委人数可能输入负数分数可能超出合理范围比如0-100分制。我们一步步来加固它。3.1 输入验证与异常处理Scanner的nextInt()方法在用户输入非整数时会抛出InputMismatchException。我们必须捕获这个异常并给用户友好的提示。import java.util.InputMismatchException; // ... 其他import int judgeCount 0; boolean validInput false; while (!validInput) { System.out.print(请输入评委人数正整数); try { judgeCount scanner.nextInt(); if (judgeCount 0) { validInput true; } else { System.out.println(输入错误评委人数必须是正整数。); } } catch (InputMismatchException e) { System.out.println(输入错误请输入一个整数。); scanner.next(); // 清除缓冲区中错误的输入避免死循环 } }对于分数的输入我们也需要类似的验证。可以封装一个方法/** * 获取指定范围内的整数分数 * param scanner Scanner对象 * param prompt 提示信息 * param min 最小有效值 * param max 最大有效值 * return 有效的分数 */ public static int getValidScore(Scanner scanner, String prompt, int min, int max) { while (true) { System.out.print(prompt); try { int score scanner.nextInt(); if (score min score max) { return score; } else { System.out.printf(分数必须在%d到%d之间请重新输入。%n, min, max); } } catch (InputMismatchException e) { System.out.println(输入错误请输入一个整数。); scanner.next(); // 清除无效输入 } } }然后在主循环中调用for (int i 0; i judgeCount; i) { scores[i] getValidScore(scanner, 第 (i 1) 位评委分数0-100, 0, 100); }3.2 并列最高分/最低分的处理策略这是一个容易忽略的边界情况。假设分数是 [95, 95, 80, 90]最高分95出现了两次。基础版本的逻辑是“去掉一个最高分和一个最低分”那么是只去掉一个95还是两个都去掉这取决于业务规则。策略一只去掉一个实例基础版本逻辑我们的基础版本在求和时sum包含了所有分数。在最后计算时我们只减去了maxScore和minScore各一次。这意味着如果最高分有多个我们只扣除了其中一个最高分的值剩下的最高分仍然参与平均。这符合“去掉一个最高分”的字面意思。同理最低分也是如此。策略二去掉所有最高/最低分实例如果需要去掉所有并列的极值我们就不能在最后简单地减去一个值。我们需要在遍历时记录下哪些分数是最高分哪些是最低分或者在计算总和时就不把这些分数加进去。修改求和的循环int maxScore scores[0]; int minScore scores[0]; int sum 0; int maxCount 0; int minCount 0; // 第一轮遍历找出最大值和最小值 for (int score : scores) { if (score maxScore) maxScore score; if (score minScore) minScore score; } // 第二轮遍历计算总和并统计极值出现次数 for (int score : scores) { sum score; if (score maxScore) maxCount; if (score minScore) minCount; } // 计算最终平均分 int totalRemoved maxCount minCount; int validCount scores.length - totalRemoved; if (validCount 0) { System.out.println(所有分数都是最高分或最低分无法计算平均分。); } else { // 减去所有最高分和最低分的总和 double finalAverage (double)(sum - maxScore * maxCount - minScore * minCount) / validCount; System.out.printf(去掉了 %d 个最高分(%d分) %d 个最低分(%d分)。%n, maxCount, maxScore, minCount, minScore); System.out.printf(最终平均分是%.2f%n, finalAverage); }选择哪种策略一定要和需求方确认清楚。在面试中如果能主动提出这个边界情况并给出解决方案绝对是加分项。3.3 使用集合ArrayList的动态版本基础版本使用数组要求评委人数固定。如果我们希望程序更灵活允许中途结束输入或者动态增加评委ArrayList是更好的选择。import java.util.ArrayList; import java.util.Scanner; public class JudgeScoreDynamic { public static void main(String[] args) { Scanner scanner new Scanner(System.in); ArrayListInteger scoreList new ArrayList(); System.out.println(请输入评委分数输入-1结束输入); while (true) { System.out.print(分数-1结束); try { int input scanner.nextInt(); if (input -1) { break; // 结束输入 } if (input 0 input 100) { // 假设分数范围0-100 scoreList.add(input); } else { System.out.println(分数应在0-100之间已忽略。); } } catch (InputMismatchException e) { System.out.println(输入无效请输入整数。); scanner.next(); } } if (scoreList.size() 3) { System.out.println(有效分数不足3个无法计算。); scanner.close(); return; } // 将ArrayList转换为数组方便后续操作也可以直接用ArrayList操作 // 这里为了复用之前的逻辑先转换 int[] scores new int[scoreList.size()]; for (int i 0; i scoreList.size(); i) { scores[i] scoreList.get(i); } // ... 后续查找极值、计算平均分的逻辑与之前相同 // 注意如果使用“去掉所有极值”的策略用ArrayList会更方便可以直接remove元素。 } }使用ArrayList的好处是代码更灵活内存使用也更高效动态扩容。缺点是对于纯粹的数字遍历和计算其性能比数组略差有自动装箱/拆箱和边界检查开销但在这种小规模场景下完全可以忽略。4. 架构与设计模式思考让代码更优雅当我们把基本功能做稳定后可以思考一下代码的组织结构。一个简单的控制台程序似乎用不上设计模式但良好的分层和职责分离能让代码更清晰、更易测试、更易维护。4.1 职责分离MVC思想的简单应用我们可以粗略地将程序分为三层模型Model负责数据和核心业务逻辑。比如一个ScoreCalculator类它接收一个分数列表负责计算最高分、最低分、平均分。视图View负责与用户交互输入和输出。在我们的控制台程序中就是main方法里那些System.out.print和Scanner操作。控制器Controller负责协调模型和视图。它从视图获取输入传递给模型处理再将模型的结果返回给视图展示。我们来重构一下代码1. 模型类 ScoreCalculatorpublic class ScoreCalculator { /** * 计算去掉一个最高分和一个最低分后的平均分 * param scores 分数数组 * return 平均分 * throws IllegalArgumentException 如果数组为null或长度小于3 */ public static double calculateAverage(int[] scores) { if (scores null || scores.length 3) { throw new IllegalArgumentException(分数数组不能为null且长度必须大于等于3); } int max scores[0]; int min scores[0]; int sum scores[0]; for (int i 1; i scores.length; i) { int current scores[i]; sum current; if (current max) max current; if (current min) min current; } return (double)(sum - max - min) / (scores.length - 2); } /** * 计算去掉所有最高分和最低分后的平均分 * param scores 分数数组 * return 包含平均分和移除信息的对象这里简单用数组返回 */ public static Object[] calculateAverageRemoveAll(int[] scores) { // ... 实现逻辑返回平均分、最高分、最低分、移除个数等信息 // 为了简化这里不展开实际可以用一个自定义的Result类 return null; } }2. 视图和控制器在Main类中import java.util.Scanner; import java.util.InputMismatchException; public class JudgeScoreMVC { public static void main(String[] args) { // View Controller Scanner scanner new Scanner(System.in); int[] scores null; try { // 1. 获取输入View scores getScoresFromUser(scanner); // 2. 调用模型处理Controller double avg ScoreCalculator.calculateAverage(scores); // 3. 展示结果View System.out.printf(最终平均分%.2f%n, avg); } catch (IllegalArgumentException e) { System.out.println(计算错误 e.getMessage()); } catch (Exception e) { System.out.println(发生未知错误 e.getMessage()); } finally { scanner.close(); } } private static int[] getScoresFromUser(Scanner scanner) { // ... 包含输入验证的复杂逻辑 // 返回一个有效的分数数组 return new int[]{90, 85, 92, 88, 95}; // 示例返回 } }经过这样的拆分ScoreCalculator类变得非常纯净只关心计算逻辑很容易编写单元测试。而主程序则专注于流程控制和用户交互。虽然对于这个小程序来说有点“杀鸡用牛刀”但这种思想在构建更大、更复杂的系统时至关重要。4.2 使用Stream API的现代写法如果你使用的是Java 8及以上版本利用Stream API可以让代码变得异常简洁和优雅。它通过声明式的方式处理集合避免了显式的循环。import java.util.Arrays; import java.util.IntSummaryStatistics; public class JudgeScoreStream { public static void main(String[] args) { int[] scores {90, 85, 92, 88, 95}; // 将int数组转换为IntStream IntSummaryStatistics stats Arrays.stream(scores).summaryStatistics(); long count stats.getCount(); int max stats.getMax(); int min stats.getMin(); long sum stats.getSum(); if (count 3) { System.out.println(分数个数不足。); return; } double average (double)(sum - max - min) / (count - 2); System.out.println(最高分 max); System.out.println(最低分 min); System.out.printf(平均分%.2f%n, average); // 更极致的单行写法可读性稍差但很酷 double avg Arrays.stream(scores) .sorted() // 排序 .skip(1) // 跳过最小的一个最低分 .limit(scores.length - 2) // 限制长度相当于也去掉了最大的一个 .average() // 求平均 .orElse(Double.NaN); // 如果流为空返回NaN System.out.printf(使用Stream排序法计算的平均分%.2f%n, avg); } }Arrays.stream(scores).summaryStatistics()这一行代码直接帮我们拿到了总和、最大值、最小值、平均值和个数。sorted().skip(1).limit(n-2).average()这个链式调用则展示了另一种思路先排序然后跳过第一个最低分再只取前n-2个自然就去掉了最后一个即最高分最后求平均。Stream API的代码非常简洁但要注意它可能比手写循环有额外的开销比如排序在性能极端敏感的场合需要测试。5. 常见问题、调试技巧与性能考量即使是一个简单的程序在编写和运行过程中也会遇到各种问题。这里我总结几个常见坑点和调试方法。5.1 典型问题排查清单问题现象可能原因解决方案输入数字后程序直接结束或跳过下一次输入nextInt()后残留换行符被nextLine()读取在nextInt()后调用一次scanner.nextLine()消耗换行符计算出的平均分总是整数小数部分为0整数除法确保在除法运算前将被除数或除数转换为double类型如(double) sum / count数组索引越界ArrayIndexOutOfBoundsException访问了不存在的数组下标如scores[-1]或scores[scores.length]检查循环条件确保下标在0到length-1之间输入非数字时程序崩溃或进入死循环未处理InputMismatchException使用try-catch捕获异常并在catch块中调用scanner.next()清除错误输入最高分/最低分初始化错误导致结果不对初始值设置不合理如max0但所有分数都是负数使用数组的第一个元素来初始化max和min是最安全的方法当所有分数相同时计算有效人数为负未检查validCount scores.length - 2的结果在计算前判断if (scores.length 3)进行特殊处理5.2 调试技巧如何定位逻辑错误当程序运行结果不符合预期时不要急于重写代码。学会调试是程序员的基本功。打印中间变量这是最朴素也最有效的方法。在关键步骤后打印出变量的值。// 在查找极值的循环里 for (int i 1; i scores.length; i) { int currentScore scores[i]; sum currentScore; System.out.println(当前分数 currentScore , 当前总和 sum); // 调试语句 if (currentScore maxScore) { maxScore currentScore; System.out.println(更新最高分为 maxScore); // 调试语句 } // ... }使用IDE的调试器以IntelliJ IDEA或Eclipse为例。在你怀疑有问题的代码行左侧单击设置一个断点。以“Debug”模式运行程序。程序会在断点处暂停你可以将鼠标悬停在变量上查看其当前值。使用“Step Over”(F8)逐行执行“Step Into”(F7)进入方法内部“Resume”(F9)继续运行到下一个断点。在“Variables”窗口可以查看和修改变量的值。这是理解程序运行流程、定位隐蔽错误的利器。单元测试为你的核心计算方法如ScoreCalculator.calculateAverage编写单元测试。使用JUnit等框架传入各种边界情况的数组如空数组、单个元素、全部相同的元素、正序、逆序数组验证输出是否符合预期。这能从根本上保证核心逻辑的正确性。5.3 性能与扩展性浅析对于这个“评委打分”程序数据量很小通常不超过几十个所以任何写法的性能差异都可以忽略不计。但作为一种思维训练我们可以探讨一下时间复杂度无论用单次遍历找极值还是先排序再取中间值时间复杂度都是 O(n)其中n是分数个数。这是最优解。空间复杂度使用数组是 O(n)。如果分数是源源不断输入的流数据我们甚至可以在不存储所有分数的情况下计算最终结果——只需要动态维护当前的总和、最大值、最小值即可。当新分数到来时更新这些值。这能将空间复杂度降到 O(1)。// 流式处理伪代码 int sum 0, max Integer.MIN_VALUE, min Integer.MAX_VALUE, count 0; while (有新的分数 score) { sum score; count; if (score max) max score; if (score min) min score; } // 最终计算时依然需要 count 3 double avg (double)(sum - max - min) / (count - 2);扩展性如果业务规则变化比如“去掉两个最高分和两个最低分”或者“计算加权平均分”我们基础版本的代码只需要修改计算部分。而如果采用了MVC或策略模式的设计扩展起来会更加轻松只需增加新的计算策略类而不影响其他部分。6. 从项目到面试可能被问到的深入问题如果你在面试中被要求手写这个程序或者面试官看到你简历上有这个项目他可能会沿着以下几个方向深入提问考察你的知识广度和思考深度如果分数不是整数是小数怎么办将int数组改为double数组。注意Scanner用nextDouble()读取。比较大小和求和的逻辑完全一致。但要注意浮点数的精度问题对于严格的金融计算可能会使用BigDecimal。如果不允许使用数组你还有什么方法可以使用ArrayList。如果连集合都不能用可以在输入过程中动态维护sum,max,min,count这就是上面提到的流式处理思路。但需要一次性输入所有分数或者改变交互逻辑。你的程序是线程安全的吗目前的版本所有变量都是局部变量在单线程的main方法中运行是线程安全的。但如果ScoreCalculator的方法被设计为静态工具方法并被多个线程同时调用且参数是共享的数组或集合那么就需要考虑线程安全问题。不过对于这个简单的计算任务通常不需要。如何为这个程序编写单元测试针对ScoreCalculator类使用JUnit。测试用例应包括正常情况、边界情况如刚好3个分数、异常情况如数组为null、长度不足。示例Test public void testCalculateAverage_Normal() { int[] scores {90, 85, 92, 88, 95}; double expected (909288) / 3.0; // (450-95-85)/3 270/390 double actual ScoreCalculator.calculateAverage(scores); assertEquals(expected, actual, 0.001); // 第三个参数是允许的误差 } Test(expected IllegalArgumentException.class) public void testCalculateAverage_InvalidInput() { ScoreCalculator.calculateAverage(new int[]{90, 85}); // 长度不足应抛异常 }如果评委人数非常多比如上百万你的方法还有效吗时间复杂度 O(n) 仍然是可以接受的遍历一次是必须的。空间上如果一次性加载所有分数到数组内存可能压力很大。此时流式处理不保存所有数据是更好的选择。还可以考虑将数据分片用MapReduce的思想并行计算每个分片的总和、最大值、最小值然后再汇总。但这对于本地程序来说过于复杂了。把这个简单的项目吃透背后涉及的输入输出、数据结构、算法逻辑、异常处理、代码设计、测试调试等知识点就都串联起来了。它就像一颗种子能生长出很多编程必备的枝干。下次当你再看到“评委打分”时希望你的思路不再局限于那几行求平均值的代码而是能看到一个更广阔的程序设计世界。