每年到期末,学习群里总会出现同一种消息:“Java第十版第九章习题全部答案,求一份”。发消息的同学通常处于两种状态:作业马上要交而代码还没写完,或者选择题对完答案后错了一半,开始怀疑手里的答案册有问题。作为从Java SE 5用到现在Java 17的老开发,我太清楚这个标题背后的分量了——不管你是不是在用那本经典的第十版教材,第九章几乎都是Java学习路上的第一道分水岭。这一章的代码如果是自己一行行敲出来的,后面学集合、学框架、学并发都会顺很多;如果这章是靠抄答案混过去的,面试遇到手写代码的环节,前面欠的债会连本带利还回来。
1. 先别急着求答案,搞清楚你的“第九章”是哪一章
1.1 不同教材的第九章,题目风格可能完全不一样
先说一个扎心的事实:Java教材的第十版不止一本,第九章甚至不是同一个主题。有些教材把集合放在第九章,有些把异常处理放在第九章,也有些把图形界面和事件处理放在第九章。我在学习群里见到的最多求助,多半来自那本把集合当成核心章节的教材,因为集合章节的题目最灵活、代码量最大,也是最不适合直接背的一章。
| 教材体系 | 第九章常见主题 | 题目特点 |
|---|---|---|
| 经典基础教材 | 集合框架 | 大量编程题、API选型、底层原理简答题 |
| 工程向教材 | 异常处理 | 逻辑判断题、自定义异常、try/catch改写 |
| 应用型教材 | GUI/事件处理 | 界面布局、事件监听、功能联动 |
如果你手上的教材把集合放在第九章,也就是最常见的情况,那这篇博客的每一节基本都能用上;如果你的第九章叫异常处理或图形界面,下面讲的“先搭知识地图,再自证答案”的学习方法同样成立,只要把主线知识点替换成对应的章节内容。
1.2 为什么“全部答案”这个需求本身很危险
“全部答案”听起来省事,实际上会把你拖进三种困境。第一,作业交得越顺利,期末复习越空虚,因为集合章节的知识点根本不是靠背能背下来的。第二,网传答案未必是对的,很多答案是用老版本JDK写的,Java 8、Java 11、Java 17的API行为存在差异,直接拿旧答案跑新环境,运行结果可能完全不同。第三,面试环节最喜欢问集合的底层实现,HashMap如何扩容、为什么增强for循环里调用list.remove()会报异常,这类问题抄过作业的同学很难答上来。
所以我的建议一直很直接:答案可以看,但必须是自己跑通代码之后用来核对的答案,而不是交作业之前用来复制的答案。这篇博客不会罗列一堆题目的标准解,而是给你一套自己推导答案、验证答案、排查错误的方法,这套方法比任何答案册都抗用。
2. 集合章节的知识主线:把API背后的机制摸透
如果第九章真的是集合,你需要做的第一件事,就是把整章知识点整理成一张地图。集合章节内容多而杂,List、Set、Map各有各的实现类,接口层次也深,没有主线的话,题目稍微一变型就懵。
2.1 List、Set、Map三大家族的选型逻辑
集合框架可以拆成三块:List存有序可重复的数据,Set存不重复的数据,Map存键值对。表面上只是接口不同,实际上每个接口背后都连着不同的数据结构。ArrayList底层是动态数组,查询快、插入慢;LinkedList底层是双向链表,头尾插入删除快、随机访问慢;HashSet和HashMap底层是哈希表,查找接近常数时间;TreeSet和TreeMap底层是红黑树,元素天然有序。
| 接口 | 典型实现 | 底层结构 | 适用场景 |
|---|---|---|---|
| List | ArrayList | 动态数组 | 频繁随机访问、按索引遍历 |
| List | LinkedList | 双向链表 | 频繁头尾插入删除 |
| Set | HashSet | 哈希表 | 快速去重、无序存储 |
| Set | LinkedHashSet | 哈希表加链表 | 去重且保持插入顺序 |
| Set | TreeSet | 红黑树 | 去重且自动排序 |
| Map | HashMap | 哈希表 | 按键查询值 |
| Map | TreeMap | 红黑树 | 按键有序的映射 |
很多习题会给你一个场景,让你选择集合类型。记住一条主线:要索引访问选List,要快速查重选Set,要按键找值选Map,要自动排序优先考虑TreeSet或TreeMap。代码题里“统计每个单词出现次数”这类题,背后其实就是“Map存映射关系”这一条。
2.2 ArrayList扩容和HashMap哈希是简答和选择题的重灾区
期末试卷里,集合章节的简答题和选择题最爱考两个底层细节:ArrayList扩容和HashMap哈希。
ArrayList默认容量是10,每次扩容大约是原容量的1.5倍。JDK 7到JDK 8之后扩容实现有变化,但考察的核心是“为什么需要扩容”和“怎么减少扩容”。如果明确知道数据量,就应该通过构造函数指定初始容量,避免数据还没放完就多次扩容,白白浪费时间。
HashMap默认初始容量16,加载因子0.75,当元素个数超过容量乘以0.75时,HashMap会扩容为原来的两倍。Java 8之后,当某个桶的链表长度超过8,并且数组容量大于等于64时,链表会转换成红黑树,把最坏情况下的查询复杂度从O(n)降到O(log n)。
题目的陷阱往往藏在参数计算里。比如让你估算HashMap存1000个元素需要多大的初始容量,按加载因子0.75算,1000除以0.75约等于1334,而HashMap要求容量是2的幂,向上取整就得给2048的初始容量。如果只给1024,数据还没放完就会触发多次扩容,性能雪崩。
2.3 迭代器与并发修改:一动手写代码就会踩的坑
集合章节的编程题,代码写出来不难,跑起来报错很常见。最典型的报错是ConcurrentModificationException。直接用增强for遍历list,在循环体里调用list.remove(),十有八九会抛这个异常。原因很简单:迭代器内部维护了一个修改计数器,检测到集合被外部修改后就会触发fail-fast机制,宁可抛异常也不给你不确定的结果。
正确的做法是使用迭代器自己的remove()方法,或者在Java 8之后用removeIf()。这里先记住结论:遍历中修改集合,永远优先考虑迭代器自带的方法,而不是集合自己的方法。这个点我在第5节还会专门展开,因为它是集合章节编程题里翻车率最高的位置。
2.4 三个特别容易记错的判断题
集合章节的选择题里,有些说法看起来对,实际是错的。我平时辅导同学时总结出三个高频易错点。
第一,“HashSet不允许存放null”。这个说法是错的。HashSet允许放入一个null值,因为null的哈希值固定,它会落在某个桶里。真正不允许null的是TreeSet,因为TreeSet在插入时就需要比较元素大小,null无法参与比较,运行时会抛NullPointerException。
第二,“TreeSet可以对任意自定义对象直接排序”。这个说法也是错的。TreeSet要求元素实现Comparable接口,或者在创建TreeSet时传入一个Comparator,否则插入第二个对象时就会在比较环节抛ClassCastException。做题时想给自定义对象排序,要么让类实现Comparable,要么单独写Comparator,没有第三种偷懒方式。
第三,“Map的values()返回的是List”。这也是错的。Map.values()返回的是Collection视图,不是List,它不保证顺序,而且对视图的修改会直接反映到原Map。很多同学想通过values()拿到List再去排序,结果代码能编译,运行结果却不可控,本质就是没理解“视图”这个定位。
3. 别急着搜答案,先掌握三步“自证法”
你有没有经历过这种情况:题做完了,感觉答案是对的,但心里没底,最后只能去网上搜。其实代码对不对,用三个手段就能判断,根本不用依赖别人的答案。
3.1 用JShell把API行为当场验证
Java 9以上自带JShell工具,终端里输入jshell回车,就能一行一行执行Java代码。比如你想验证ArrayList的remove(int)和remove(Object)到底有什么区别,直接在JShell里敲出来,结果立刻可见。
jshell import java.util.*; var list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); list.remove(1); // 这里删的是下标1对应的元素2 list.remove(Integer.valueOf(1)); // 这里删的是值为1的元素 System.out.println(list);JShell对学习集合尤其友好。集合的API方法多,光靠记忆容易混,用JShell随手试出来的结果就是最权威的答案。我遇到不确定的API行为时,几乎不会去翻旧帖子,直接JShell跑一遍,几秒钟就出结论。
3.2 写测试代码,验证题目的边界条件
每道编程题都有隐含的边界条件:输入为空时怎么办?全部元素相同时怎么办?数据量很大时会不会超时?很多网传答案在普通用例上能跑通,一到边界就翻车。你在核对答案之前,先想清楚题目的边界,再把你自己的解法拿到边界用例上跑一遍。
举个例子,题目要求“返回去重后的列表”,至少需要测四种输入:空列表、单个元素列表、全部相同元素的列表、元素顺序被打乱的列表。只测一种用例就认为自己写对了,是新手最常见的错觉。测试用例越极端,你对自己答案的信心就越足,这才是“答案”真正的意义。
3.3 用调试器观察集合内部的数据结构
IDE的调试器不只是用来查错的,它还能让你直观看到ArrayList扩容前后的内部数组长度、HashMap桶里的链表结构。我在学习集合章节时,最喜欢在断点处展开变量面板,看HashMap的内部数组长度,亲眼看着它从16变成32。理解了扩容之后,很多题目不用死记也能推出来。
遇到题目答案有争议时,调试器里看到的事实比任何贴吧答案都可靠。你甚至可以给HashMap的resize方法加一个断点,看一眼newCap的计算过程,比背十遍“扩容为两倍”都有用。
4. 三个典型的第九章编程题解法推演
我挑了三个在集合章节出镜率最高、也最经典的编程题,演示完整解题流程。每一道都按“题目拆解、参考实现、验证方法”的顺序来讲。
4.1 统计单词出现频率
题目要求通常是这样:给定一段文本,统计每个单词出现的次数,按出现次数从高到低输出。
这类题的本质是分组聚合。拆解出来的步骤是:分割文本、构建Map、排序输出。参考实现如下:
import java.util.*; import java.util.stream.*; public class WordCounter { public static void main(String[] args) { String text = "java is easy but java is powerful"; Map<String, Long> freq = Arrays.stream(text.split(" ")) .collect(Collectors.groupingBy(w -> w, Collectors.counting())); freq.entrySet().stream() .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) .forEach(e -> System.out.println(e.getKey() + " : " + e.getValue())); } }运行结果中“java : 2”排在最前面,其余单词各出现1次。如果你不习惯流式写法,用for循环也能实现同样的效果,代码量略多,但更容易读。关键是要理解Map在这里承担“键值聚合”的角色,而不是机械背API。
验证这道题的正误,除了输出结果,还要检查空字符串输入。如果text是空串,split出来的数组长度为1,里面是一个空字符串,统计结果会是空串出现1次,这往往不是题目想要的结果。所以完整解法应该在split之前先判空,或者对空串做过滤。
4.2 对列表去重并保持原始顺序
题目要求通常是这样:给定一个整数列表,原地去重,并保持元素第一次出现的顺序。
这道题既考Set的去重能力,又考List的顺序保持,是典型的组合技巧题。最简洁的写法是用LinkedHashSet,它内部既有哈希表的查重能力,又有链表把插入顺序记录下来:
public class Deduplicate { public static void main(String[] args) { List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2, 3, 4, 1)); Set<Integer> set = new LinkedHashSet<>(list); list.clear(); list.addAll(set); System.out.println(list); // [3, 1, 2, 4] } }很多同学会用HashSet代替,结果输出变成[1, 2, 3, 4],顺序被打乱,这就是没分清HashSet和LinkedHashSet的区别。这种细节恰恰是习题要考核的点,也是面试官最喜欢追问的点。
验证方法很简单:构造几个重复元素分布在首尾的用例,比如[3, 1, 2, 3, 4, 1, 3],确保输出保留的是每个元素的第一次出现位置,而不是最后一次。
4.3 自定义对象排序
题目要求通常是这样:有一批学生对象,包含姓名和成绩,先按成绩降序排,成绩相同按姓名升序排。
这道题的考点是Comparable和Comparator的使用。可以让Student类实现Comparable,也可以单独写一个Comparator。工程上更推荐Comparator,因为它把排序规则和业务对象解耦,以后排序规则变了,不用去改业务类。
class Student { String name; int score; Student(String name, int score) { this.name = name; this.score = score; } } public class SortDemo { public static void main(String[] args) { List<Student> students = Arrays.asList( new Student("Alice", 88), new Student("Bob", 95), new Student("Cara", 88)); students.sort(Comparator.comparingInt((Student s) -> s.score).reversed() .thenComparing(s -> s.name)); students.forEach(s -> System.out.println(s.name + " : " + s.score)); } }输出顺序是Bob、Alice、Cara。注意thenComparing的用法,它是在第一排序条件并列时启用的第二条件。不理解comparing方法的话,可以先用匿名内部类写Comparator,功能一样,只是代码长一点。
验证这道题,重点看稳定排序。成绩相同的Alice和Cara,最终是否按姓名升序排列。把其中一条成绩改成和另一条相同,再看输出顺序是否符合第二排序条件,就能确认Comparator有没有写对。
5. 常见问题与排查技巧实录
这一节全是干货,我把辅导同学过程中反复遇到的高频问题整理出来,按“现象、原因、解决思路”的方式展开。
5.1 代码在IDE里能跑,为什么放到在线平台就报错
这类问题的原因通常有三个。第一,类名和文件名不一致,多数在线平台要求主类名固定为Main,你本地叫别的名字自然过不了。第二,Scanner读取输入时没有处理换行残留,整数读取完再读字符串,经常读到空行。第三,编码问题,中文注释或字符串常量在Windows命令行下编译偶尔乱码。
遇到在线平台报错,先检查主类名,再检查Scanner读取逻辑,最后把源码编码统一成UTF-8。中文内容尽量不要硬编码在代码里,可以提取到单独的资源文件,这样能避开大部分编码坑。
5.2 遍历时删除元素,为什么老是漏删或者报异常
这是集合题目里最经典的坑。看下面这段代码,它看起来没有任何问题,但实际输出会让人意外:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "b")); for (int i = 0; i < list.size(); i++) { if (list.get(i).equals("b")) { list.remove(i); } } System.out.println(list);很多同学以为输出是[a, c],实际可能是[a, c, b]。原因是每次remove之后,后面的元素会往前移动,下标i指向的元素其实已经换了,继续i++就会跳过本该被检查的元素。解决办法是倒着遍历,或者用Iterator的删除方法:
Iterator<String> it = list.iterator(); while (it.hasNext()) { if (it.next().equals("b")) { it.remove(); } }如果用的是增强for循环,循环内部直接list.remove(),会触发ConcurrentModificationException。原因和处理方法在第2.3节已经讲过,这里再提醒一次:遍历中修改集合,优先用迭代器。
5.3 网传答案为什么经常对不上你的题目
“Java第十版第九章”这个标题太模糊了,至少好几本主流教材都有对应的第十版,章节主题可能是集合、异常处理或者图形界面。就算教材相同,不同印次的修订也可能导致章节顺序调整。你搜到的答案可能是旧版的,也可能是另一本教材的。
我的经验是:先对照题目的截图或章节目录,确认版本一致,再局部核验;不要整篇照搬。凡是遇到答案和你的运行结果不一致,优先怀疑JDK版本差异,再怀疑题目条件理解偏差,最后才怀疑代码本身逻辑有问题。顺序反了,你会浪费大量时间纠结在一个本来就没错的答案上。
5.4 速查表
| 现象 | 最常见原因 | 解决思路 |
|---|---|---|
| ConcurrentModificationException | 增强for中调用list.remove() | 改用Iterator.remove()或removeIf() |
| HashSet去重后顺序变了 | 正常现象,HashSet不保证顺序 | 需要保持顺序时改LinkedHashSet |
| HashMap中文key乱码 | 控制台编码问题 | 源码和运行环境统一UTF-8 |
| 运行超时 | 数据量大还用了O(n^2)算法 | 用HashSet或Map把查找复杂度降到常数级 |
| 编译报重复符号 | 类名和其他类重名 | 用包名隔离或改类名 |
| TreeSet插入时报ClassCastException | 元素对象没实现Comparable | 让类实现Comparable或传入Comparator |
最后再说点实在的。我年轻时也借过同学的Java作业答案,后来面试被问“HashMap如何解决哈希冲突”时答不上来,场面极度尴尬。那次之后我养成了一个习惯:任何题目都先自己写一版,再去对照别人给的答案,把不一样的地方逐行debug清楚。这不是鸡汤,而是最省时间的学法——只有在你已经思考过的情况下,参考答案对你才是有效输入,否则它就是一堆你背不下来的代码。
如果你正在为“Java第十版第九章习题全部答案”焦虑,我的建议很朴素:先从第2节的知识主线开始,把List、Set、Map的底层关系理清,再挑三道编程题自己写一遍,最后用第3节的自证法验证。你亲手跑出来的输出,比你搜到的任何答案都可靠。这一章学扎实了,后面学集合操作、学Stream、学并发,都会顺畅很多。