做Java面试官这几年我攒了一抽屉的JAVA笔试题真题每年校招春招秋招都能看到同一批题目变着花样出现也看到同一批候选人反复在几个固定的坑里翻车。上周一个学弟把某平台整理的Java必背两百题发给我让我帮划重点我翻了一遍发现一半题目连答案都是错的另一半只是在背结论。这让我决定把手头真正经得起推敲的真题整理成系列分享出来今天先发第一季全部带答案、带解题思路、带踩坑说明。这篇内容适合准备校招笔试的应届生也适合基础不牢想跳槽的初中级开发用来查漏补缺。我尽量还原考场上的真实节奏来写每道题先给题目再给答案最后讲清楚为什么是这个答案。你别急着往下翻答案先自己在脑子里推一遍推错了再对照印象会深得多。1. 这批真题的来路与正确的刷法先交代一下题目来源。我手头的这批题主要来自三类场景校招在线笔试大厂和中小厂都有、社招机试、以及一部分外企的纸质笔试。还有一部分是我这几年的面试考官的出题我的出题标准很简单——如果一道题能把背过八股文和真的懂原理区分开就值得进题库。所以这批题有个共同特点表面考记忆实际考理解答案都简单论证过程才是拿分点。从题型分布上看Java笔试基本逃不出这几块Java基础语法和核心类库大约占四成集合框架占两成并发和JVM占一成半手写算法占一成半剩下的就是代码输出题和开放题。前几年还流行大段概念默写题比如简述HashMap原理现在越来越少因为网上全是答案问不倒人。现在的趋势是给一段看似简单的代码让候选人猜输出或者给一个业务场景让候选人做选型判断这两种题在这篇文章里覆盖的比例很高。刷这批题的时候我给三条实操建议先遮住答案在纸上或者IDE里自己推一遍推错了再对照答案记忆会深很多。每道题都追问一句为什么比如HashMap默认容量是16这句话背下来没用你要能说出来扩容阈值怎么算、为什么容量必须是2的幂。把错题按知识点归并不要按题目归并。同一个知识点哪怕换十种包装内核都是一样的归并以后你会发现真正要记的没那么几条。按知识点分布的话可以参考下面这个粗略的比例做复习优先级考查板块大致占比这季覆盖情况基础语法与核心类库40%第2、6节集合框架20%第3节并发与线程安全15%第4节手写算法15%第5节代码输出与开放题10%穿插在各节2. 基础题String常量池、静态方法和Integer缓存基础语法是所有笔试题的底色也是区分背过和真的懂最明显的板块。这一节三道题几乎每年笔试都会出现出题人换过无数马甲内核从未变过。2.1 第一题String的 到底比的是什么题目如下String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // ? System.out.println(s1 s3); // ? System.out.println(s1.equals(s3)); // ? System.out.println(s1 s3.intern());// ?答案依次是 true、false、true、true。s1和s2由字面量创建编译器会把字符串放进常量池第二次遇到相同字面量时直接复用池里的对象所以两者引用相同 为true。s3通过new强制在堆上创建一个新对象和常量池里的s1是两个不同对象 自然为false。equals比较的是内容String类重写过equals所以为true。intern()的作用是把堆中的String对象入池如果常量池已有相同内容的字符串就直接返回池中引用因此s3.intern()返回的就是s1那个引用 还是true。这道题背后真正的考点有三个常量池机制、与equals的语义区别、intern的行为。面试官很喜欢追加一个追问如果s2写成 new String(abc).intern() 呢答案还是一样intern之后拿到的就是常量池引用所以和s1依旧是同一个对象。注意常量池里的字符串是JVM在类加载阶段就确定下来的和运行时的new完全是两码事。答题时把编译期决定和运行期创建这条界线划清楚基本就稳了。2.2 第二题静态方法能不能被重写这是一道经典判断题正确答案是不能。但很多候选人第一反应是能因为子类里确实可以写一个签名完全相同的方法。class Parent { public static void hello() { System.out.println(Parent.hello); } } class Child extends Parent { public static void hello() { System.out.println(Child.hello); } } Parent p new Child(); p.hello();这段代码里p.hello()输出什么答案是Parent.hello很多人在这里踩坑。原因在于静态方法属于类而不属于对象它在编译期就根据引用类型绑定好了不存在运行期的动态分派。所以Child里的hello只是把Parent的静态方法隐藏了而不是重写。想验证这一点有一个很笨但很有效的方法在子类方法上加Override注解编译器直接报错因为Override只允许标注在实例方法上。真正的实例方法重写考核点是方法签名、返回值协变、访问修饰符放宽这些细节我后续系列会单开一节展开。这里先把静态方法的隐藏和实例方法的重写区分开这一分就稳稳拿住。2.3 第三题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。关键在于自动装箱Integer a 127 等价于 Integer.valueOf(127)而valueOf会先查缓存。JVM默认缓存 -128 到 127 之间的Integer对象在这个范围内直接返回缓存中的同一个引用所以a和b指向同一个对象超出范围则new新对象c和d是两个不同对象。这个考点每年都有变体极多比如Integer e new Integer(100); Integer f new Integer(100); System.out.println(e f); // false两个不同的堆对象 System.out.println(e.equals(f)); // true内容相等再加上Integer与int的比较Integer g 200; int h 200; System.out.println(g h); // true有基本类型参与时会自动拆箱写答案时顺手把valueOf源码的缓存逻辑说清楚比写十行解释都顶用。下面是这组题的快速参照表比较场景结果原因字面量相同的String用 true常量池复用new String 与字面量用 false堆对象 vs 常量池对象Integer 127 之间用 trueIntegerCache缓存命中Integer 128 之间用 false超出缓存范围new新对象包装类与基本类型用 true自动拆箱3. 容器题HashMap底层、ArrayList扩容和Fail-Fast集合框架是Java笔试的另一个重灾区其中HashMap出镜率一骑绝尘。这一节挑三道最典型的不只是背结论而是把推演过程讲清楚。3.1 HashMap为什么是16和0.75常见问法是HashMap默认初始容量是多少加载因子是多少什么时候扩容标准答案是16和0.75容量达到16 × 0.75 12时扩到32。但只答到这里只能拿一半分剩下的一半在为什么。先说为什么初始容量用16。这背后有一个底层约束容量必须是2的幂。HashMap计算桶下标用的是位运算 (n - 1) hash而不是取模只有当n是2的幂时(n-1)的低位全为1哈希结果才能比较均匀地落在每个桶上碰撞概率最低。16是2的4次方够小够用又是个常用值所以被选为默认。再说为什么加载因子取0.75。这个值本质是空间和时间的权衡加载因子越小扩容越频繁内存浪费越多但哈希冲突越少、查询越快加载因子越大则相反。0.75是一个在冲突概率和内存开销之间都比较均衡的点。网上流传的一种说法来自JDK源码注释里的概率推算在加载因子0.75下单个桶里链表长度达到8的概率极低因此Java 8选择在链表超过8时转红黑树。这个推导过程面试官很吃这一套但如果你记不清细节就强调这是一个基于概率和工程权衡定下的默认值同样好过空口说结论。真正容易栽的是扩容机制的追问。扩容时容量翻倍但不只是数组变长还要求重新确定每个元素在新数组里的下标。Java 7的设计是逐个重新哈希Java 8做了优化元素要么留在原位置要么移动到原位置 旧容量的位置利用扩容后新增的高位bit直接判断省掉了大量取模运算。如果笔试问扩容时元素怎么迁移能答出这个区别就已经超过绝大多数候选人了。3.2 ArrayList的扩容为什么是1.5倍ArrayList这道题一般这么出ArrayList初始大小是多少扩容机制是什么答案要点JDK 8的new ArrayList()创建的是空数组第一次add时才扩容到默认容量10之后容量不足时新容量约为旧容量的1.5倍即 oldCapacity (oldCapacity 1)扩容本身靠的是System.arraycopy。追问多半是为什么是1.5而不是2。可以从两个角度解释。扩得太少比如每次只加一个元素那么频繁add就可能反复触发数组复制性能很差。扩得太多比如直接翻倍数组大部分空间空闲内存浪费明显。1.5倍是一个工程上的折中每次扩容的摊销成本与浪费的空间比例能达到一个平衡也兼容那些扩容后不会再增长的使用场景。这道题还有一个隐藏考点数组拷贝用System.arraycopy还是Arrays.copyOf两者都行但机制不完全一样Arrays.copyOf内部其实也调System.arraycopy。在扩容问题里说清楚创建新数组 批量拷贝就可以了。3.3 Fail-Fast为什么迭代时删除元素会抛异常代码输出题最爱出这个场景ListString list new ArrayList(); list.add(a); list.add(b); list.add(c); for (String s : list) { list.remove(s); }这段代码通常会在迭代到第二个元素时抛出ConcurrentModificationException。原因在于ArrayList内部维护了一个modCount字段每次结构性修改add、remove都会加一迭代器创建时会保存expectedModCount。每次调用next()迭代器都检查modCount是否等于expectedModCount不等就抛异常。上面这段代码第一次remove之后modCount变成1而迭代器的expectedModCount还是0等到第二次next()就被抓个正着。注意一个细节在JDK 8的实现里删除第一个元素后并不会立刻抛异常而是下一轮调用next()时才抛因为检查时机在next()而不是remove()。笔试选择题如果问异常在什么时候抛出答案要写下一次调用next()时不是删除那一刻。那正确删除该怎么做两个主流方案用迭代器自己的remove方法它会在删除后同步expectedModCount或者遍历结束后单独调用list.removeIfJDK 8之后这个方法非常实用。并发读多写多的场景还可以考虑CopyOnWriteArrayList它的迭代器基于快照不会抛ConcurrentModificationException代价是每次写操作会复制整个数组。4. 并发题volatile语义、synchronized锁对象与DCL单例并发是区分初中级开发的关键分水岭笔试最常见的三道题我全部摊开讲。这部分也是很多人背了又忘的重灾区必须把底层语义吃透。4.1 volatile到底保证了什么没保证什么很多人把volatile记成线程安全的万能钥匙这恰恰是命题人最爱的陷阱。volatile保证两件事可见性和有序性。它保证一个线程修改变量后其他线程能立刻看到最新值也禁止编译器与CPU对包含该变量的指令做重排序。但它不保证原子性。经典考题public class Counter { private volatile int count 0; public void increment() { count; } }10个线程各调用1000次increment最终count是多少大多数情况下不是10000而是一个小于10000的数。因为count根本不是原子操作它由读取、加一、写回三步组成。volatile虽然保证了写回后其他线程能看到但两个线程可能同时读到相同的旧值各自加一写回结果就丢了一次更新。如果笔试里遇到如何让这个Counter线程安全优先答AtomicIntegerCAS实现或者synchronized方法。注意如果是一个线程写多个线程读的场景volatile完全够用读多写少时它几乎没有性能损失。4.2 synchronized修饰静态方法和实例方法的差别这道题常以判断对错形式出现一个类里有一个synchronized的静态方法还有一个synchronized的实例方法两个线程分别调用它们会不会互斥正确答案是不会。因为synchronized静态方法锁的是类对象Class对象synchronized实例方法锁的是当前实例对象this这是两把完全不同的锁根本不互斥。想互斥要么都锁Class对象要么都锁同一个实例对象。再往外延伸一层候选人经常混淆的还有同一个类的两个不同实例各自调用synchronized实例方法会不会互斥答案也是不会锁是挂在各自实例上的。这也能解释为什么很多框架要求单例模式单例下锁才有意义。笔试答到这个深度基本可以判断你是真懂锁机制而不是背了两行定义。4.3 双重检查锁单例为什么要加volatile单例模式是笔试手写题的高频题双重检查锁DCL十个人里九个人会写但很多人少了一个关键修饰符class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }这里的volatile不是可有可无。instance new Singleton()在字节码层面不是一步它大致包含三步分配内存、初始化对象、把引用赋值给instance。由于指令重排序可能出现先赋值引用、后初始化对象的顺序。此时另一个线程在第一次if判断时看到instance不为null直接返回拿到的却是一个还没完成初始化的半成品对象。volatile禁止了这条赋值路径上的重排序保证引用赋值一定发生在对象初始化完成之后。这道题答到防止指令重排序导致返回未初始化对象就到位了。如果笔试只让写单例、没指定DCL我更推荐你写静态内部类版本代码更短也不容易出错这个以后有机会再展开。5. 手写代码题快排、链表反转和有效括号手写算法在Java笔试里占比不算最高但属于一道定生死的题型——基础题大家都会算法题才是拉开分差的位置。第一季先安排三道最容易被考、且实现起来不长的题。5.1 手写快速排序快排是排序题里的绝对王者笔试要求通常是原地排序、不用额外数组。最稳妥的写法是这样public void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivotIndex partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex 1, right); } private int partition(int[] arr, int left, int right) { int pivot arr[right]; int i left; for (int j left; j right; j) { if (arr[j] pivot) { swap(arr, i, j); i; } } swap(arr, i, right); return i; } private void swap(int[] arr, int a, int b) { int temp arr[a]; arr[a] arr[b]; arr[b] temp; }这里用数组最后一个元素做pivot从左往右扫描比pivot小的往前换最后把pivot换到中间位置这种partition写法最少犯错建议背熟。笔试里通常还会追问复杂度。快排平均O(nlogn)但数组本身有序时退化成O(n²)所以很多增强写法会引入随机pivot把退化概率压到极低。手写时不要上来就搞三数取中先把基础版写对有时间再优化。5.2 反转单链表链表题在机试里出现频率极高反转单链表是最基础的一题可每年依然有大量候选人写不出来。迭代版关键代码public ListNode reverseList(ListNode head) { ListNode prev null; ListNode cur head; while (cur ! null) { ListNode nextTemp cur.next; cur.next prev; prev cur; cur nextTemp; } return prev; }思路就一句话遍历时把每个节点的next指向前一个节点。三个指针prev、cur、nextTemp顺序绝对不能乱先保存nextTemp再改cur.next然后移动prev和cur。很多人错就错在改完cur.next之后把原本的下一个节点弄丢了所以记住四个字先存后断。追问一般是递归版怎么写如果是每K个一组反转呢第一季先掌握迭代版递归版和K组反转我放在后续系列里和更多链表题一起讲。5.3 有效括号匹配给定一个只包含 (、)、[、]、{、} 的字符串判断它是否有效——这题是栈应用的入门题出镜率极高。public boolean isValid(String s) { DequeCharacter stack new ArrayDeque(); for (char c : s.toCharArray()) { if (c ( || c [ || c {) { stack.push(c); } else { if (stack.isEmpty()) { return false; } char left stack.pop(); if (c ) left ! () { return false; } if (c ] left ! [) { return false; } if (c } left ! {) { return false; } } } return stack.isEmpty(); }三个易错点遇到右括号时栈为空直接返回false字符串遍历完栈里还有残留说明左括号多了像([)]这种交叉嵌套是false不能只数左右数量相等就放过去。机试的隐藏用例专门抓这些边界写的时候一定留一手。6. 代码输出题finally、i和类型提升代码输出题是笔试里最阴的部分很多时候不是不懂原理而是被题目的文字游戏骗了。这一节挑三道翻车率最高的把思维掰回正轨。6.1 finally和return谁先执行题目public static int test() { try { return 1; } finally { System.out.println(finally); } }输出是什么先输出finally然后方法返回1。很多人记成finally在return之后执行这是错的。finally块在return表达式计算完之后、方法真正把值返回给调用者之前执行。打个不太严谨但好记的比方return相当于打包寄件finally是在包裹寄出之前做最后检查。更狠的追问是public static int test() { try { return 1; } finally { return 2; } }这个方法返回多少答案是2。因为finally里有return它会把try里准备好的返回值整个覆盖掉。实际开发中没人会这么写但笔试爱出因为能检测你是背了结论还是真的理解执行顺序。6.2 i i 到底等于几这段代码是笔试选择题里的万年老坑int i 0; i i; System.out.println(i);输出是0。原因要拆到执行层面i这个表达式的值是自增前的旧值先把旧值0存起来然后i自增变成1最后把存起来的旧值0赋回给i于是i又变回0。对比记忆int j 0; j j; System.out.println(j); // 输出1j表达式的值是自增后的新值计算完j是1再把1赋回去结果就是1。考场上如果一时转不过弯记住一句话i表达式的值是自增前的值i表达式的值是自增后的值。6.3 short运算为什么会编译报错最后一道基础题short a 1; short b 2; short c a b;这段代码能编译通过吗不能。Java的类型提升规则里short和short做算术运算时都会先提升为int所以a b的结果是int类型直接赋给short类型的c会报不兼容的类型错误。改成 short c (short)(a b) 才能编译通过。有个容易混淆的变体short a 1; a 1;这段是能编译通过的。因为复合赋值运算符、-等会自动进行隐式类型转换等价于 a (short)(a 1)。这也是笔试特别爱埋的点明明看着差不多一个报错一个不报错。这组输出题和前面的基础题其实是互相呼应的如果你能把编译期类型和运行期值分开思考大部分代码输出题都能一次答对。7. 一个老考生的刷题心得7.1 错题整理的核心套路这批题我每年帮候选人复盘时都会反复讲讲到最后最想说的一点是笔试题不是用来死记答案的是用来校准认知的。你收藏一百套题不如把一套题里的每个为什么都问到底。比如我上面写到HashMap加载因子是0.75如果你不追问一句为什么是0.75下次它换个问法你还是会懵。我的错题整理方法很简单每道错题标出它真正考的知识点比如第6.2节那道i i我归类为赋值运算符与自增/自减的执行顺序而不是归为输出题。同一个知识点下的题目不管伪装成哪种形式核心解法都是一套。把这套核心解法浓缩成一句话写进清单后面复习就只能看清单不看原题——看原题容易产生我会了的错觉看一句话考点清单才是真实水平检验。7.2 考前复习与后续规划我个人觉得最有效的刷题节奏是每一季题目先空刷一遍错题标红然后对着答案重新推演每道题的执行过程能不能在脑海中的内存模型里把每一步跑通是判断自己是否真懂的标准最后把错题归纳成十行以内的知识点清单考前只看清单。这样刷下来第一季这二十多道题大概需要两个晚上但效果比囫囵吞枣刷二百道雷同题好得多。后面这个系列我会接着拆JVM内存模型、类加载过程、面向对象深挖题还有手写算法里的递归和动态规划。如果你手头有拿不准的真题也可以先记下来等系列更新到对应板块时再对一遍思路。笔试这条路没有太多捷径但每道错题都值得认真对待——考场上丢掉的分往往就是平时少问的那句为什么。
阅读完成 · 觉得有帮助?