
2019年秋天的校招季我在投递顺丰科技Java开发工程师岗位时最先迎头撞上的不是面试官而是一套覆盖面极广的客观题。这套题从Java基础语法一路扫到集合框架、JVM、并发、编码规范题量大、时间紧靠临时背题基本撑不过去。当时自己也是边踩坑边总结后来帮几届学弟学妹复盘同类试卷时发现虽然年份写的是2019但它本质上就是一份非常典型的Java工程师基础能力笔试和今天各大厂的校招客观题题库几乎共用一套逻辑。这篇内容我就以这套题为引子把Java客观题里出场率最高的考点、做题时的判断思路、以及复习时容易忽略的盲区逐块拆开讲清楚。1. 为什么2019年的客观题放到今天仍然值得刷先聊一个很多人会问的问题都过去好几年了Java版本都升到17、21了刷2019年的题还有意义吗我的答案是非常有必要前提是你得知道怎么刷。1.1 客观题背后真正的筛选逻辑大厂笔试环节设置客观题目的不是考倒你而是做海选过滤。一份简历可以写熟练掌握Java但客观题没办法造假——它考察的是你的知识有没有形成体系记忆是不是准确以及面对干扰项时能不能快速识别陷阱。顺丰科技这类物流科技公司业务场景覆盖仓储、运输、调度、末端派送等大量系统笔试环节更需要用同一套标准去衡量候选人的代码基础客观题是最经济的筛选方式。这套题有几个明显特征选择题居多选项之间往往是一字之差覆盖面很广但单点深度不会太深偏爱考底层原理的宏观结论而非具体算法细节。换句话说它考的是你知不知道而不是你能不能现场推导。但也正因为如此很多人栽在做题速度上——不是不会而是每道题都在几个选项之间犹豫太久。1.2 从Java 8到Java 17核心考点其实没怎么变我整理过不同年份的校招题库发现一个规律ArrayList扩容、HashMap转红黑树、synchronized锁升级、类加载双亲委派、线程池拒绝策略这些点几乎年年都出现。原因很简单它们背后是整个Java运行时的核心机制是任何业务开发都绕不开的地基。Java版本升级带来的变化比如switch表达式、record、sealed class虽然逐渐进入新题库但在校招客观题里的占比仍然远低于经典考点。所以我的建议是这套题完全可以直接当今天的复习材料使用把注意力放在那些十年不变的核心机制上再额外补充一些Java新特性的基础概念应付校招笔试就够用了。2. 语言基础客观题从面向对象到运算符的抢分策略客观题里最不应该丢分的就是语言基础部分。这部分题目通常不难但陷阱密度很高尤其是面向对象、字符串、运算符这三块。2.1 接口与抽象类的区别几乎每场笔试都有这道题太经典了经典到很多人已经条件反射抽象类是is-a关系接口是can-do能力契约一个类只能继承一个抽象类但能实现多个接口。但你以为这就完了笔试不会只考一句结论它会把结论拆成散落在四个选项里的细节。当年类似的一道题我印象很深下列关于接口和抽象类的描述正确的是 A. 接口可以有构造方法 B. 抽象类不能有构造方法 C. 一个类可以实现多个接口但只能继承一个抽象类 D. 接口中所有方法都必须是抽象的A和B是同一个知识点接口不能有构造方法抽象类可以有构造方法只不过这个构造方法是给子类实例化时用的抽象类本身不能直接new。C是正确的。D在Java 8之后就错了因为接口里可以有default方法和static方法。很多人如果只背了Java 7那套口诀遇到D就会犹豫。做题的时候我建议养成一个习惯把每个选项当作一个判断题不仅要判断对错还要在脑子里补出为什么对、为什么错。这样一道题你花的时间看似多了但知识网络会被反复加固后面遇到类似的变体题能秒选。2.2 字符串与包装类笔试里的文字游戏重灾区String、StringBuilder、StringBuffer这组对比以及包装类缓存是语言基础里最容易被绕进去的地方。先记牢这几点String是不可变对象字符串常量池在JDK 1.7之后移到了堆中new String(abc)会创建两个对象常量池一个、堆上一个但a b如果是编译期常量会在编译期直接拼接成ab。StringBuilder线程不安全StringBuffer线程安全但方法上加了synchronized性能更低。包装类的equals比较的是值比较的是对象引用Integer在-128到127之间有缓存Integer a 100; Integer b 100;用判断是true一旦超过127就是false除非你new Integer()。典型的笔试题长这样Integer a 128; Integer b 128; System.out.println(a b); // false System.out.println(a.equals(b)); // true String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true System.out.println(s1 s3); // false客观题的选项一般会把值相等引用相等缓存范围混在一起你只要记得除了基本类型的是比数值其他一切都是比地址。这句话能救你半张卷子。2.3 运算符优先级与数组边界送分题不要变成送命题运算符部分笔试很少考复杂的优先级表格背诵更多是考短路运算和自增自减。和的区别有短路效果左侧为false时右侧不执行左右两侧都会执行。有一类经典题int a 5; boolean b (a 3) (a 5); System.out.println(a); // 5因为短路a根本没执行如果换成a就变成6了。这种题考的就是你有没有真正理解表达式求值的时序。数组越界是另一个高频点。ArrayIndexOutOfBoundsException的触发条件是访问了下标为负数或大于等于length的位置。注意数组的length是属性不是方法字符串的length()才是方法。这两个混在一起也是常设陷阱。还有标识符命名规则只能由字母、数字、下划线、美元符号组成不能以数字开头不能和关键字重名Java中关键字都是小写的比如true、false、null虽然不是关键字但是是字面量同样不能用作变量名。这类题属于看你细不细心的题做题时逐项检查就行。3. 集合框架题HashMap是客观题的绝对C位如果说语言基础是保分题那集合框架就是拉分题。一套Java客观题里集合能占到20%-30%的比例而HashMap又会占掉集合题里的大半江山。3.1 ArrayList扩容与modCount最容易踩冷门细节很多人对ArrayList的理解停留在动态数组默认容量10但笔试会考得更细JDK 1.8里new ArrayList()并不会直接创建容量为10的数组而是在第一次add时才初始化初始化容量也是10。扩容时新容量大约是旧容量的1.5倍oldCapacity (oldCapacity 1)底层调用Arrays.copyOf。modCount字段记录结构性修改次数迭代器遍历时会检查modCount是否变化一旦变化就抛ConcurrentModificationException这就是fail-fast机制。接着看ArrayList和LinkedList的对比ArrayList基于数组随机访问O(1)插入删除在中间位置需要搬移元素LinkedList基于双向链表中间插入删除理论上是O(1)但前提是你已经持有那个位置的节点否则找节点还是O(n)。很多笔试题会说LinkedList的插入删除一定比ArrayList快这是错的——在尾部追加时ArrayList往往更快因为LinkedList还要新建节点、维护前驱后继。3.2 HashMap的put流程背下来不如理解下来HashMap是客观题里的必考点必错点。考察方向通常集中在初始容量与负载因子、哈希与散列、链表转红黑树的条件、扩容机制。先把核心参数记牢参数值说明默认初始容量16必须是2的幂默认负载因子0.75元素个数超过容量*负载因子时扩容链表转红黑树阈值8单链表长度超过8且数组容量64时转红黑树红黑树转链表阈值6树节点数减到6时转回链表最小树化容量64数组容量小于64时优先扩容不直接树化做关于HashMap的判断题只要记住一句话树化条件不是链表长度到8这一个它还需要数组容量64。很多题会把这两个条件拆开让你误以为只要链表长度到8就一定转树。HashMap的put流程笔试不一定会让你手写但可能会以以下哪个步骤不在put方法中之类的形式出现。完整流程是先对key的hashCode做扰动计算高16位异或低16位然后按(n - 1) hash定位桶下标如果桶为空直接放新节点如果桶不为空判断首节点是否key相同相同则覆盖否则判断首节点是否为TreeNode是就走红黑树插入不是就尾插法遍历链表链表长度超过阈值后尝试树化最后判断扩容。还有一个常考细节JDK 1.7用头插法扩容时多线程环境下可能形成循环链表造成死循环JDK 1.8改成尾插法避免了这个问题但HashMap仍然不是线程安全的。所以JDK 1.8的HashMap是否线程安全这道题答案是否。3.3 fail-fast与ConcurrentHashMap并发场景的基础判断集合里除了HashMapConcurrentHashMap也是热门考点。关键是理解它的演进JDK 1.7的ConcurrentHashMap用Segment分段锁理论上最大并发度等于Segment数默认16。JDK 1.8抛弃了Segment直接用CAS synchronized锁住桶头节点锁粒度更细并发度更高。读操作大部分不加锁借助volatile保证可见性。做题时凡是出现ConcurrentHashMap允许null key和null value的选项直接排除。HashMap允许一个null key和多个null value但ConcurrentHashMap和Hashtable都不允许。这个差异是经典陷阱。fail-fast机制也是客观题常客。除了ConcurrentModificationException的触发场景还会考CopyOnWriteArrayList和普通ArrayList在迭代时的差异——CopyOnWriteArrayList在迭代时修改不会抛异常因为它遍历的是修改前的快照。还有一句话值得记住Hashtable是线程安全的但所有方法都加synchronized性能差已经属于遗留类。如果选项里吹捧Hashtable的并发性能那基本就是错误项。4. JVM与内存题遇到OutOfMemoryError时题目在考什么很多人在生产环境第一次接触JVM就是那句java: OutOfMemoryError: insufficient memory。但客观题不会让你解决一个具体堆栈它考察的是你对JVM运行时数据区、类加载和GC的宏观理解。4.1 运行时数据区划分与OOM家族JVM运行时数据区可以画成六个区域程序计数器、虚拟机栈、本地方法栈、堆、方法区JDK 1.8后被元空间取代、运行时常量池在堆或元空间中。客观题爱考的是哪个区域溢出对应什么异常区域溢出类型常见原因堆OutOfMemoryError: Java heap space对象太多GC来不及回收虚拟机栈/本地方法栈StackOverflowError递归调用过深方法区/元空间OutOfMemoryError: Metaspace动态生成类太多直接内存OutOfMemoryError: Direct buffer memoryNIO使用不当这里有一个容易混淆的点StackOverflowError是错误不是异常它和OutOfMemoryError都继承自Error不属于受检异常。判断题如果写StackOverflowError是Exception的子类错。insufficient memory这个提示在IDE里碰到一般不是真的物理内存不足而是启动参数里-Xmx设置太小或者项目里有内存泄漏导致堆空间被占满。排查思路就是先看Heap Usage再dump堆快照分析大对象别一上来就盲目加内存。4.2 类加载过程与双亲委派机制类加载题的核心是双亲委派。考点集中在加载流程、为什么这么设计、以及Class.forName和ClassLoader.loadClass的区别。类加载的五个阶段加载、验证、准备、解析、初始化。其中准备阶段会为静态变量分配内存并设置零值不会执行赋值语句初始化阶段才执行静态代码块和静态变量赋值。所以这道判断题你知道答案吗在准备阶段public static int a 10;中的a会被赋值为10。 答案错。准备阶段a是0初始化阶段才变成10。双亲委派的顺序是一个类加载器收到类加载请求先不自己加载而是把请求委托给父加载器父加载器也继续往上委派直到启动类加载器如果父加载器加载不了子加载器才尝试自己加载。好处是保证核心类库的安全避免核心API被篡改。做题时注意一个细节Class.forName会触发类的初始化执行static块而ClassLoader.loadClass默认只做加载和连接不会初始化。很多框架题会拿这一点做文章。4.3 GC与对象存活判断引用计数法为什么被抛弃GC判断题集中在两个方向判断对象是否存活的方法、GC算法的适用场景。对象存活判断引用计数法每个对象维护一个计数器引用1失效-1实现简单但解决不了循环引用问题所以主流JVM用可达性分析。从GC Roots出发沿着引用链走不到的对象就是可回收的。笔试常问哪些对象能当GC Roots包括虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、本地方法栈中native方法引用的对象、被synchronized持有的对象。GC算法方面标记-清除有碎片问题标记-复制没有碎片但浪费一半空间适合年轻代标记-整理没有碎片但移动对象成本高适合老年代。题里如果问为什么新生代要分Eden和Survivor区答案是为了配合复制算法减少内存浪费。默认Eden:Survivor比例是8:1:1但如果选项写比例可以调整那是对的——用-XX:SurvivorRatio参数就能改。5. 并发编程客观题synchronized、volatile与线程池的判定并发部分是最能区分背过八股和真正理解的模块。因为选项里经常出现一定保证所有这种绝对化表述只要对底层机制理解不到位很容易被带偏。5.1 线程创建方式与状态流转创建线程的几种方式笔试里一般是多选题继承Thread、实现Runnable、实现Callable、使用线程池。很多人会纠结实现Runnable和Callable算不算一种严格来说算两种因为Callable有返回值、能抛异常。另外启动线程的唯一方式是调用start()直接调用run()只是普通方法调用不会创建新线程。线程状态是另一个高频题。Java线程状态有六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。注意没有单独的READY状态新建线程启动后处于RUNNABLE它包含了操作系统层面的就绪和运行两种状态。判断题里如果写线程调用yield()后进入BLOCKED状态那是错的yield之后还是RUNNABLE。sleep和wait的区别也常考sleep是Thread的静态方法不释放锁wait是Object的方法释放锁。sleep到时间自动醒来wait需要别人notify/notifyAll。5.2 synchronized与锁升级判断题的常客synchronized在JDK 1.6之后引入了锁升级机制无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这个过程笔试未必考得很深但选项会拿synchronized是重量级锁做文章——在1.6之后这句话不再准确它有一个逐渐升级的过程。还有一个经典考点synchronized锁的是什么。修饰实例方法锁当前实例this修饰静态方法锁当前类的Class对象修饰代码块锁括号里指定的对象如果是静态方法和实例方法同时用synchronized是否互斥——不互斥因为它们锁的对象不同。这个题经常出现很多人下意识以为synchronized都是一样的就掉坑里了。和ReentrantLock的对比笔试会以表格形式出对比项synchronizedReentrantLock锁释放自动释放必须手动unlock可中断不支持支持公平锁非公平支持公平/非公平条件变量只能wait/notify支持多个Condition5.3 volatile可见性≠原子性volatile是并发选择题里最经典的文字陷阱道具。它保证两件事对volatile变量的写操作对其他线程立即可见禁止指令重排。但它不保证原子性i这种读-改-写操作依然不是线程安全的。典型题多个线程同时对volatile int i执行i操作最终结果可能是 A. 一定等于并发次数 B. 可能小于并发次数 C. 一定小于并发次数 D. 一定大于并发次数答案是B。因为即使i是volatile的i也不是原子操作多个线程可能同时读到相同旧值导致丢更新。如果题目问用volatile修饰能否保证线程安全答案一般是否定的除非你的操作本身是原子的或者不依赖当前值。5.4 线程池参数与拒绝策略客观题里的计算题线程池题有两种考法一种直接问你ThreadPoolExecutor有几个参数另一种给你参数让你推任务执行路径。线程池核心参数一共七个corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime非核心线程空闲存活时间、unit时间单位、workQueue工作队列、threadFactory线程工厂、handler拒绝策略。任务提交后的流程是当前线程数 corePoolSize直接新建核心线程执行。当前线程数 corePoolSize任务进入阻塞队列。队列满了且当前线程数 maximumPoolSize创建非核心线程执行。队列满了且线程数已达到maximumPoolSize执行拒绝策略。拒绝策略有四种AbortPolicy抛异常默认、CallerRunsPolicy调用者线程执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最老任务。这种题一旦算错整个选项串就全错。我的建议是做题时先在草稿纸上画一个流程分支先把队列、核心线程、最大线程三个数标出来再逐个任务往后推。6. 新特性与编码规范题枚举、Lambda与try-with-resourcesJava 8之后的新特性在校招笔试题里逐年增加但考得都不算深属于知道就得分、不知道就靠蒙的类型。6.1 枚举的本质它不是普通的类枚举在Java里是一个完整的类继承自java.lang.Enum。这意味着枚举可以定义字段、方法、构造方法但构造方法必须是私有的。枚举可以实现接口但不能显式继承其他类因为已经继承了Enum。枚举天然是单例的Enum.valueOf和.values()都是编译器帮我们生成的方法。每个枚举常量都是在类加载时初始化的静态常量所以天然线程安全。Effective Java里推荐用枚举实现单例笔试也爱考这个点。另外枚举和switch配合使用是常客选项可能是枚举可以用于switch这个说法是正确的。6.2 Lambda与函数式接口Lambda表达式的核心是函数式接口——只有一个抽象方法的接口。Runnable、Callable、Comparator、Function、Consumer、Predicate、Supplier都是。客观题常考的点加了FunctionalInterface注解的接口必须只有一个抽象方法但这注解不是强制的只要满足条件即使不标注也能用Lambda实现。Lambda捕获外部变量时该变量必须是effectively final即初始化后不再被修改。可以在声明时不写final但赋值后就不能再改。方法引用ClassName::method是Lambda的简便写法不改变语义。如果题干里写Lambda表达式可以访问局部变量并修改它错。这是很多人的易错点。6.3 try-with-resources的关闭顺序与抑制异常这类题迷惑性极强因为退出代码块时资源关闭的顺序和异常的处理方式都有明确规则。try-with-resources要求资源实现AutoCloseable接口多个资源会按声明顺序的逆序关闭。如果try块和资源关闭都抛出异常会把关闭时的异常作为被抑制异常附加到主体异常上而不是覆盖主体异常。还有一个经典坑try块中有returnfinally块中也有return最终返回值以finally为准。但finally里的return会导致异常被吞掉笔试选项里如果出现finally中不建议写return是正确的。7. 工程配置与踩坑题源发行版警告、Lombok失效、环境变量这一块在纯粹的选择题里出现频率不是最高但在面试、笔试题里的纠错题或者排错思路题里经常遇到。尤其是现在大家用IDEA配Maven几乎每个人都撞过这几类问题。题目不会直接让你修环境但会把错误提示放进选项问你该怎么做。7.1 源发行版17需要目标发行版17到底在说什么这个警告的原文是类似java: 警告: 源发行版 17 需要目标发行版 17核心原因是Maven编译插件的source和target版本不一致或者代码里用了高版本语法但编译目标设置为了低版本。source指定源码语法级别target指定生成的字节码版本。如果source17target就必须是17或更高否则编译器会报错。解决方案是统一pom.xml里的属性properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties或者直接配置maven-compiler-plugin的source和target。另外在IDEA里还需要确认Project Structure里的Project SDK、Project language level、Modules的language level是同一个版本。很多人改了pom.xml还报错就是IDEA自己的language level没同步。7.2 Lombok报compiler supported by lombok警告Lombok的报错信息非常典型You arent using a compiler supported by lombok, so lombok will not work.这个问题我帮人排查过很多次几乎都是同一个原因Lombok使用的注解处理器和你当前的JDK版本不兼容。Lombok是通过注解处理器在编译期修改AST来生成代码的JDK升级后旧的Lombok版本不认识新的编译器内部API就会罢工。解决办法是升级Lombok依赖到与JDK匹配的版本。比如JDK 17对应Lombok 1.18.30以上。还有一个容易漏的点如果用了Maven需要确认maven-compiler-plugin是否配置了annotationProcessorPaths因为一旦显式指定了annotationProcessorPathsLombok必须也出现在这个列表里否则不生效。7.3 JAVA_HOME、PATH、CLASSPATH的配置逻辑环境变量配置题属于面试官随口一问不少人当场懵的类型。三者的分工JAVA_HOMEJDK的安装目录是其他工具定位JDK的入口。PATH系统查找可执行命令的路径java、javac在$JAVA_HOME/bin里只有把这里加进PATH命令行才能直接敲java -version。CLASSPATHJVM查找class文件的路径配错会导致找不到或无法加载主类。实际工作中我建议只配置JAVA_HOME和PATH尽量少用全局CLASSPATH因为现代Java开发用Maven/Gradle管理依赖全局CLASSPATH反而容易引发类冲突。这道题如果笔试考选项里写配置CLASSPATH为JDK的bin目录就是典型的错误表述。8. 2019这套题给秋招复习带来的启示前几年我还能比较频繁地看到1920年的笔试题直接出现在新题库里说明这些经典考点的生命力确实长。与其漫无目的地刷几百道题不如把这套客观题当成一张知识体检表让它帮你定位盲区然后按优先级补。8.1 复习路线的优先级基础阶段绝不是quick pass我给准备校招的同学建议的顺序是Java语法与面向对象 → 集合框架 → JVM内存与GC → 并发编程 → 新特性与工程工具 → 框架与项目。这套顺丰科技的客观题前四个部分占了大概七成以上的分数。也就是说哪怕你Spring Boot项目写得很熟如果集合、JVM、并发这些基础题拿不到分笔试照样过不了。这里要提醒一句很多人觉得基础简单跳过不复习直接刷框架八股。结果笔试一到HashMap的树化条件、volatile的语义这类题就卡住了。基础部分恰恰是最容易平时以为会一考就错的地方老老实实过一遍性价比最高。8.2 刷题的正确姿势错题必须归类刷客观题最忌讳的就是只记正确答案不看错误选项。每道错题至少要做三件事弄清楚正确选项为什么对把每个错误选项单独拎出来写出它错在哪个词上把这道题关联的知识点写进自己的笔记。比如HashMap转红黑树的条件是链表长度8且数组容量64这背后就关联了负载因子、扩容时机、为什么要有MIN_TREEIFY_CAPACITY。我还会把同类题合并整理。比如所有关于线程安全的题放一起你会看到HashMap、ArrayList、SimpleDateFormat、StringBuilder这些类为什么不安全各自不安全在哪个场景。这样复习一轮之后再遇到类似的题目基本不会再错。8.3 客观题暴露盲区然后去补机制而不是背题目笔试只是校招第一关后面还有面试。客观题里出现的知识点面试官大概率会追问为什么底层怎么实现你项目里怎么用的。所以刷题时就不要只满足于选对而是要把每个考点当成一个话题自己试着讲清楚。举个例子你刷到OutOfMemoryError的题不能止步于堆溢出选Java heap space你可以顺着往下想什么情况堆会溢出怎么排查Mat或者JProfiler怎么用Dump文件怎么看这些思考在面试里非常加分。最后再分享几个当年自己踩过的坑第一审题要慢。客观题时间紧张但不意味着每一秒都在赶。我吃过亏的是把以下说法错误的是看成以下说法正确的是一整套选项全反了。建议拿到卷子先扫一眼题干里的否定词标注出来再做题。第二不要在一个选项上纠结太久。笔试的题量和时间通常不允许你反复推敲遇到拿不准的先选一个最可能的标记一下等后面做完再回头。很多时候做到后面的题会突然想起来前面题目的知识点。第三多选题宁缺毋滥。如果题目明确是多选不确定的选项宁可不选。我见过的题目里模棱两可的选项经常是出题人故意设置的干扰项少选拿一半分多选直接零分。第四环境配置和编译警告这类题别觉得超纲。顺丰这套题以及很多大厂的笔试题里都会混入一些真实开发中必然会遇到的问题比如源发行版不一致、编译插件失效。平时自己多折腾环境、多读报错日志这些题基本送分。这套2019年的客观题放在今天依然是很好的练手材料。扎实的基础、准确的记忆、稳定的心态这是我刷完同类题目之后最深的体会也是后来帮别人复盘时反复强调的三件事。希望这份拆解能帮你在下一场笔试里少一点慌乱多一点把握。