Java基础案例教程课后题详解:数组、集合、IO与异常避坑指南
简介《Java基础案例教程第2版》课后习题答案第1部分面向正在学习Java入门课程的在校生、自学者及备考读者。内容覆盖Java开发环境、基础语法、控制结构、数组、面向对象封装继承多态、访问修饰符与变量作用域等章节参考答案包含填空题、判断题、选择题、简答题及编程题代码并整理了JDK目录作用、编译运行流程、break与continue区别等易混淆点方便对照教材逐章自测、查漏补缺。资源包内共1个docx文档压缩包大小约50KB附件结构简洁解压后即可打开查看。目前已有6173人浏览学习是Java基础阶段常用的习题参考。借助其中的简答梳理和示例程序读者能快速定位薄弱知识点理解求奇数之和、数组最大值等典型算法的实现思路对巩固课堂所学、准备阶段性考试有实用帮助。1. 课后习题答案不是抄代码先弄清这本案例教程在考什么网上搜《Java基础案例教程第2版》课后习题答案能找到不少版本但多数人拿到答案后做的事只有一件对着代码抄一遍跑通关掉。这样练完一本书遇到稍微变形的题目——比如把「求数组和」改成「求数组平均值并忽略负数」——照样写不出来。答案的真正价值不是那几行代码而是代码里藏着的判断为什么用 LinkedHashSet 而不是 HashSet为什么读取文件要指定 UTF-8为什么循环删除集合元素会抛异常。这篇笔记不打算复述某个「标准答案包」而是按真实做题顺序把它拆开从 JDK 选型和命令行编译起步到数组、字符串、集合、IO、异常这几类高频课后题的标准解法再讲五个让程序「看似对实则错」的典型坑。适合两类人一类是自学 Java 基础、刚学到面向对象、想验证自己写的答案对不对的新手另一类是带学生、需要快速指出作业问题的导师。目标只有一个——让每一道课后题变成你亲手跑通过、能讲清楚为什么的东西。2. 环境准备与最小骨架把课后习题跑起来再谈答案课后题最尴尬的时刻不是想不出解法而是代码写完不知道该按哪个按钮验证。有人用集成开发环境点一下运行就完事也有人跟着网上教程配环境配完发现javac都是未知命令。我一般建议不管最后用不用 IDE先把命令行编译运行这一条路走通。原因是编译器的报错信息是 Java 给你的第一手反馈想靠 IDE 的红波浪线替代它会错过很多对类型、异常、资源关闭的理解。2.1 JDK 版本怎么选不是越新越好选 JDK 版本这件事很多初学者直接装最新版其实没必要。这本基础案例教程的课后题主要集中在语法、面向对象、集合和 IO这些内容从 JDK 8 开始就非常稳定。选版本时看三件事教材默认版本、机器上是否已有其他项目依赖、你之后要去的岗位常用版本。常见组合是学校机房用 JDK 8企业存量项目用 JDK 8 或 11新项目慢慢往 17 迁。版本关键特性课后题建议JDK 8Lambda、Stream、接口默认方法最稳妥老教材习题全兼容JDK 11模块化完善、var、增强 HttpClient兼容性好写基础题无感知差异JDK 17密封类、统一日志等可用但部分老教程的编译参数要微调个人建议装 JDK 11 或 17 的 LTS 版本因为本教程第 2 版里多少会涉及新版 API。安装后打开终端执行javac -version能打印出版本号就说明环境变量正常。如果系统装了多个 JDK注意JAVA_HOME指向的是哪一个这决定了javac和java实际用的是哪个版本命令版本不一致是后面很多玄学报错的根源。2.2 用命令行跑通第一个课后程序从最简单的「数组求和」开始。这类题是整本书后面所有数组操作的基础虽然题目简单但能一次性暴露编码、类名、入口方法三个问题。// Exercise1.java // 课后题给定一个整型数组返回所有元素之和 public class Exercise1 { public static void main(String[] args) { int[] nums {3, 1, 4, 1, 5, 9, 2, 6}; System.out.println(sum arraySum(nums)); // 边界验证空数组不能抛异常 System.out.println(empty sum arraySum(new int[0])); } /** * 求元素和为空或长度为0时返回0 * param arr 传入的整型数组 * return 元素和 */ public static int arraySum(int[] arr) { int sum 0; for (int v : arr) { sum v; } return sum; } }javac -encoding UTF-8 Exercise1.java java Exercise1这段代码的逻辑很简单用 for-each 遍历数组累加空数组时循环体一次都不执行所以返回 0这是天然安全的写法。-encoding UTF-8是编译参数里最容易忽略的一个它告诉编译器源码文件用 UTF-8 读取不加的话在中文操作系统上按 UTF-8 保存的注释和字符串字面量会出现乱码甚至直接编译报错。arraySum方法里的边界判断不用显式写if (arr null)因为课后题默认传入非空但如果你要写得更健壮可以在方法第一行加if (arr null) return 0;。2.3 IDE 导入与运行的两点注意命令行跑通之后再用 IDE 写题会顺手很多。新建一个标准 Java 项目把Exercise1.java复制进 src 目录即可没必要用 IDE 的模板生成一堆多余文件。两点注意第一项目编译级别要和javac版本一致比如命令行是 JDK 17项目里也选 17避免出现「编译用 8、运行用 17」的错位这种错位往往以奇怪的UnsupportedClassVersionError收场。第二如果报「类找不到或无法加载主类」九成是public class的名字和文件名不一致。Java 里文件名必须等于 public 类名大小写也要完全一样这一点课后题答案里经常被默认忽略但新手手敲代码时最容易翻车。3. 典型课后题拆解数组去重、字符统计与集合排序的标准解法翻开这本案例教程的习题目录数组、字符串、集合三块占据了前半本书的大半篇幅。很多学生觉得题量太多其实核心题型就那几类给数组去重、统计字符出现次数、把对象集合按规则排序。把这些题的解法模板化后面的课程设计和综合案例不过是在这些模板上做组合。3.1 数组去重LinkedHashSet保序与双指针的取舍数组去重是出场率极高的题但答案质量差别很大。最省事的是用 HashSet但 HashSet 不保证顺序去重结果经常和题目预期不一致。标准做法是用 LinkedHashSet它在哈希表基础上维护了一条双向链表既能去重又能保持元素第一次出现的顺序。// 课后题去掉数组中重复元素保持第一次出现的顺序 import java.util.*; public class DistinctDemo { public static ListInteger distinct(int[] nums) { LinkedHashSetInteger seen new LinkedHashSet(); for (int n : nums) { seen.add(n); // 重复元素会被自动忽略 } return new ArrayList(seen); } public static void main(String[] args) { int[] data {4, 2, 2, 8, 4, 9}; System.out.println(distinct(data)); // [4, 2, 8, 9] } }LinkedHashSet.add的执行逻辑是先算哈希定位如果元素已存在直接返回 false链表顺序不变。所以上面代码的时间复杂度是 O(n)空间 O(n)。如果题目额外要求「不能使用集合类」那就退回到先Arrays.sort再双指针压缩的写法空间降到 O(1)但顺序会被打乱。做题时先看题目有没有「保持原顺序」这句话有就用 LinkedHashSet没有就按是否允许使用集合类来二选一。方案时间复杂度空间复杂度顺序保持适用场景LinkedHashSetO(n)O(n)保持课后题默认推荐HashSetO(n)O(n)不保持题目只要求去重不要求顺序排序双指针O(n log n)O(1)排序后顺序题目禁止使用集合类顺便提醒一点去重结果如果直接打印例如System.out.println(list)输出的是[4, 2, 8, 9]这种形式和很多答案示例一致。但如果你要逐行输出记得在循环里用普通 for 或 for-each别把整个 List 放进System.out.println。3.2 字符统计HashMap计数、TreeMap排序与遍历方式字符串统计是 HashMap 最经典的应用题。题目常见描述是「统计一个字符串中每个字符出现的次数并按字符升序输出」。这里有两个考点一是计数逻辑二是输出顺序。很多人的第一反应是HashMapCharacter, Integer但如果题目要求按字符顺序输出HashMap 不保证顺序得换成 TreeMap。// 课后题统计字符串中每个字符出现的次数按字符升序输出 import java.util.*; public class CharCounter { public static void countChars(String text) { if (text null || text.isEmpty()) { System.out.println(empty input); return; } MapCharacter, Integer counter new TreeMap(); for (char c : text.toCharArray()) { counter.put(c, counter.getOrDefault(c, 0) 1); } for (Map.EntryCharacter, Integer entry : counter.entrySet()) { System.out.println(entry.getKey() - entry.getValue()); } } public static void main(String[] args) { countChars(hello world); } }getOrDefault(c, 0)是计数时的惯用写法它的含义是取键 c 对应的值如果不存在就返回 0然后加 1 再放回去。这比先containsKey判断再 put 少写两行也很少出错。TreeMap 底层是红黑树按键的自然顺序排列所以遍历时字符天然就是升序。如果你要按出现次数从高到低排需要把entrySet()放进 List用Comparator.comparingInt(Map.Entry::getValue)反序比较。字符串题还有一个常见边界空字符串和 null。text.isEmpty()只判断长度是否为 0如果传入 null这一行就会 NullPointerException所以要先判断text null。很多课后题参考答案不写这个但你自己写的时候加上判题时能多拿分代码也不复杂。3.3 集合排序Comparable与Comparator哪个优先对象排序是面向对象章节的常客典型题目是「对学生按成绩降序排序成绩相同按姓名升序」。这道题的陷阱不在排序本身而在排序规则的表达能力——用Collections.sort能排但排完发现成绩相同的名字顺序乱了就是没考虑次级排序条件。// 课后题对学生对象按成绩降序成绩相同按姓名升序 import java.util.*; class Student { String name; double score; Student(String name, double score) { this.name name; this.score score; } public String toString() { return name : score; } } public class SortDemo { public static void main(String[] args) { ListStudent students Arrays.asList( new Student(Alice, 88.5), new Student(Bob, 92.0), new Student(Carol, 88.5) ); students.sort(Comparator.comparing(Student::getScore).reversed() .thenComparing(Student::getName)); System.out.println(students); } }Comparator.comparing(Student::getScore)指定按 score 升序.reversed()翻转成降序.thenComparing(Student::getName)是次级排序成绩一样的按姓名升序。这个链式调用比实现Comparable更直观而且可以随时换排序规则。如果课后题明确要求「Student 类实现 Comparable」那你就在类里加compareTo方法规则写死在类内部如果题目没限定优先用 Comparator因为它是外部策略不改类就能换排序维度。还有一个细节getScore()返回 doubleComparator.comparingDouble会更合适写法一样但避免装箱。基础题里用comparing也能跑只是多了一次自动装箱性能无伤大雅但代码风格上建议区分清楚。4. IO与异常习题案例题里最容易丢分的两个得分点学完集合和面向对象教材后半部分的课后题开始从「纯计算」转向「读文件、写文件、校验输入」。这类题在纸面上看着简单实际跑起来最容易丢分因为同一道题同时牵扯资源释放、文件编码、异常处理三件事。很多学生的答案思路对但忘了关流、没用 try-with-resources、异常信息直接吞掉这三点在案例教程的第 2 版里几乎是必考。4.1 用try-with-resources替换传统IO传统的 IO 习题答案长这样FileReader套BufferedReader读完在finally里close()。这个写法没错但代码啰嗦而且如果close()本身抛异常finally 块里的处理逻辑会很难看。JDK 8 之后的标准答案是 try-with-resources。// 课后题读取data.txt统计行数并打印前3行 import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.io.*; public class ReadLines { public static void main(String[] args) { Path path Paths.get(data.txt); int count 0; try (BufferedReader reader Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; while ((line reader.readLine()) ! null) { if (count 3) { System.out.println(line); // 只打印前3行 } count; } System.out.println(total lines count); } catch (IOException e) { System.err.println(读取失败: e.getMessage()); } } }try (...)括号里的资源会在 try 块结束时自动调用close()不需要写 finally这是 Java 7 引入、Java 8 开始普及的写法。Files.newBufferedReader比new FileReader强的一点是第二个参数可以显式指定字符集比如这里指定StandardCharsets.UTF_8避免不同操作系统默认编码不一致导致乱码。Paths.get(data.txt)里的路径是相对路径取决于启动命令所在的目录。运行前确认 data.txt 和.class文件在同一层目录或者直接传绝对路径否则会翻车在 FileNotFoundException 上。异常处理这里只写了catch (IOException)这是最小可用的版本。如果你还想看是文件不存在还是读取权限问题可以在 catch 里用e instanceof FileNotFoundException做区分或者直接打印异常堆栈e.printStackTrace()课后题答案里这两种都能接受。4.2 自定义异常把业务规则写进校验逻辑异常章节的课后题有两种套路一种是考 try-catch-finally 的基本语法另一种是自定义异常类。后者更能体现对受检异常和非受检异常的理解。常见题目「写一个成绩校验方法成绩在 0 到 100 之间合法否则抛出异常」。// 课后题成绩必须在0~100否则抛出异常 class ScoreOutOfRangeException extends Exception { public ScoreOutOfRangeException(String message) { super(message); } } public class ScoreValidator { public static void setScore(int score) throws ScoreOutOfRangeException { if (score 0 || score 100) { throw new ScoreOutOfRangeException(成绩必须在0~100当前值 score); } // 合法成绩的后续逻辑 System.out.println(score score); } public static void main(String[] args) { try { setScore(120); } catch (ScoreOutOfRangeException e) { System.err.println(e.getMessage()); } } }自定义异常的关键是选父类。extends Exception表示受检异常调用setScore的方法必须 try-catch 或继续 throws否则编译不过——这是教材喜欢的设计因为它强制你写出完整的异常处理流程。extends RuntimeException则相反抛出不强制捕获适合参数校验这种「调用方应该提前避免」的场景。做题时先看题目有没有「必须捕获」的要求没有的话我更推荐 RuntimeException代码更简洁也不会污染方法签名。还有一点值得说异常信息里拼上当前值当前值120看起来是小事但排查问题时非常有用。课后题答案经常只写一句固定的「成绩不合法」实际工程里这样写会被同事骂——没有上下文信息的异常等于没说。4.3 把习题答案改造成命令行小工具一本案例教程里课后题和综合案例的区别在于课后题的数据是写死在代码里的综合案例的数据是外部输入的。把写死的数据改成从命令行参数读取是一道题从「作业」变成「工具」的分水岭。public static void main(String[] args) { if (args.length 2) { System.out.println(用法: java CharCounter 目标字符串 统计字符); return; } // args[0]是目标字符串args[1]是待统计的字符 String target args[0]; char c args[1].charAt(0); int count 0; for (char ch : target.toCharArray()) { if (ch c) { count; } } System.out.println(字符 c 出现次数: count); }args是 JVM 启动时把命令行里空格分隔的参数打包成的字符串数组args[0]是第一个参数。运行方式是java CharCounter hello world o注意带空格的字符串要用引号包住否则会被拆成两个参数。这样改造之后同一道题可以反复用不同输入验证不用每次改源码重新编译。我一般建议每道课后题做完后都顺手把 main 方法改成能接收参数的结构后面做课程设计时会发现几乎是现成的。5. 习题自查避坑5个让程序「看似对实则错」的典型坑下面这 5 个坑是我看别人交上来的课后作业时遇到频率最高的。每一条都是「程序能跑、输出好像也对」但换个输入、换个机器就露馅。建议对照自己的代码逐条检查。5.1 中文乱码输出全是问号现象程序里System.out.println(姓名)输出到终端变成????或者编译时报「错误编码GBK的不可映射字符」。原因源码文件是 UTF-8 编码但编译时用了系统默认编码或者源码是 GBK运行时终端按 UTF-8 显示两边对不上。解决编译时显式指定编码运行前把终端切到 UTF-8。命令行写法是javac -encoding UTF-8 Exercise1.javaWindows 系统下可以临时执行chcp 65001把控制台切到 UTF-8再运行java Exercise1。这是新手最常撞见的第一个坑本质上不是 Java 语法问题是环境编码问题。5.2 浮点数比较金额算完不等于预期现象循环里累加 10 次 0.1最后和 1.0 比较返回false。原因double 用二进制小数表示十进制小数0.1 在二进制里是无限循环小数累加过程有误差。解决涉及精确金额的题目用 BigDecimalBigDecimal a new BigDecimal(0.1); BigDecimal b new BigDecimal(0.2); BigDecimal c a.add(b); System.out.println(c.compareTo(new BigDecimal(0.3)) 0); // true注意new BigDecimal(0.1)的构造参数是字符串不要写成new BigDecimal(0.1)后者是把已经不精确的 double 转进去误差已经存在了。比较两个 BigDecimal 用compareTo不要用equals因为 1.0 和 1.00 的 scale 不同equals 会判为不相等。5.3 集合遍历时删除元素ConcurrentModificationException现象在 for-each 循环里对List调remove()运行时抛ConcurrentModificationException。原因for-each 语法糖底层是迭代器遍历迭代器会维护一个修改计数循环外删除元素导致计数不一致。解决如果是简单遍历删除用迭代器的remove()IteratorInteger it list.iterator(); while (it.hasNext()) { if (it.next() % 2 0) { it.remove(); // 通过迭代器删除安全 } }如果不需要原地修改更推荐用 Stream 过滤后收集成新列表原列表保持不变这是 JDK 8 之后更现代的做法。5.4 Scanner的nextLine读到空串现象先int n sc.nextInt();再String s sc.nextLine();s 拿到的永远是一行空的。原因nextInt()只消费掉了数字 token输入行末尾的换行符还在缓冲区nextLine()读到的第一个字符就是那个换行符所以返回空串。解决统一用nextLine()读行然后手动解析或者在使用nextLine()前多调一次int n Integer.parseInt(sc.nextLine()); // 读数字也走nextLine再转int String s sc.nextLine(); System.out.println(s s);这个坑在课后题答案里很少被提及但几乎所有第一次用 Scanner 的人都踩过。建议从第一天就养成「所有输入都走 nextLine需要数字再转」的习惯。5.5 文件路径硬编码换台机器就找不到文件现象代码里写Files.readAllLines(Paths.get(data.txt))在自己电脑上跑没问题把 .java 和 .class 一起打包发给别人对方报 FileNotFoundException。原因相对路径是相对于「启动 Java 命令时所在的目录」而不是 .class 文件所在的目录。对方在另一个目录执行java路径自然就失效了。解决把路径改成基于当前工作目录显式拼接或放到 classpath 里用类加载器读取。最简单的兜底写法Path path Paths.get(System.getProperty(user.dir), data.txt);user.dir是 JVM 启动目录这样至少路径是按「工作目录 文件名」明确构造的换个目录运行只要把 data.txt 放到对应位置就行。要想更稳可以把文件放到源码资源的同级目录用ClassLoader.getResource(data.txt)读取那种方案适合综合案例课后题先把user.dir的版本理解透即可。6. 从抄答案到写测试把课后题验证从玄学变成确定课后题做完怎么知道答案对不对多数人的验证方式是「跑一遍看输出和印象里的答案差不多」这其实是把验证交给了运气。更靠谱的做法是为每道题写一个可重复执行的最小测试骨架不依赖任何第三方库跑一次就知道所有边界过没过。// Exercise1Test.java public class Exercise1Test { public static void main(String[] args) { // 用例1普通数组 check(arraySum(new int[]{1, 2, 3}) 6, 普通数组求和); // 用例2空数组返回0 check(arraySum(new int[0]) 0, 空数组返回0); // 用例3负数参与求和 check(arraySum(new int[]{-1, 2, -3}) -2, 负数求和); System.out.println(全部用例通过); } static void check(boolean condition, String name) { if (condition) { System.out.println([PASS] name); } else { System.err.println([FAIL] name); System.exit(1); } } }这套骨架的思路很简单check方法接收条件布尔值和用例名条件为 false 时打印 FAIL 并终止程序。每个用例的输入由你自己挑挑的时候想三个方向正常值、边界值空数组、0、最大值、异常值负数、超范围。这三个方向覆盖下来一道题的正确性基本就稳了。写完测试再回来看答案会有一个很明显的感受变化之前是「我看答案觉得对」现在是「我设计用例证明它对」。这两个心态的区别基本就是新手和熟手的分界线。我早年刷题时也对着答案抄直到有一次课程设计被某导师一句「边界条件呢」问住才回头把每一道题补上测试。从那以后我收到别人的习题答案第一反应也是问一句你跑过哪几组输入你手头这批课后题建议按这个顺序重跑一遍——环境、编译、拆题、避坑、写测试。希望帮到你。本文还有配套的精品资源点击获取