这份“Java测验4”其实是我给自己布置的一套自测题复盘。做了这么多年Java开发,我每年都会挑几套基础题来检验基本功,不是为了面试前临时抱佛脚,而是想看看哪些细节已经开始模糊了。测验4这套题覆盖的范围很典型:面向对象、集合框架、JVM内存、Lambda与Stream、枚举、常用类、异常处理,还有几个在真实项目里特别容易踩的编译运行问题。如果你正在准备Java面试,或者刚学完Java基础想检验掌握程度,这套题的分析应该对你有用。
我在整理这套测验时,并没有像网上很多“Java面试八股文”一样只给答案,而是把每个考点背后的设计逻辑、容易忽略的边界情况、以及实际开发中对应的场景都梳理了一遍。因为题可以背,但理解不会骗人。接下来我按测验的模块顺序,把整套题的核心内容拆开讲,同时把我在做题和复盘过程中踩过的坑、总结的经验一并写出来。
1. 内容整体设计与思路拆解
1.1 为什么用“测验”的形式做复盘
很多人学Java喜欢看视频、看博客,看的时候觉得“都会了”,一到写代码就卡壳。我自己的经验是,知识以为自己会了不算会,能在限时、不查资料的情况下准确回答出来,才算真正内化。所以我会定期做一套自测题,模拟面试或者基础考核的场景,强迫自己把脑子里的知识“输出”一遍。
测验和直接看面试题集最大的区别在于,测验带有时间压力和反馈闭环。我做测验4的时候,给自己定了每道题最多15分钟,超时就算不会。这种压力能很真实地暴露薄弱点——有些知识点你觉得自己熟,可真到写答案的时候才发现连概念都说不利索。做完之后再逐题复盘,比单纯刷一百道“Java面试大全”有效得多。
另外,测验题目的设计最好不要全是概念题。纯理论背诵没有意义,我在这套测验里刻意加入了代码分析题、异常排查题、参数配置题,因为这些才是工作中真正会遇到的事情。比如java: 警告: 源发行版 17 需要目标发行版 17这个编译问题,几乎每个从旧版本JDK切到17的人都会碰到,但它很少出现在传统面试题里。
1.2 测验范围:覆盖基础语法到实战陷阱
这套测验4的考察范围,我按照Java开发中最常用的知识域来划分。首先是基础语法层面,包括标识符命名、运算符与表达式、数组边界、String与包装类;其次是面向对象核心,包括继承、多态、重载重写、接口与抽象类的选择;然后是集合框架,重点是HashMap、ArrayList、TreeMap这些高频容器的底层行为和选型;再往后是JVM内存与异常处理,这部分结合了OutOfMemoryError、栈溢出等实际问题的排查思路;最后是Java 8之后的新特性,Lambda、Stream、方法引用、枚举、Comparator.comparing这类实用工具。
这个范围设计是有讲究的。我见过太多人工作三五年,Spring Boot用得飞起,但让他说说HashMap在JDK 8里的结构变化,或者ArrayList默认容量为什么是10,就说不上来。而这些恰恰是Java基础面试题里出现频率最高的部分,也是很多“八股文”喜欢考的点。我的观点是:八股文本身不可怕,可怕的是只背结论不追问原因。所以这套测验的每个题目,我都会在后面追问一个“为什么”。
1.3 难度梯度:从送分题到拉分题
一套好的测验必须要有层次感。测验4的题目我分了三个梯度:第一梯度是概念题,比如“Java标识符能否以数字开头”“==和equals的区别”,这些是送分题,用来检验基础是否扎实;第二梯度是代码分析题,比如给一段循环代码问输出结果、判断数组越界的位置,这些需要动笔推演;第三梯度是综合排查题,比如“启动项目时出现OutOfMemoryError: insufficient memory怎么定位”,答案不唯一,考察的是实战思路。
难度梯度的设计直接决定了测验的诊断价值。如果全是送分题,所有知识点看起来都掌握了,实际能力却未必;如果全是难题,挫败感太强,也不利于系统复习。我建议你在给自己出题时也按这个比例设计:60%基础、30%综合、10%挑战。这样既保证覆盖面,又能把真正薄弱的地方逼出来。
2. 核心细节解析与实操要点
2.1 面向对象:继承、多态、重载与重写
面向对象编程Java的核心思想,在这套测验里占了相当大的比重。我这套题里有这样一道题:父类有一个方法public void run(),子类重写后改成protected void run(),问能不能编译通过。答案是编译报错,因为重写时访问权限不能更低。很多人背过“重写规则”,但做题时还是会选错,根本原因是没有理解“里氏替换原则”——子类必须能无缝替换父类出现在任何地方,如果访问权限变严,外部调用父类方法的地方就访问不到子类方法了。
另一个高频考点是重载和重写的区别。重载是同一个类里方法名相同、参数列表不同,编译期就决定了调用哪个版本;重写是子类重新实现父类方法,运行期根据对象实际类型决定调用哪个版本。测验里我给了这样一段代码:
class Animal { public String speak() { return "animal"; } } class Dog extends Animal { public String speak() { return "dog"; } } public class Quiz4 { public static void main(String[] args) { Animal a = new Dog(); System.out.println(a.speak()); } }输出结果是dog,因为sleep方法被重写了,JVM在运行期动态绑定到Dog的实现。这个考点几乎是所有Java面试的必问题,也是检验是否理解多态最直接的样例。我还加了一个追问:“如果speak是静态方法,输出结果是什么?”答案是animal,因为静态方法属于类,不存在重写,只存在隐藏。这个细节能筛掉一大批背题人。
在实际开发中,我见过很多因为重写规则理解不到位导致的诡异Bug,尤其是equals和hashCode一起重写的问题。测验4里专门有一道题问:只重写equals不重写hashCode会有什么后果?放到HashSet里时,可能会存储两个逻辑上相等的对象,因为它们哈希值不同,被放到了不同的桶里。这是一个非常经典的坑,也是集合框架正确使用的前提。
2.2 集合框架:选型对比与线程安全
集合是Java里使用频率最高的类库之一,测验4里我出了不少选型题。比如“已知元素需要按自然顺序排序,且元素唯一,应该选什么容器?”答案是TreeSet。很多人下意识选HashSet,因为“去重用HashSet”,却忽略了“排序”这个条件。TreeSet底层是TreeMap,基于红黑树实现,插入时按Comparator或Comparable排序,代价是插入和删除的时间复杂度为O(log n),比HashMap的O(1)慢,但换来了有序性。
另一个经典问题是ArrayList和LinkedList的对比。网上的Java基础面试题几乎必考这个,但很多人只背“ArrayList查询快、增删慢,LinkedList增删快、查询慢”,这个结论其实很粗糙。真实情况是,在中间插入元素,LinkedList只是不需要搬移后续元素,但它需要先遍历找到插入位置,这部分也是O(n);而ArrayList虽然需要System.arraycopy搬移元素,但批量搬移在JIT优化后并不一定慢。所以我的建议是:业务代码里90%的场景用ArrayList就好,LinkedList的优势场景非常窄。这套题里我专门设置了一个代码分析题,让做题者判断“for循环里用LinkedList.get(i)遍历的复杂度”,正确答案是O(n²),因为每次get都是从头部开始遍历。
线程安全集合也是我一直强调的重点。测验中有一道题问:多个线程同时往HashMap里写数据会出现什么。这个问题在JDK 7里可能导致扩容时的环形链表,从而引发死循环;JDK 8改成了尾插法,死循环问题解决了,但依然存在丢数据、覆盖等线程安全问题。正确的做法是使用ConcurrentHashMap,它通过CAS加synchronized锁桶的方式实现高并发读写,比给整个HashMap加锁要精细得多。
2.3 异常处理与资源关闭
异常处理这块,测验4里考了几个特别容易混淆的细节。第一个是finally块到底什么时候不执行。结论是:只有在System.exit()或JVM崩溃时,finally才不会执行。很多人以为return了就不执行finally了,实际上finally会在方法返回前执行。我出了一道题:try { return 1; } finally { return 2; }输出是多少?答案是2。因为finally里的return会覆盖try里的return,这在实际代码里是极其危险的操作,所以我建议无论如何不要在finally里写return。
第二个是受检异常和非受检异常的选择。IOException、SQLException这类受检异常强制要求调用方处理,而NullPointerException、ArrayIndexOutOfBoundsException这类RuntimeException子类不需要强制捕获。我见过很多团队在业务代码里疯狂抛出Exception,导致调用方被迫写一堆空的catch块。我的理念是:编程错误用非受检异常,外部环境异常用受检异常,但千万不要把所有异常都抛成Exception,那样等于什么都没说。
第三个是try-with-resources。JDK 7之后,凡是实现了AutoCloseable的资源都可以写在try后面的括号里,JVM会自动关闭。测验里我让做题者比较两段代码,一段是传统写法里手动close(),一段是try-with-resources,要求说出后者能避免什么问题。答案是可以避免在关闭资源时抛出新的异常把原始异常覆盖掉。try-with-resources会保留原始异常,被抑制的异常会被添加到Suppressed数组里,这在排查问题时太重要了。
2.4 JVM内存:OutOfMemoryError与启动参数
java: OutOfMemoryError: insufficient memory这个错误,看到就头疼。热词里出现了它,我在测验4里也专门出了一道JVM调优题。要理解这个错误,得先知道JVM内存分哪几块:堆内存存放对象实例,栈存放局部变量和方法调用,元空间存放类元数据,还有直接内存等区域。常见的OOM分为三类:
| OOM类型 | 触发场景 | 典型排查命令 |
|---|---|---|
java heap space | 堆内存不足以分配新对象,常见于大对象或内存泄漏 | jmap -heap <pid>,查看堆使用情况 |
GC overhead limit exceeded | GC频繁回收但回收效果差,GC时间占比超过98% | jstat -gcutil <pid>,观察GC曲线 |
insufficient memory | 原生内存分配失败,常见于线程数过多或直接内存溢出 | jmap -dump,结合top看进程内存 |
我在实际项目里遇到过一种典型情况:测试环境堆内存只配了512M,压测时一下涌入大量请求,每秒钟产生几十万个对象,触发频繁Full GC,最后直接报GC overhead limit exceeded。这种问题不能只看报错信息,要用jstat观察GC频率,再用jmap -dump:format=b,file=heap.bin <pid>导出堆快照,用MAT分析哪些对象占据了最大空间。
对于启动参数,我常用的配置思路是:-Xms和-Xmx设置成一样的大小,避免运行期堆扩容带来的性能抖动;-Xss控制线程栈大小,默认1M,如果应用开了大量线程,可以考虑适当调小到512K,但不建议再低,否则容易栈溢出。-XX:MaxMetaspaceSize一定要显式设置,因为元空间默认是没有上限的,一旦框架动态生成大量类,就会把机器内存吃光。这套题的答案是开放式的,但只要能把上面这些参数和排查思路说清楚,就证明JVM基础是过关的。
2.5 Lambda、Stream与方法引用
Java 8 引入的Lambda表达式和Stream API,现在已经成为日常工作里离不开的工具。测验4里我考察了几个容易出错的点。第一个是Lambda表达式的变量捕获要求:被捕获的局部变量必须是final或事实上不可变的。我出了一道代码分析题:
int count = 0; list.forEach(x -> count++);答案是编译失败,因为count在Lambda里被修改了。这个限制背后是Java对并发安全的设计考虑:如果允许修改外部变量,多线程环境下就会出现共享可变状态的问题。编译器强制要求“不可变捕获”,倒逼开发者用更安全的写法。
Stream的惰性求值是另一个重点。我让做题者判断下面这个例子会输出几次“filter”:
Stream.of(1, 2, 3, 4, 5) .filter(x -> { System.out.println("filter " + x); return x % 2 == 0; }) .map(x -> x * 10) .limit(2) .forEach(System.out::println);很多不熟悉Stream的人会以为先全量filter再map再limit,实际上Stream是垂直求值的,每个元素依次经过filter、map,直到满足limit(2)就停止。所以输出是“filter 1”“filter 2”“filter 3”“filter 4”,总共4次filter,而不是5次。这个题非常能区分是“会用Stream”还是“理解Stream”。
parallelStream我也考了一道判断题:为什么并行流里有ArrayList的非线程安全操作是不安全的。因为ArrayList的内部数组在扩容时会创建新数组并复制元素,多个线程同时写就可能丢失数据或者出现数组越界。解决方式要么改成线程安全的容器,要么用Collectors.toConcurrentMap()这类并发收集器。实际业务里我反而建议谨慎使用并行流,因为其性能受核数、数据量、拆分成本多重影响,小数据量用并行流反而更慢。
2.6 枚举、常用类与运算符陷阱
Java枚举类型的使用,是基础题里比较好的拉分点。测验里我出了一道题:枚举能否定义抽象方法?能,每个枚举常量都可以实现。枚举天生适合做单例,因为JVM保证枚举实例的全局唯一性,而且序列化框架对枚举有特殊处理,不会像普通单例那样在反序列化时创建新对象。switch配合枚举使用,代码可读性会提升很多,编译期还能检查出遗漏的分支。
常用类这块,重点考了String不变性、StringBuilder与StringBuffer的区别、包装类缓存。这里有个非常容易踩的坑:Integer在-128到127之间会走缓存,在这个范围之外才会new Integer对象。所以有这样的代码:
Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; System.out.println(a == b); System.out.println(c == d);输出是true和false。这个题目我故意放在测验里,就是为了提醒做题者:判断包装类相等一定要用equals,用==是对缓存机制的依赖,一旦超出范围就翻车。这个知识点在真实开发里很容易造成隐晦的Bug,尤其是接口返回数据做比对的时候。
运算符与表达式的知识点,我出了一道自增的题:int x = 5; int y = x++ + ++x;问y的值。答案是12,因为第一个x++先取5,此时x变成6;第二个++x先自增成7,再取7,5加7得12。这类题看起来“面试八股”,实际考察的是对表达式求值顺序的理解。我自己在写代码时,除非很简单的i++,否则尽量避免把多个自增运算塞进一个表达式里,因为可读性太差,也容易改出副作用。
2.7 数组边界与比较器排序
数组越界异常ArrayIndexOutOfBoundsException是最常见的运行时异常之一。测验里我出了一道循环遍历的题:
int[] arr = {1, 2, 3}; for (int i = 0; i <= arr.length; i++) { System.out.println(arr[i]); }这里i <= arr.length就是典型的越界条件,数组下标最大只能是length - 1。这个题的讨论点不是报错本身,而是“如何写出防御性代码”。我建议在遍历时优先使用增强for循环或者流式遍历,它们从语法层面规避了下标管理的问题。
Comparator.comparing是Java 8里非常实用的排序工具,尤其配合方法引用,能把排序代码写得很简洁。测验里有一道题:“如何把一个对象列表按年龄升序排列,同时让某个特定元素排到第一个”。我用两种方案展示了实现:
// 先按“是否为VIP”降序,让VIP排前面,再按年龄升序 list.sort(Comparator.comparing(User::isVip).reversed() .thenComparing(User::getAge)); // 或者直接自定义比较器,把特定id放最前 list.sort(Comparator.comparingInt((User u) -> u.getId() == 99 ? 0 : 1) .thenComparingInt(User::getAge));关键点是Comparator.comparing返回的是一个比较器,可以用.thenComparing组合多个排序维度。这里有一个比较坑的细节:Comparator.comparing(User::isVip).reversed()只反转了“是否VIP”这个维度的顺序,不会反转后面.thenComparing的年龄维度。很多人误以为reversed()作用于整个链,实际上它只作用于调用它的那个Comparator对象。我在测验里专门设置了一个判断题来考察这个细节,因为实际写代码时太容易在这里搞错。
3. 实操过程与核心环节实现
3.1 从零搭建测验环境:JDK安装与环境变量配置
这套测验题里有一部分是编译运行类题目,所以环境搭建是第一步。很多初学者卡在“Java环境变量配置”上,其实流程极其简单。先下载JDK 17,注意选择对应操作系统的版本。安装完成后,最重要的是配置三个环境变量:JAVA_HOME、PATH、CLASSPATH。
在Windows上,新增系统变量JAVA_HOME,值为JDK安装路径,比如C:\Program Files\Java\jdk-17。然后在PATH里追加%JAVA_HOME%\bin。CLASSPATH在很久以前用来指定类搜索路径,现在大多数场景已经不需要手动配置了,但有些老项目还在用,所以我在测验里保留了一道“简述JAVA_HOME作用”的题目。在Linux或macOS上,需要往~/.bashrc或~/.zshrc里加:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH配置完成之后,打开新终端,输入java -version和javac -version,如果都能正常输出版本信息,说明环境没问题。这里我踩过一个坑:改了环境变量之后,没有重开终端,导致一直用的旧版本。尤其是多个JDK版本共存时,一定要在终端里执行which java确认当前用的到底是哪个路径。现实中很多人项目跑不起来,排查到最后就是IDE里跑的和命令行里跑的JDK版本不一致。
3.2 编写并运行测验代码:命令行与IDE两种方式
测验题里有一类“写出以下代码运行结果”的题,我建议不要凭脑补,直接跑一遍。比如这段非常经典的String比较题:
public class Quiz4 { public static void main(String[] args) { String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1.equals(s3)); } }在命令行里,先javac Quiz4.java编译出.class文件,再java Quiz4运行。输出结果是true、false、true。原因是字符串字面量会进入常量池,s1和s2指向常量池同一个对象,所以==为true;new String("hello")在堆上新建了一个对象,和常量池对象地址不同。这个例子如果只在脑子里推理,很容易漏掉“常量池”这个关键点,亲手跑一次印象会深很多。
使用IDE同样方便,但要注意两点。第一是项目SDK版本和语言级别必须一致。IDEA里如果Project SDK选了17,却把Java Language Level设置成8,就会出现低版本语法无法使用的问题。第二是构建工具的选择。我建议做测验题这种小项目时,直接用IDEA的普通Java工程就行,不用上Maven或Gradle,减少配置干扰。测验的目的是验证语言基础,而不是验证构建工具熟练度。
3.3 编译期常见问题:源发行版17、Lombok警告与乱码
在做测验4的代码题时,我遇到了三个典型的编译问题。第一个是java: 警告: 源发行版 17 需要目标发行版 17。这个警告的意思是编译器认为源文件版本是17,但目标发行版配置的比17低,例如8。出现这个问题的原因几乎总是IDE里Project Structure的Project language level和Modules里的语言级别不一致。解决办法是把它们统一设成17,同时检查Maven的pom.xml里maven-compiler-plugin的source和target配置,最好加上release标签:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>第二个问题是Lombok的警告:you aren't using a compiler supported by lombok, so lombok will not work。这种情况通常出现在使用了较新的JDK版本,而项目里的Lombok版本太老,注解处理器还不支持新JDK。最常见的解决方式是把Lombok升级到适配新版JDK的版本,比如JDK 17配合Lombok 1.18.30以上。如果公司强制不能升级依赖,可以试试在Maven编译插件里显式指定注解处理器路径,但最省心的还是升级。这个坑在真实项目里能卡住一整天,我特意把它加进测验里当作一道“你如何解决”的题目。
第三个是VSCode运行Java报乱码。这个问题在Windows上尤其常见,本质是源码文件编码和编译运行时的编码不一致。我一般建议全部统一为UTF-8:在.vscode/settings.json里设置"java.debug.settings.consoleEncoding": "UTF-8"和"java.encoding": "UTF-8",同时在启动时加上-Dfile.encoding=UTF-8参数。乱码问题本身不难解决,但排查起来很烦,因为同一个问题可能由三四个不同的配置错误导致。我的经验是先把源码文件确认成UTF-8,再看控制台编码,最后看编译参数,按顺序排查。
4. 常见问题与排查技巧实录
4.1 数组越界异常:定位边界条件
数组越界的问题,我把它单列出来是因为它在测验和实战里出现的频率都太高了。下面这个场景是我从真实代码里抽出来的:
public static int sum(int[][] grid) { int total = 0; for (int i = 0; i < grid.length; i++) { for (int j = 0; j < grid[i].length; j++) { total += grid[i][j]; } } return total; }这里如果grid是一个不规则二维数组,比如第一行有3列、第二行有2列,内外层循环用到的是grid[i].length,所以不会越界。但如果写成了外层for (int i = 0; i < grid[0].length; i++),遇到列数少的行就会越界。排查思路很简单:看到ArrayIndexOutOfBoundsException时,先看报错信息给出的下标,再回看循环边界是<还是<=,以及数组长度获取的是哪个维度的length。
我还踩过Java的一个经典坑:对Arrays.asList()得到的List调用remove,会抛UnsupportedOperationException,而不是数组越界。这是因为Arrays.asList返回的ArrayList是Arrays内部类,没有重写remove方法,直接调用了AbstractList的默认实现。这个坑和越界问题形似但本质不同,我在测验里把它们放在相邻位置,让做题者区分“下标问题”和“集合结构不支持操作”两种异常。
4.2 排序结果不符合预期:Comparator的细节
用Comparator.comparing排序时,结果不符合预期是常见问题。我遇到过一个实际案例:要对用户列表排序,管理员排最前,然后按注册时间倒序。第一版代码如下:
users.sort(Comparator.comparing(User::isAdmin).reversed() .thenComparing(User::getCreatedAt).reversed());这段代码的问题在于,第二个reversed()把整个链都反转了,结果是普通用户排前面、注册时间升序,完全反向。正确的写法是把时间字段本身取反,或者先构造比较器再整体取反:
Comparator<User> comparator = Comparator .comparing(User::isAdmin) .thenComparing(User::getCreatedAt) .reversed(); users.sort(comparator);这里reversed()作用于整个comparator,才符合“管理员优先、注册时间倒序”的需求。这个细节,测验里我用了一个“找出错误”的题型来考察。实际写排序代码时,我强烈建议把比较器单独抽出来,用变量命名说明排序意图,避免在一个Expression里堆多个reversed()导致逻辑混乱。
4.3 OOM排查实战:从报错到定位的完整链路
排查java: OutOfMemoryError: insufficient memory或者java heap space,一定要有明确的方法论,不能重启完事。我处理过一次线上问题,流程可以供参考:第一步,拿到报错信息后,先用jcmd <pid> VM.version确认JVM版本,再用jstat -gcutil <pid> 1000每秒输出一次GC情况。如果看到Old区持续增加到接近100%,且Full GC频繁,基本可以判定是内存泄漏或堆配置过小。第二步,用jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>导出堆快照,这里加live参数会先触发一次Full GC,只dump存活对象,文件会小很多。第三步,用MAT打开dump文件,查看Dominator Tree,找哪个对象集合占据了最大内存。
我在这次测验里复盘出一个特别容易忽略的点:很多时候OOM不是因为堆太小,而是因为代码里某个List不断被添加元素且没有清理。比如缓存场景,如果用了普通的HashMap当作缓存,又没做容量限制,时间长了必然内存爆炸。解法是使用Caffeine或Guava Cache这类自带过期和淘汰策略的本地缓存。这个背后的原理,比单纯背“-Xmx调多大”要重要得多。
4.4 编译运行问题速查表
把这次测验和日常工作中遇到的编译运行问题整理成一个速查表,方便遇到时直接对照排查:
| 现象 | 常见原因 | 解决方法 |
|---|---|---|
源发行版 17 需要目标发行版 17 | IDE语言级别或Maven编译参数不一致 | 统一SDK版本、语言级别、maven.compiler.source/target |
| Lombok编译警告 | Lombok版本不支持当前JDK | 升级Lombok到兼容版本 |
| VSCode运行乱码 | 编码不一致 | 统一UTF-8,加-Dfile.encoding=UTF-8 |
OutOfMemoryError: insufficient memory | 原生内存不足或进程内存限制 | 降低线程数,检查直接内存,调整容器内存限制 |
ArrayIndexOutOfBoundsException | 循环边界或索引计算错误 | 检查</<=,确认length来源 |
UnsupportedOperationException | 调用不可变集合的修改方法 | 创建新的ArrayList后再修改 |
找不到或无法加载主类 | JAVA_HOME配置错误或编译输出目录不对 | 检查环境变量,重新编译 |
这个表我一直在维护,每次遇到新问题就补一行。做Java开发,遇到错误不可怕,怕的是每次都用“重启”来碰运气。把问题归类、记录、整理成表,下一次排查速度能快一个数量级。这套测验4复盘下来,我把里面每一个问题都归入了这个表,之后再看一眼就能定位。
我个人在实际操作中的体会是:做这套测验最大的价值不是分数,而是逼着自己把平时“会用但说不清”的知识点完整地讲了一遍。尤其是Comparator.comparing的链式调用、try-with-resources的抑制异常、JVM堆内存的排查手段,这些在写业务代码时未必天天用,但一旦碰到就是大问题。如果你正在走Java学习路线,我建议每学完一个阶段就给自己出10道题,限时作答,然后像我这样把每道题背后的原理和坑都写出来。第一轮会很慢,但第二轮、第三轮就会明显感觉到知识体系变得立体了。最后再分享一个小技巧:把每道错题对应的关键词记下来,比如“枚举”“OOM”“Comparator”,隔两周用这些关键词重新出题,能有效巩固记忆,这也是我持续把测验做成系列的原因。