1. 你该不该做这份Java测验?先聊聊我的出发点
最近在带几个刚转行学Java的朋友,发现一个特别普遍的问题:视频看了一堆,代码跟着敲了无数遍,一到合上书自己写,或者被问到某个基础知识的时候,脑子里就开始模糊。尤其是那种"看着都认识、一考就错"的知识点,比如String和StringBuilder的区别、==和equals到底什么时候相等、HashMap扩容之后链表什么时候转红黑树……全挤在一团。
所以我就整理了一套"Java测验5"——其实是我自己给第五轮阶段自测起的名字。这套测验不是学校期末考那种死记硬背题,而是围绕Java基础核心知识点、高频面试题和真实开发中容易踩坑的细节设计的。做完这套题,你基本能摸清自己处在哪个水平:是刚入门的"会敲代码",还是已经开始理解"为什么要这么写"的阶段。
我把它分成几个部分:基础语法与运算符、面向对象、常用类与集合、异常处理、还有几道综合编程题。每道题我都配了解析和踩坑提醒,顺便把背后涉及的面试追问也列了出来。不管你是在校学生、准备跳槽的初级开发,还是自学Java想找个自测方向的人,这套测验都适用。
需要提前说明的是,我自己设计这套题的时候,刻意避开了一些偏题怪题。Java面试八股文里确实有很多刁钻的问题,但作为阶段自测,我更推荐先扎扎实实把底层逻辑搞明白,再去背那些奇技淫巧。毕竟靠背出来的八股,面试官多追问两句就露馅了。
2. 题目覆盖思路:为什么考这些,不考那些
2.1 核心考点选择逻辑
如果你去看市面上的Java基础面试题大全,动辄几百道,但其实高频精华也就那么几十个方向。我在设计Java测验5的时候,遵循了三个选考点原则:
第一,高频使用原则。凡是日常开发中每天都要见面的知识,必须考。比如运算符优先级、循环控制、面向对象三大特性、集合框架的选型、异常处理机制。这些不是八股文,是基本功。
第二,易混淆原则。很多Java知识点单独拎出来谁都会,放在一起对比就容易乱。典型如int和Integer的自动装箱比较、ArrayList和LinkedList的适用场景、重载和重写的区别。这些恰恰是面试官最爱挖坑的地方。
第三,底层原理原则。不要求你背出JVM源码,但要能说出"为什么"。为什么HashMap是无序的?为什么String不可变?为什么lambda表达式要求变量是 effectively final ?这些"为什么"才是区分初级和中级的门槛。
2.2 我故意没放进去的内容
有一种题我基本不出:纯背书型题目。比如"Java的8种基本数据类型是什么"——这种题你上网搜一下全有,考了没有区分度。我更愿意出一道"short s1 = 1; s1 = s1 + 1; 和 s1 += 1; 有什么区别"这样的变体,表面上考类型转换,实际上考运算符重载和隐式类型转换,这种题才有意思。
另外,我也不建议在这个阶段纠结一些过于底层的八股,比如JVM内存模型中对象头占多少字节、volatile的指令重排序细节。不是说这些不重要,而是基础语法还没打通的人学这些,只能靠背,背完就忘。等你的代码量上去之后,回头再理解这些会轻松得多。
2.3 这份测验适合什么人
我的建议是:你至少已经学完Java SE的核心内容,或者正在准备Java初级/实习岗位面试,或打算系统地检查一遍自己有没有知识盲区。如果刚学到循环语句,可以先跳过后面的集合和异常部分。我自己给朋友的用法是:周末花一个小时做完整套题,对答案,错题全部标记出来,下周针对性地补。
3. "Java测验5"全卷解析:题目、答案与考点细说
3.1 基础语法与运算符专项
这部分是热身题,但热身不代表简单。很多老手在这里也会翻车。
题1:下面代码运行后,输出结果是什么?
int i = 0; i = i++ + ++i; System.out.println(i);解析:这个题是老演员了,几乎每一套Java面试题里都有它。核心考的是i++和++i的本质区别:一个先返回后自增,一个先自增后返回。计算过程拆开看——左侧的i++先取原值0参与计算,i变成1;右侧的++i先自增,i从1变成2,再取2参与计算,所以结果是0+2=2。我见过不止一个干了两年Java的人在这里答错,说明平时写代码基本不会用这种写法。
实操建议:写业务代码的时候,千万不要把i++和++i混在同一个表达式里。你自己可能知道执行顺序,但读你代码的同事未必能在3秒内反应过来。好的代码应该是"一眼就能看懂",而不是"推理五分钟才能确认"。强行秀操作,review的时候大概率会被骂。
题2:以下哪些表达式的值是true?(多选)
A. "abc" == "abc" B. new String("abc") == "abc" C. "a" + "bc" == "abc" D. new String("abc").equals("abc")解析:这题是我眼里覆盖面最广的一道基础题,因为它同时考了字符串常量池、编译期常量折叠、equals和==的区别。A选项,两个字符串字面量在常量池中引用同一个对象,所以是true。B选项,new出来的对象在堆上,引用地址不同,是false。C选项,"a"+"bc"在编译期就能确定结果是"abc",编译器会直接折叠成字符串字面量,所以和"abc"是同一个常量池引用,true。D选项,equals比较的是内容,自然是true。
实战经验:很多面试官会把这道题变形成String s1 = new String("abc"); String s2 = s1.intern(); s1 == s2来问。这里的核心是intern()方法会在常量池里查找或创建相同内容的字符串并返回常量池引用。所以s1是堆上对象引用,s2是常量池引用,两者不相等。这种追问逻辑你要是能当场画图说明,面试官印象分会高很多。
题3:下面代码哪里有问题?
short s1 = 1; s1 = s1 + 1;解析:经典陷阱题。s1 + 1的结果是int类型,把int赋值给short需要强制类型转换,所以编译报错。如果改成s1 += 1就可以,因为复合赋值运算符自带隐含强转。这道题的变体是问s1 = s1 + 1和s1 += 1在什么极端情况下结果不同——答案是当s1 + 1的结果超出short范围时,s1 += 1会发生精度丢失但不报编译错误,而如果你强制转换,也会丢失。所以更严谨的说法是:复合赋值运算符隐含强转可能导致数据溢出,这是它的副作用。
3.2 面向对象核心机制
面向对象是Java的根,也是面试八股文的重灾区。我的出题思路是避开"什么是封装/继承/多态"这种背课本的题,直接上代码场景。
题4:下面代码的输出顺序是什么?
class Parent { static { System.out.print("1 "); } public Parent() { System.out.print("2 "); } { System.out.print("3 "); } } class Child extends Parent { static { System.out.print("4 "); } public Child() { System.out.print("5 "); } { System.out.print("6 "); } } public class Test { public static void main(String[] args) { new Child(); } }解析:这题考的是类加载和实例化时的初始化顺序,属于那种看着简单,一写就错的题。正确输出是 1 4 3 2 6 5。规律总结成口诀就是:先静态(父类静态块优先于子类静态块),再父类实例化(父类构造代码块先于父类构造方法),最后子类实例化(子类构造代码块先于子类构造方法)。
我踩过的坑:有一次真在项目里遇到一个诡异Bug——父类构造方法里调用了一个被子类重写的方法,结果子类字段还没被初始化,读出来是null。这就是"构造方法中不要调用可重写方法"这条铁律的来源。如果父类构造器在子类字段初始化之前调用了一个动态绑定方法,得到的结果一定不是你期望的。排查这个Bug花了我们半个下午,从那以后我写代码坚决不在构造方法里做动态绑定调用。
题5:抽象类和接口,下面说法正确的是?
A. 抽象类可以没有抽象方法 B. 接口可以包含成员变量 C. 一个类可以实现多个接口但只能继承一个类 D. 接口方法默认都是public abstract的解析:正确答案是A和C。A选项是对的,一个类即使没有抽象方法也可以声明为abstract,目的就是防止被实例化;B选项,接口可以定义成员变量,但默认是public static final的,也就是常量,这跟"成员变量"的语义有区别;C选项,Java单继承多实现,没问题;D选项,Java 8之后接口可以有default和static方法,不能说"默认都是",这个说法过时了。
关于接口和抽象类的选择,我的个人经验是:能用接口表达"能做什么",就不要用抽象类表达"是什么"。除非你确实有一批子类共享了公共状态(字段),否则优先接口。组合优于继承,这是我从设计模式里学到的最大教训之一。
3.3 集合框架与泛型
集合是Java面试必考模块。我在这里设计了两个题,一个是HashMap的基础机制,一个是集合排序。
题6:关于HashMap,下面哪个说法是错误的?
A. HashMap允许key和value为null B. HashMap线程不安全 C. HashMap在JDK 8之后使用数组+链表+红黑树 D. HashMap的默认初始容量是16,加载因子是0.75,扩容时翻倍 E. HashMap的put方法在key冲突时,新值会放到链表头部解析:错误的是E。JDK 8之后,HashMap链表插入元素采用尾插法,也就是新元素追加到链表尾部。JDK 7及以前是头插法。为什么改成尾插法?因为头插法在并发扩容时会造成链表环,导致get死循环。虽然HashMap本来就不保证线程安全,但JDK团队还是顺手把这个问题修了。这道题能考察你是不是停留在"背结论"的层面,真的理解过HashMap的演进逻辑。
题7:使用Comparator.comparing对列表排序,把指定元素排到最前面。
这个题很有实战价值。我问过几个工作两三年的朋友,第一反应都是"先排序,再把指定元素移出来放到头部",这当然可以,但不够优雅。我给的参考实现是:
List<String> list = Arrays.asList("apple", "banana", "cherry", "date"); String first = "cherry"; list.sort(Comparator .comparing((String s) -> s.equals(first)) .reversed() .thenComparing(Comparator.naturalOrder()));这段代码的逻辑是:先把"是否等于first"这个布尔值排序,布尔值false(0)排在true(1)前面,所以reversed()之后true排前面,然后其余元素再按字母序排。代码量少,也不破坏原数据结构。不过要注意:Comparator.comparing的泛型推断偶尔会出问题,所以第一个lambda的参数类型我显式写成了(String s),这是处理泛型推断的常用技巧。
3.4 异常与常见运行时错误
异常这块,我想出一个动手纠错型的题,比背"Error和Exception的区别"更实用。
题8:下面代码有什么问题和隐患?
try { List<Integer> list = new ArrayList<>(); while (true) { list.add(1); } } catch (Exception e) { e.printStackTrace(); }解析:你永远也不可能在这个程序里捕获到任何异常,因为它抛出的是OutOfMemoryError,不是Exception。OutOfMemoryError是Error的子类,只有Throwable才能接住它。用catch(Exception)捕获不到。
这个题在热搜词里也出现了,就是"java: outofmemoryerror: insufficient memory"。真实开发里遇到OOM,正确的做法是:先通过jmap等工具把堆内存dump下来,再用MAT分析是哪个对象占了大头,而不是简单加-Xmx参数。我遇到过一些团队,一遇到OOM就无脑调大内存,结果内存加到32G照样OOM,最后才发现是代码里有死循环在疯狂往List里塞数据——就像我出的这道题一样。
3.5 编程实操题
到了综合编程题,我的要求是:不但要写对,还要说清楚为什么这么写。下面这道题直接对标初级开发岗的笔试题。
题9:写一个方法,判断一个字符串是否是回文字符串,忽略大小写和非字母数字字符。
public static boolean isPalindrome(String s) { int left = 0, right = s.length() - 1; while (left < right) { char lc = s.charAt(left); char rc = s.charAt(right); if (!Character.isLetterOrDigit(lc)) { left++; } else if (!Character.isLetterOrDigit(rc)) { right--; } else { if (Character.toLowerCase(lc) != Character.toLowerCase(rc)) { return false; } left++; right--; } } return true; }解析:这道题考三个点:双指针思路、Character工具类的使用、对大小写和非字母数字字符的处理。很多人第一反应是用正则表达式s.replaceAll("[^A-Za-z0-9]", "").toLowerCase(),然后逐字符对比,这个方案功能上没问题,但会多一次O(n)的遍历和一次String对象创建。双指针一次遍历搞定,空间复杂度O(1)。在笔试场景里,双指针是更优解。
3.6 附加题:Java 8新特性与常见编译问题
最后加一道lambda和编译相关的题,原因是这两个方向是很多自学者的痛点。我见的报错太多了,什么"vscode运行java报错乱码"、"java: 警告: 源发行版 17 需要目标发行版 17"、"java: you aren't using a compiler supported by lombok",全都是环境配置问题,不是代码问题。
题10:关于lambda表达式,下面哪种写法是正确的?
A. Consumer<String> c = s -> System.out.println(s); B. Runnable r = () -> System.out.println("run"); C. BiFunction<Integer, Integer, Integer> f = (a, b) -> a + b; D. Supplier<Integer> sup = () -> 42;解析:四个选项全部正确。但为什么我要出这道题?因为我发现很多人其实分不清这几个函数式接口的适用场景。简单记法:Consumer消费一个参数不返回、Supplier不接收参数但返回一个值、Function接收一个参数返回一个值、BiFunction接收两个参数返回一个值、Predicate接收参数返回boolean。lambda表达式的类型推断是编译期完成的,不是运行期,所以如果你到处写(Object a, Object b) -> a.equals(b),一旦两个参数不是同一个类型,编译期就会报错。
题11:关于lombok编译警告"you aren't using a compiler supported by lombok, so lombok will not work"
这个我在热搜里看到有朋友遇到,顺手聊一下。出现这个警告,绝大多数情况下是你项目的编译器和IDE内置的lombok版本不匹配,常见的比如用的JDK 22但lombok版本还停留在1.18.30以下。解决办法是升级lombok依赖到最新版,同时确保IDE的annotation processing是开启的。还有一个冷门原因是项目里混用了不同版本的lombok依赖,Maven依赖树里出现了多个lombok,这种建议用mvn dependency:tree排查。
题12:vscode运行Java报错乱码
典型的编码问题。Windows下默认GBK编码,而VSCode默认UTF-8,控制台输出中文就会乱。解决方案有两个:一是在launch.json里加上"vmArgs": "-Dfile.encoding=UTF-8";二是改系统的环境变量JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8,但推荐第一种,影响范围可控。
4. 从测验里挖出来的高频面试追问:背会这些,面试不慌
做完上面这一套题,你会发现有不少知识点,面试官是可以连环追问的。我把自己在面试候选人时最爱问的追问链整理出来,方便你自查:
| 题目 | 常见追问链 |
|---|---|
| String常量池 | 追问1:intern()方法的作用?追问2:new String("abc")创建了几个对象?追问3:String为什么设计成不可变? |
| HashMap | 追问1:链表转红黑树的阈值为什么是8?追问2:为什么加载因子是0.75?追问3:HashMap在多线程下会有什么问题?追问4:ConcurrentHashMap的实现原理? |
| 自动装箱 | 追问1:Integer缓存范围是多少?追问2:为什么是-128到127?追问3:Integer a = 100; Integer b = 100; a == b结果是? |
| 异常 | 追问1:Error和Exception的区别?追问2:受检异常和非受检异常的区别?追问3:有没有用过自定义异常?定义的时候继承了哪个类? |
| lambda | 追问1:lambda捕获的局部变量为什么必须是effectively final?追问2:Function和UnaryOperator有什么区别?追问3:stream的并行流parallelStream有什么问题? |
这里挑几个重点展开讲讲,因为这些追问背后才是真正拉开差距的地方。
String不可变性的价值:三方面:安全、线程安全、字符串常量池。安全是因为String经常被用作参数或配置项,如果可变,传入方法后可能被篡改;线程安全是因为不可变对象天然线程安全;常量池复用是因为不可变才能安全地被多个引用共享。另外,哈希缓存也是一个隐藏好处——String的hashCode被缓存了,因为值不会变,所以在HashMap里作为key效率极高。
HashMap加载因子为什么是0.75:这是一个时间和空间的折中。加载因子越小,比如0.5,空间浪费严重;加载因子越大,比如1.0,链表会很长,查询效率变低。0.75是JDK团队通过统计学泊松分布计算出来的一个相对平衡值。至于链表转红黑树的阈值8,也和泊松分布有关:在加载因子0.75的前提下,链表中节点数量达到8的概率是极低的(约千万分之六),所以用8作为阈值是合理的设计。
Integer的缓存范围为什么是-128到127:因为-128到127是使用最频繁的整数范围,JVM启动时直接把这些包装对象创建好放入缓存,用的时候直接返回引用。如果超过这个范围,Integer a = 200; Integer b = 200; a == b就是false了。我见过有人考这个题,直接问"new Integer(100)和Integer.valueOf(100)有什么区别"——valueOf会走缓存,new一定创建新对象。这就是为什么我们写代码时更推荐valueOf或自动装箱,而不是new Integer。
stream的parallelStream有什么坑:并行流的底层是ForkJoinPool,使用的是公共线程池。如果你的任务里有阻塞操作(比如IO调用、数据库查询),会占满公共池的线程,影响整个JVM里其他并行流的执行。所以只有CPU密集型、无状态、无阻塞的任务才适合用parallelStream,否则老老实实用串行流。我见过有同学在接口里用parallelStream批量调外部API,结果把线程池打满导致整个应用变卡,排查了半天才定位到。
5. 常见问题与排查技巧实录:做题之外的实战避坑
5.1 编译环境的常见报错
做这套测验的时候,很多同学会在环境上先卡住。我把高频报错和解决方法列出来,你直接照着操作就行。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
java: 警告: 源发行版 17 需要目标发行版 17 | IDE编译器的java版本低于项目配置版本 | 在IDE里检查Project Structure把SDK和language level统一;Maven项目检查pom.xml里maven-compiler-plugin的source/target |
java: internal error in the mapping processor: java.lang.NullPointerException | 常见于MapStruct在JDK高版本下编译异常 | 升级mapstruct-processor到1.5.5+(至少要适配JDK 21),或者检查是否有属性名不匹配 |
java: you aren't using a compiler supported by lombok | lombok版本和JDK不兼容 | 升级lombok版本,或者确认IDE的annotation processing已开启 |
vscode运行java报错乱码 | 控制台编码和源码编码不一致 | launch.json配置vmArgs: -Dfile.encoding=UTF-8 |
java.lang.ArrayIndexOutOfBoundsException | 数组越界 | 检查循环边界,建议用增强for循环或Arrays工具类 |
5.2 环境变量配置这个老大难
热搜词里"java环境变量配置"一直居高不下,说明很多新手第一步就没走顺。我分享一个我自己教别人配置环境变量的思路,特别简单:
先安装JDK(比如JDK 17),然后配置三个变量——JAVA_HOME指向JDK安装目录,Path添加%JAVA_HOME%\bin(Windows下),然后配置CLASSPATH为.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar(其实高版本JDK不太需要手配CLASSPATH了)。配置完在命令行输入java -version验证。
最容易踩的坑:装了多个JDK版本,系统Path里既有JDK 8的路径又有JDK 17的路径,顺序不同导致java版本不确定。排查方式是在命令行输入where java(Windows)或which java(Mac/Linux),看实际解析到的是哪个路径。这个问题在热搜词里反复出现我完全不意外,因为几乎所有新手都会装错。
5.3 这份测验我自己做了一遍的感受
我自己写完这套题顺手做了一遍,本来以为自己闭着眼都能全对,结果还是有题目愣了一下——第3题的short类型转换,因为平时写代码都是用int,short类型确实很少用,隔了半年再做容易反应不过来。这说明什么?哪怕你是一个熟练开发者,不常用的知识点也会生疏。这也是我建议大家定期做这种基础测验的原因,它不是考你的智商,而是帮你的大脑定期刷新这些底层知识点。
5.4 如何判断这道题该不该深究
我做技术问答经常被问到一个问题:"这个知识点我看到过,但感觉太深了,要不要花时间学?"
我的判断标准很简单:看它能不能被日常业务代码或两次以内面试追问覆盖。比如HashMap的红黑树结构——日常代码你用不到红黑树,但面试一定会问,而且会追问"为什么是8",这种就值得学。而JVM的G1垃圾回收器分代GC的具体细节,如果你不是做性能优化的,完全可以等用到的时候再深入。学习要有优先级,先学生产力最高的知识。
6. 最后的建议:做完这套测验,接下来怎么学
这套"Java测验5"覆盖的是Java基础的核心骨架,但绝不代表全部。如果你做完全卷,感觉大部分题有把握,下一步我建议按照这几个方向继续:
第一个方向是集合框架源码。别急着看数据结构课本,直接把ArrayList、LinkedList、HashMap、ConcurrentHashMap的JDK源码读一遍。我第一次读HashMap源码的时候花了整整两天,但读完之后很多原本要靠记忆的面试题突然就通了。比如为什么HashMap的容量总是2的幂次方?因为(n - 1) & hash这个位运算可以直接替代取模运算,效率高得多。
第二个方向是JVM基础。重点看内存区域划分、类加载机制、垃圾回收算法。特别是垃圾回收,建议配合可视化工具JVisualVM或G1GC的日志分析来学,光看概念效率太低。我当时是在自己的电脑上写了一个死循环生成对象的程序,然后观察堆内存的变化曲线,印象特别深刻。
第三个方向是并发编程。synchronized和ReentrantLock的区别、volatile的语义、ThreadLocal的原理、线程池的核心参数。这个模块的面试高频程度不亚于集合框架,而且特别容易出现"看着简单、一深挖就答不上来"的情况。
第四个方向是框架和工具链。Spring Boot + MyBatis基本是Java后端岗位的标配,Maven/Gradle构建工具也得熟练。如果你是纯自学者,建议用Spring Boot写一个简单的CRUD接口,部署到本地,完整走一遍开发流程,这一步能帮你把前面学的所有基础知识串起来。
我个人建议是按这个顺序推进,不要跳步。这一步扩展其实是很多人忽略的,学完基础语法就急着上Spring Boot,结果出了问题连日志都不会看,就是地基没打牢。
最后再说一个小技巧:做这种基础测验,不要开卷,不要边看解析边做,做完再统一对答案。错了的题不只是看答案,还要自己在IDE里把代码跑一遍,打断点看执行过程。看到变量真实的变化,比背一百遍解析都有用。
祝做题顺利,有问题欢迎留言交流,我看到都会回复。