Java技术类选择题复盘:从校招真题看高频考点与知识体系构建
互联网公司校招笔试里技术类选择题曾经是筛简历之后的第一道坎。上个月有个学弟准备秋招翻出一份京东2017校招技术类选择题一的回忆版做完跑来问我“答案我看了但换一道类似的题我大概率还是错怎么办”我说你肯定只是对了答案。这类题的价值从来不在ABCD里而在每个选项背后牵出来的知识网络。这篇博文就是我当时给他讲题时的高频考点还原不保证跟原卷一字不差但题型和坑位都是当年真实出现过的适合正在准备校招、实习笔试的同学参考也适合想快速过一遍Java基础的技术人。技术类选择题虽然现在很多公司改成了在线编程和问答题但核心考点就那几块Java语法与容器、并发基础、JVM与类加载、网络、数据库。把这几块的底层逻辑吃透比刷一百套题管用。下面我按复盘方法、典型题目拆解、连环追问、失分点、老题价值这几个角度来讲。1. 技术类选择题复盘方法把一套题的价值榨干1.1 选择题其实是“有提示的简答题”很多人做选择题时有个坏习惯排除两个明显不对的剩下两个靠记忆去猜选完就走。如果对了就觉得自己会了如果错了看一眼正确答案记下来然后继续下一题。这种刷题方式效率极低因为你在训练的是“猜题能力”不是“知识能力”。把选择题换一个角度理解四个选项不是用来让你选的而是用来提醒你的。它就像简答题的参考答案碎片每个选项都是一条线索。你做对一道题至少应该能说清楚四个选项里每一个为什么对、为什么错。举个例子题目问“HashMap默认初始容量是多少”如果你只是记住了16那我追问一句“为什么是16而不是15或20”你答不上来这道题在面试官眼里就等于没做对。我在复盘这套2017年的选择题时坚持的标准是一道题如果不能在30秒内把每个选项的理由讲给旁边的人听就说明这个考点还没有真正变成你的东西。选择题的正确打开方式不是“选答案”而是“讲题”。一个简单有效的自测方法是把自己当成面试官对着选项逐个追问为什么能答得上来才叫会。1.2 三遍复盘法把一套题的价值榨干我给学弟定的复盘节奏是分三遍走每一遍的目的都不一样。第一遍是模拟考试。限时做不翻书不查资料做完之后只对答案把错题标出来。这一遍的目的不是学习而是摸底让你知道自己哪些地方是真不会哪些地方是粗心。第二遍是逐题拆解。不管是做对的题还是做错的题都要把每个选项的原因写一遍。对的题要确认自己的思路没有问题错题要追根溯源找到当初记错或理解错的知识点。比如这道题考的是String不可变性你就应该翻出源码看看count和value数组是怎么定义的而不是背一个“String不可变”的结论。第三遍是归类串联。把整套题按考点归类Java基础多少题、并发多少题、网络多少题、数据库多少题哪一块错得最多哪一块就是你复习的重点。然后针对薄弱模块把同类知识点的题目从历年其他公司校招题里找出来集中做。我见过太多同学刷题只走第一遍第二遍第三遍直接跳过结果就是“题目认识我我不认识题目”。技术类选择题考察的最大能力其实是知识结构化能力你脑子里不是零碎的知识点而是一张能快速定位、能互相联通的网。三遍复盘法看着费时间但比起刷了忘、忘了刷的循环反而省事。2. 京东2017校招Java高频选择题重点拆解2.1 i i 输出结果从一道送分题看字节码与并发2017年这套题里有一道很经典的代码输出题大概是下面这个风格public class Test { public static void main(String[] args) { int i 0; i i; System.out.println(i); } }问输出是0还是1。很多人第一次做这道题都会掉坑里因为凭直觉会觉得ii变成1再赋值给i结果就是1。但实际输出是0。原因在于Java中i这个表达式整体的值是自增之前的值也就是旧值0。整个赋值过程是先用i的旧值0算出表达式结果再把i自增成1最后把右侧表达式的结果0赋给i。所以i先经历了0-1又经历了1-0最终输出0。如果想看得更透彻可以看字节码。把上面的类用javap -c反编译main方法里会有类似这样的操作先把常量0压栈存到局部变量表然后从局部变量表加载i的值执行iinc自增指令再把之前保存的旧值弹回局部变量表。关键就是这条iinc指令它先把i改成了1但后续又把压栈的旧值0存了回去等于自增被覆盖了。这道题讲完我顺势问了学弟一个延伸问题如果多个线程同时执行i会出现什么情况答案是会丢失更新。因为i不是一个原子操作它底层是“读取→修改→写入”三步线程切换可能发生在任意两步之间。两个线程同时读到0各自加1后写回结果i最终是1而不是2。这个问题的本质属于并发编程也是后面第3章要展开的连环追问起点。注意不要在项目代码里写带副作用的表达式比如array[i] value里的变量在两个地方被修改这种写法可读性差且容易踩坑。面试题是面试题工程代码是工程代码。2.2 HashMap 默认容量与扩容机制为什么是16、0.75、2倍这套选择题里HashMap的出场率非常高几乎每年都有。常见问法包括默认初始容量是多少默认负载因子是多少什么时候扩容扩容后旧数据怎么迁移。答案大家都知道默认初始容量16负载因子0.75元素个数超过容量 * 负载因子时扩容为原来的2倍。但如果只答到这里这道题你只拿到了一半分数。关键是理解“为什么”。先看容量为什么是2的幂次。HashMap在JDK 8里定位数组下标时不是用hash % length取模而是用hash (length - 1)这个位运算能生效的前提就是length必须是2的幂次。因为当length是2的幂次时length - 1的二进制低位全是1hash (length - 1)能等效于取模而且比取模快得多。如果length不是2的幂次低位就不是全1散列结果会大打折扣哈希碰撞概率明显上升。再看负载因子为什么是0.75。这是时间成本和空间成本的折中负载因子越大比如1代表数组快满了才扩容空间利用率高了但哈希冲突也变多链表会变长查找效率下降负载因子越小比如0.5冲突少了但浪费空间频繁扩容也会带来性能开销。0.75是官方在大量统计和实验基础上选出来的比较均衡的值。再说JDK 8引入的树化机制。当某个桶的链表长度超过8并且数组长度不小于64时链表会转成红黑树。为什么选8这个阈值官方注释里提到遵循泊松分布在负载因子0.75下链表长度达到8的概率大约是千万分之六属于极小概率事件。如果真出现链表这么长多半是key的hashCode实现太烂这时候用红黑树来扛一下保证最差情况下的查找效率。2017年的题目里还有个高频坑HashMap在多线程环境下扩容。JDK 7的扩容在并发场景下可能让链表形成环导致get操作死循环。JDK 8里也依然不是线程安全的。所以并发场景请用ConcurrentHashMap而不是在外面套一个synchronized锁整个map。这道题用来考察候选人对并发容器原理的理解程度直到今天都不过时。2.3 String 与常量池一个构造方法能创建几个对象String是Java里最基础也最容易出题的类。这套选择题里有一道非常经典的问题String a new String(abc);一共创建了几个对象标准回答是1个或2个。如果字符串常量池里已经有abc那么只创建一个堆中的String对象如果常量池里还没有abc那么JVM会先在常量池中创建abc再在堆中创建String对象一共2个。这里要注意字面量abc对应的对象是在常量池里的而new出来的对象在堆上两者不是同一个对象所以new String(abc).intern() abc在JDK 7之后通常返回true因为intern()会把堆中字符串的引用放入常量池。顺着这道题又能延伸出String不可变性的讨论。String类内部是private final char value[]JDK 8及以前JDK 9之后改成了byte[]但不可变的性质没变。为什么String要设计成不可变第一可以安全地作为HashMap的key因为hashCode在创建时就缓存在int hash字段里如果String可变缓存就失效了第二常量池可以放心复用字符串不用担心某个引用修改了它导致其他引用看到脏数据第三多线程环境下不可变对象天然线程安全不需要加锁。还有一个经典拼接问题String s a b c;创建了几个对象答案是1个如果常量池里没有abc那么JVM编译器在编译期就会把常量字符串拼接优化成abc运行期不需要额外创建对象。但如果写String s a; String t s b;由于s是变量编译器无法在编译期确定结果运行时会通过StringBuilder拼接生成新的String对象。这个区别在很多笔试题中出现过本质是考察编译期常量折叠。注意不要用String做高频字符串拼接会产生大量中间对象。日常开发用StringBuilder方法内用局部变量即可不需要刻意全局使用StringBuffer。2.4 finally 与 return谁先执行谁覆盖谁题目风格大概是这样的public static int test() { int x 1; try { return x; } finally { x 2; } }问方法返回什么答案是1。为什么因为当return x执行时JVM会先把x的值1保存到返回值槽里然后去执行finally块。finally把x改成2但返回值槽里存的还是1。所以finally对局部基本类型的修改不会影响已经确定的返回值。但有一个前提如果finally块里也有return情况就完全不同了。finally中的return会覆盖try或catch中return的返回值比如下面的代码public static int test() { try { return 1; } finally { return 2; } }这个方法的返回值是2。因为finally块在方法返回之前一定会执行一旦finally执行了return 2方法就直接从这里返回之前的返回值被丢弃。更严重的是如果try块里抛了异常而finally块里也有return那么try里的异常会被静默吞掉。这在工程里是个大坑排查问题时你会看到方法明明没有抛异常结果却不正确特别难定位。讲到这里我给学弟的建议是生产代码中不要在finally里写return也尽量不要在finally里做可能抛异常的操作。finally的正确用途是释放资源、关闭连接、清理状态而不是修改返回结果。很多人背下了“finally一定会执行”这句话但忽略了“finally中的return会导致提前返回并吞异常”这个重要例外。这类题目考察的其实是Java语言规范里关于异常处理和返回语句的执行顺序理解字节码层面的操作之后自然就不会记混。2.5 双亲委派模型类加载器为什么要把请求往上抛JVM类加载相关题目也是2017年前后校招选择题的热门。最典型的问题是JVM自带的类加载器有哪几个它们之间是什么关系双亲委派模型是怎么回事。三个内置类加载器启动类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoaderJDK 9后叫Platform ClassLoader、应用类加载器AppClassLoader。它们的层次关系是AppClassLoader的父加载器是ExtensionExtension的父加载器是Bootstrap但注意这里的“父”不是继承关系而是组合关系每个子加载器里持有父加载器的引用。双亲委派模型的加载流程是这样的当一个类加载器收到类加载请求时它不会自己先去加载而是先把这个请求委派给父加载器父加载器又会继续往上委派直到Bootstrap ClassLoader。只有在父加载器找不到这个类时子加载器才会尝试自己去加载。比如我们自己写一个java.lang.String即使编译通过运行时也不会被Bootstrap之外的类加载器加载因为Bootstrap已经加载过标准的java.lang.String了子加载器不会重复加载。为什么要设计成这样核心目的有两个。第一避免同一个类被不同的类加载器重复加载造成内存中同时存在多个版本的核心类那会乱套。第二保证Java核心类库的安全。如果允许自定义类加载器自己加载java.lang.String恶意代码就可能替换核心类往里面塞一些危险逻辑整个JVM的安全模型就崩塌了。还有一个衍生考点双亲委派的“破坏”场景比如JDBC、Tomcat这类需要SPI机制的容器它们通过ContextClassLoader打破双亲委派让父加载器可以去加载子加载器路径下的实现类。这个知识点比较深但在面试追问里出现的概率很高我建议准备校招的同学把“为什么要打破、怎么打破”也一起看掉。3. 连环追问从一个考点扩展成一棵知识树3.1 volatile 能否解决 i 的原子性前面2.1节的i并发问题往深挖就是volatile和原子性的辨析。很多同学只知道volatile有两个作用保证可见性和禁止指令重排序但对原子性这个概念很模糊。我曾问学弟“把i声明为volatile多线程执行i就安全了吗”他想了想说不安全但要他说出为什么卡壳了。原因很简单volatile只保证一个线程修改了i之后其他线程能立刻看到最新值但它不保证“读取→修改→写入”这三步作为一个整体不被中断。两个线程仍然可能同时读到同一个旧值各自加1再先后写回导致更新丢失。这就是复合操作需要额外机制来保证原子性的原因。解决方式有三个常见方向一是给方法加synchronized简单但并发度会下降二是用AtomicInteger底层是CASCompare And Swap利用CPU指令实现“比较并交换”的原子性三是在高并发、写多读少的统计场景下用LongAdder通过分段累加降低竞争。我在工程中写过统计接口请求次数的代码测试下来单机高并发下LongAdder在写操作非常多时表现更平滑AtomicInteger在竞争激烈时自旋次数会明显增多。再到CAS本身又有一个经典坑位叫ABA问题线程1读到A线程2把A改成B又改回A线程1的CAS比较发现还是A就认为没变过继续执行。解决思路是带版本号比如AtomicStampedReference。这一连串追问下来从一道i题可以牵出并发编程的完整知识树比单独背volatile特性有用得多。3.2 数据库事务隔离级别与 MVCC这套题里如果有数据库题大概率会考事务隔离级别。四个级别分别是读未提交、读已提交、可重复读、串行化对应的脏读、不可重复读、幻读问题这里不过多重复定义。我想强调的是“可重复读在InnoDB里是怎么实现的”这个点因为MySQL 8之前的InnoDB默认隔离级别就是可重复读而且它通过MVCC基本解决了幻读问题。MVCC的核心是undo log版本链和ReadView。每一行数据在更新时并不会直接覆盖旧值而是生成一个新版本旧版本通过undo log串成一条版本链。事务在执行快照读时会根据ReadView判断哪些版本对当前事务可见。可重复读隔离级别下ReadView是事务第一次执行快照读时生成的之后整个事务内都用这个ReadView所以同样一个查询在事务多次执行时看到的是同一个快照这就是“可重复读”名字的由来。但注意快照读和当前读是不同的。select ... for update、insert、update、delete这类当前读看到的是最新已提交数据不能靠MVCC的ReadView来阻止幻读还需要配合间隙锁。所以InnoDB实际上是通过“MVCC解决快照读下的幻读 间隙锁解决当前读下的幻读”双重机制来保证可重复读的事务隔离效果。如果只背概念这个链条很容易断掉但面试官一次追问就能卡住。3.3 TCP 三次握手为什么不能省成两次网络方向也是技术类选择题的常客。TCP三次握手这道题大家都很熟但换个角度问为什么不能只握两次很多人的回答是“为了确认双方收发能力正常”这不错但不完整。补充一下第一次握手客户端发送SYN服务端收到了服务端能确认自己的接收能力和客户端的发送能力正常第二次握手服务端发送SYNACK客户端收到了客户端能确认自己的发送能力和服务端的接收能力正常。到这里客户端已经确认了双方读写能力但服务端还不知道自己的发送能力和客户端的接收能力是否正常。因此还需要第三次握手客户端回复ACK服务端收到后服务端才能确认自己的发送能力和客户端的接收能力都正常。所以三次握手本质上是“双方互相确认收发能力都正常”的最小次数。如果只握两次会出现一种极端情况客户端第一个SYN因为网络延迟很久后到达服务端服务端认为有新连接建立回复SYNACK如果只有两次握手连接直接就建立了但客户端此时并不想建立连接会造成服务端白开一个连接还浪费资源。三次握手通过第三次ACK或客户端连接不存在时的RST包就能及时清理这种半开的无效连接避免服务端资源被无效占用。这个知识点进一步延伸到SYN泛洪攻击攻击者大量发送SYN但不回应第三次ACK服务端的半连接队列会被塞满正常用户连不上。防御手段包括启用SYN Cookie、增大半连接队列长度等。一套题做到这里从TCP握手能一路问到网络安全这也是选择题复盘最有价值的地方。4. 常见失分点与笔试备考策略4.1 失分点自查表结合我复盘这套2017年题目的过程也结合这届学弟做题时错误比较集中的地方整理了一个失分点对照表你可以拿去自查。失分点典型表现解决策略只看结论不看原因记住HashMap默认容量16但说不清为什么是16、0.75每个结论都问自己“为什么”从源码和官方注释里找答案选项分析不到位做对了但说不清其他选项错在哪把每题当成四道判断题逐个选项写对错理由代码执行过程靠猜i i这类题不看字节码凭感觉选写个小程序跑一遍再用javap反编译看指令顺序知识零散不成网会单答volatile作用但不知道和原子性的关系用第3章的连环追问法从一个点连到一串点忽略异常处理陷阱以为finally必定执行且不影响返回值专门整理异常表、返回值表实测几种写法复习没有重点所有考点平均发力薄弱模块没补救第三遍复盘按模块统计正确率集中补弱我在平时面试候选人的时候发现一个很有意思的规律简历上写了“熟悉常用集合类”的人十个里有六七个能答出HashMap是数组链表/红黑树但能讲清楚树化阈值为什么是8、扩容为什么是2倍的人比例会急剧下降。知识覆盖度和知识深度是两回事选择题笔试考察覆盖面后续面试追问就在考察深度。复习的时候要注意两者的平衡。4.2 笔试前的最后一晚该干什么笔试前一晚不建议再大量刷新题。这时候最该做的是翻错题本和笔记。我自己的习惯是把每个高频考点用一句话写在一张A4纸上比如“HashMap默认16/0.75/2倍/链转树/为什么”、“i表达式值为旧值”之类睡前快速过一遍。这种做法不是临时抱佛脚而是把已经掌握的知识在短时间内激活一遍保持大脑对关键点的敏感度。还有一个特别实用的技巧前一天的晚上把每个考点想一下“面试官如果接着问会怎么问”在心里把回答流程过一遍。比如看到“volatile的可见性”立刻想“那原子性呢CAS呢ABA呢”让知识以问答链的形式在脑子里滚动一遍。比干坐着背概念效率高很多。至于做题节奏笔试的时候注意别在一道题上纠结太久。技术类选择题大多有明确答案花两三分钟还拿不准的先标记跳过去做完其他题再回头。我在平时练习时也推荐限时训练一套选择题控制在45分钟内完成训练选择题的决策速度和时间分配能力。5. 老题的新价值2017年的题目放到今天还适用吗有同学觉得2017年的题太老担心考点已经过时。我的判断是这些题里的语法细节、API参数可能会变但核心原理几乎没有过时。Java语言规范、JVM类加载机制、TCP/IP协议、数据库事务隔离级别这些基础不会因为时间推移而改变。2017年考双亲委派今天面试仍然可能问当年考TCP三次握手现在依然是高频考点。不过也要承认技术类考点的侧重点确实发生了变化。2017年前后国内校招笔试很喜欢考一些偏记忆的细节比如某个类的方法返回类型是啥某个接口在哪个包下面。而现在更多公司偏向线上OJ编程题和系统设计题考察的是工程能力和解决问题的思路。但这不意味着当年的选择题就不值得看它反而是覆盖面最全的考点地图适合用来做知识体检。我在复盘这套题的时候还有一个明显感受老题往往比新题更“老实”。它不会故意绕弯子考察的就是最核心的知识点和最常见的坑非常适合用来建立知识框架等框架搭起来了再去刷更灵活的新题才有底气。把2017年的这套题当作地基把JDK 8到JDK 17的变化当作上层建筑应对现代校招完全够用。最后再分享一个小技巧准备笔试时把做错的题用一句话写在固定本子上不要贴大段答案解析只写“错在哪、正确结论一句话”。这样在秋招前一夜能快速复习一遍比翻厚厚的错题集高效很多。我当年就是这么过来的帮学弟复盘时也用了同样的方式效果还挺明显。