尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

欢聚时代校招Java笔试题解析:基础考点与实战经验

欢聚时代校招Java笔试题解析:基础考点与实战经验 最近好几个朋友问我说当年欢聚时代就是 YY 母公司的校招笔试题值不值得刷尤其是 JAVA 基础类 B 卷这类老题。我的回答是值得而且非常值得。2017 年的题目放到今天来看依然是国内互联网公司校招 Java 岗位笔试的“标准样板”——不考偏门框架不考冷门 API核心就是 Java 基础、集合、并发、JVM、异常处理这些硬通货。这篇文章我就以这套 B 卷为主线把里面涉及到的知识点、考点背后的逻辑以及我当时复习和做题的一些经验完整拆开来讲。如果你是正在准备校招的应届生或者刚转行学 Java 想检验一下自己的基础这篇文章能帮你省下不少收集资料的时间。我会按笔试题常见的几大模块来梳理每个模块都会说清楚“考什么、为什么考、怎么答才稳”还会附上一些我实际总结出来的踩坑记录。1. 先搞懂出题人想考什么笔试背后的能力模型1.1 为什么校招笔试偏爱 Java 基础很多同学有一个误区觉得笔试嘛肯定是越偏越难越好最好考几个没人见过的注解、冷门框架源码这样才能筛出“大神”。但现实恰恰相反像欢聚时代这类以业务产品为主的公司校招笔试的核心目的是“筛掉基础不牢的人”而不是“筛掉不会进阶的人”。原因很简单。校招进来的应届生绝大部分要经过少则一个月、多则三个月的培养期才能上手业务。在这段时间里公司最怕的不是你不会用 Spring Boot而是你连重载和重写都分不清、连 HashMap 的原理都说不明白。基础扎实的人框架随便学学就能上手基础不牢的人即使背了一堆框架用法遇到线上问题照样抓瞎。所以 2017 年 B 卷这套题考点全部集中在“语言本身”这恰恰是最难突击、最能反映真实水平的部分。1.2 B 卷整体考察模块分布虽然我不能逐字复述当年的原题但从考完的同学反馈和我刷过的同类型卷子来看B 卷的知识点分布大致是这样的面向对象特性封装、继承、多态、抽象类与接口的区别约 20%集合框架ArrayList、LinkedList、HashMap、HashSet 的底层原理与区别约 20%异常处理受检异常与非受检异常、try-catch-finally 执行顺序约 15%多线程与并发线程创建方式、synchronized、volatile、死锁约 15%JVM 基础内存区域划分、垃圾回收、类加载约 10%其他基础String、包装类、equals 与 hashCode、运算符优先级约 20%这个分布非常有代表性。你会发现它没有刻意去考最新的 Java 8 Lambda、Stream 之类的特性而是在考“不管 Java 版本怎么变你都必须懂”的东西。所以如果你现在才开始准备校招先把这张分布表里的内容吃透远比追着新特性跑更重要。2. 面向对象考点抽象类与接口、重载与重写的底层逻辑2.1 抽象类和接口不是背区别就完事笔试里关于抽象类和接口的题目出题方式非常直接通常就是给你一段代码问你“能不能编译通过”或者“输出什么”。这类题目表面考语法实际考的是你对设计意图的理解。先说结论抽象类是用来“抽取共性”的接口是用来“定义能力”的。一个类只能继承一个抽象类但可以实现多个接口。Java 8 之后接口里可以有 default 方法和 static 方法Java 9 之后接口里还可以有 private 方法这让接口和抽象类的边界变得模糊了一些但核心语义没有变。我记得当年刷题时遇到过一个很经典的变形题abstract class Animal { abstract void eat(); } interface Flyable { void fly(); } class Bird extends Animal implements Flyable { Override void eat() { System.out.println(Bird eat); } Override public void fly() { System.out.println(Bird fly); } }这里有一个很容易忽略的细节Flyable 接口里的 fly() 方法默认是 public abstract所以实现类里必须用 public 修饰。很多同学在实现接口方法时忘了加 public直接编译报错。这就是笔试喜欢挖的坑。2.2 重载与重写的区分除了方法签名还要看返回类型和异常重载Overload和重写Override是 Java 基础中出镜率最高的考点之一。重载发生在同一个类中方法名相同、参数列表不同重写发生在父子类之间方法签名必须完全相同。这里有个值得展开的知识点重写时子类方法的访问权限不能比父类更严格子类方法抛出的受检异常不能比父类更宽泛但返回类型可以是父类返回类型的子类型协变返回类型。举个例子class Father { protected Number getValue() throws IOException { return 1; } } class Son extends Father { Override public Integer getValue() throws FileNotFoundException { return 2; } }这个重写是合法的。如果把子类的访问权限改成 private或者把异常改成 Exception就会编译报错。笔试里经常给你四个选项让你判断哪个是正确的重写考的就是这几个细节。2.3 多态的动态绑定与静态绑定多态相关的题目通常会给一段继承关系的代码然后问输出结果。核心在于理解 Java 的方法调用机制静态方法、私有方法、final 方法是静态绑定编译期就确定了实例方法是动态绑定运行时才根据实际对象类型决定调用哪个方法。我用一个很典型的例子说明class A { void show() { System.out.println(A); } } class B extends A { void show() { System.out.println(B); } } public class Test { public static void main(String[] args) { A a new B(); a.show(); } }输出当然是 B因为变量 a 的静态类型是 A但实际对象类型是 Bshow() 是实例方法运行时动态绑定到 B 类的方法上。这个知识点本身不难但笔试喜欢把它和“字段没有多态性”放在一起考。字段访问是静态绑定的比如class A { String name A; } class B extends A { String name B; } public class Test { public static void main(String[] args) { A a new B(); System.out.println(a.name); } }这段代码输出 A因为访问字段时看的是静态类型。这是很多初学者容易踩的坑。3. 集合框架HashMap 原理、ArrayList 与 LinkedList 的取舍3.1 HashMap 的底层原理是必考题集合框架是 Java 笔试题中的“题王”尤其是 HashMap。2017 年的题目里HashMap 相关内容基本是必考的而且考得相当深入。这背后的逻辑很简单HashMap 是日常开发中使用频率最高的集合类如果一个候选人连 HashMap 的 put 流程都说不清楚很难让人相信他能写好业务代码。我建议复习时至少掌握这几个关键点HashMap 底层是数组加链表JDK 8 之后当链表长度超过 8 且数组长度大于等于 64 时链表会转成红黑树。put 流程先对 key 的 hashCode 做 hash 扰动然后通过 (n - 1) hash 计算桶下标如果桶为空就直接插入否则遍历链表或红黑树找到相同 key 就替换值找不到就新增节点。默认容量 16负载因子 0.75扩容时会重新计算每个元素的位置rehash。key 和 value 都可以为 null但 key 为 null 时 hash 值为 0放在数组下标为 0 的桶里。当年笔试里有一道很经典的题是HashMap 在并发环境下 put 会不会丢数据其实 JDK 7 里并发 put 可能导致环形链表JDK 8 里虽然解决了这个问题但在扩容时依然可能出现数据覆盖。所以答案是“会”这也是为什么并发场景要用 ConcurrentHashMap。3.2 ArrayList 与 LinkedList 该选谁这个考点在笔试题里通常不会直接问你“区别是什么”而是给你一段代码比如“频繁在列表头部插入元素用哪个实现类性能更好”或者“给你一个 LinkedList问你 get(index) 的时间复杂度是多少”。先说结论ArrayList 基于动态数组随机访问是 O(1)但在头部插入或删除元素是 O(n)因为需要移动后面的元素。LinkedList 基于双向链表头部插入和删除是 O(1)但随机访问是 O(n)因为需要从头或尾遍历。值得注意的一个细节是LinkedList 虽然头部插入是 O(1)但实际运行时的常数项很大因为每个节点都需要额外的前后指针。在笔试场景下如果题目问“在 Java 中频繁进行插入、删除操作应该使用哪个集合”标准答案就是 LinkedList但如果在实际开发中遇到类似需求我建议先用 ArrayList 做压测数据量不大的情况下 ArrayList 往往比 LinkedList 更快。3.3 HashSet 与 HashMap 的关系HashSet 的底层实现就是 HashMap只是把所有 value 都指向了一个固定的 Object 对象。这个知识点在笔试题里经常伪装成“HashSet 如何保证元素不重复”来考。答案就是HashSet 依赖元素的 hashCode() 和 equals() 方法先计算 hashCode 定位到桶再用 equals 判断是否相等如果相等就认为是重复元素不再插入。这个考点最常和“重写 equals 必须重写 hashCode”一起出现。笔试的经典问法是“两个对象 equals 相等但 hashCode 不同会发生什么”答案是这两个对象会被放到不同的桶里HashSet 会认为它们是两个不同的元素导致 Set 里出现“重复”元素违反了 Set 的语义。这个坑在开发中也很容易踩到尤其是用 Lombok 的 Data 注解时如果不小心把 hashCode 排除了一些关键字段就会出现这种诡异问题。4. 异常处理try-catch-finally 的执行顺序与返回值陷阱4.1 受检异常与非受检异常的区别异常处理在笔试题里的权重不算最高但基本每年都会出现。最常见的考法就是给你一段 try-catch-finally 代码问输出什么或者问 finally 块中的 return 对返回值有什么影响。先区分两个核心概念。受检异常Checked Exception是编译器强制要求处理的异常比如 IOException、SQLException不处理就编译不通过。非受检异常RuntimeException包括 NullPointerException、ArrayIndexOutOfBoundsException 等编译器不强制处理。这个知识点在实际笔试中会以“下列哪些异常属于 RuntimeException”的形式出现。选项通常有ClassNotFoundException属于受检异常、IOException受检、NullPointerException非受检、NoSuchFieldException受检。记住一个规律凡是名字里带 Exception 且不是 RuntimeException 子类的基本都是受检异常。4.2 finally 块中的 return 陷阱这个陷阱几乎是每套笔试的必考题而且错误率极高。public static int test() { int num 10; try { return num; } finally { num 20; } }问这段代码的返回值是多少。答案是 10不是 20。原因是try 块中的 return num 会先把 num 的当前值10保存到返回值寄存器然后执行 finally 块但 finally 块中修改的 num 只是局部变量不会改变已经保存的返回值。再看另一种写法public static int test() { int num 10; try { return num; } finally { return 20; } }这段代码返回值是 20。因为 finally 块中的 return 会直接覆盖 try 块中的返回值也就是说如果 finally 块里有 return 语句会吞掉 try 块的返回值。这在开发中是非常不推荐的做法不仅会让人觉得代码难以理解还会掩盖异常。我遇到过一个面试官追问如果 try 块里抛出了异常finally 块里又 return 了异常还会被抛出吗答案是不会因为 finally 中的 return 会让异常被吞掉。这是一个非常隐蔽的 bug 来源笔试里如果是问答题建议直接指出“不要在工作中这样写”。4.3 异常链与日志记录的最佳实践除了语法层面的考点笔试里偶尔会出现一道关于异常处理的“价值观”题比如问“生产环境抛异常时应该怎么做”。这种题没有标准答案但考察的是你有没有线上经验。我的建议是尽量使用 try-with-resources 处理资源关闭Java 7 之后的写法既简洁又安全捕获异常时不要 catch 了 Exception 就完事要精确捕获记录日志时不要只打印 e.getMessage()要记录完整堆栈 e.printStackTrace() 或者 log.error(xxx, e)。如果你能在笔试题里写出这些细节面试官对你的印象会明显加分。5. 多线程与并发从线程创建到死锁分析5.1 线程创建的三种方式与本质多线程是 Java 笔试中的重头戏也是很多应届生的薄弱点。B 卷里关于线程创建的题目非常经典说说创建线程有哪几种方式有什么区别。标准答案是三种继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 FutureTask。但更准确地说本质上只有一种就是创建 Thread 对象并调用 start() 方法其他方式都是为 Thread 提供任务逻辑的途径。笔试中经常追问Runnable 和 Callable 的区别。答案是Callable 的 call() 方法有返回值可以抛出受检异常Runnable 的 run() 方法没有返回值也不能抛出受检异常。如果需要拿到线程执行的结果用 Callable 加 FutureTask或者直接用线程池的 submit() 方法返回 Future。5.2 synchronized 与 volatile 的误区synchronized 是校招笔试里绝对绕不开的考点。你需要掌握的不只是“它是重量级锁、后来引入了偏向锁和轻量级锁”这种概念更要理解它的语义保证原子性、可见性和有序性。volatile 则只能保证可见性和有序性不能保证原子性。这是笔试里最容易混淆的考点。经常出现的问题是“volatile 能不能保证 i 的线程安全”答案是不能因为 i 不是原子操作它包含读取、加一、写回三个步骤volatile 只保证了读写的可见性但多个线程同时执行 i 时依然可能丢失更新。我自己的做题经验是遇到这类题先画一个简单的执行时序图把两个线程对同一个变量的操作列出来基本就不会答错。比如两个线程同时执行 i初始值 i0最终结果可能是 1 而不是 2这就是丢失更新。5.3 死锁的四个必要条件与排查思路死锁相关的题在 B 卷中通常是以代码分析题出现给你两把锁和几个线程问你“会不会死锁”。判断的核心就是死锁的四个必要条件互斥条件资源一次只能被一个线程占用持有并等待线程持有资源的同时在等待其他资源不可剥夺资源只能由持有者主动释放循环等待多个线程形成等待环路笔试中如果只有两个线程、两把锁判断方式很简单看两条加锁路径是否顺序相反。比如线程 A 持 lock1 等 lock2线程 B 持 lock2 等 lock1那就必然死锁。如果遇到实际开发中的死锁排查我的经验是用 jps 找到 Java 进程 ID然后执行 jstack 查看线程转储搜索 “Found one Java-level deadlock” 关键字就能看到具体的锁依赖链。这个技能当年笔试不会考但面试环节如果被追问“你有没有排查过死锁”能说出来就是加分项。6. JVM 基础内存区域、垃圾回收与类加载6.1 运行时数据区与内存溢出JVM 考点在 2017 年校招笔试中占比不算最高但一旦出现就是拉分题。最常考的是 JVM 运行时数据区的划分以及每个区域发生内存溢出的场景。Java 8 及之后的内存区域划分已经很稳定了堆、虚拟机栈、本地方法栈、程序计数器、元空间取代了 Java 7 的永久代。笔试喜欢考的是几乎所有的对象实例都在堆上分配堆溢出报 OutOfMemoryError: Java heap space虚拟机栈深度不够时抛 StackOverflowError元空间不足时抛 OutOfMemoryError: Metaspace相关热搜词里有 “java: outofmemoryerror: insufficient memory”这其实更像 JVM 启动参数问题可能是 -Xmx 设置过小导致。笔试如果遇到这种题通常是想考你 JVM 参数调优的基础比如 -Xms 初始堆大小、-Xmx 最大堆大小、-Xss 栈大小以及 -XX:HeapDumpOnOutOfMemoryError 用来在内存溢出时导出堆快照。6.2 垃圾回收算法与分代收集垃圾回收相关的考点非常固定引用计数法、可达性分析、复制算法、标记-清除、标记-整理。笔试一般不会让你手写 GC 算法而是问“为什么主流的垃圾回收器都用可达性分析而不是引用计数”或者说“年轻代为什么用复制算法”。回答前一个问题时要指出引用计数法无法解决循环引用的问题两个对象互相引用但没有任何 GC Roots 指向它们时引用计数依然不为 0导致内存无法回收。可达性分析则通过从 GC Roots 出发向下搜索解决了这个问题。回答后一个问题时要结合分代收集理论。年轻代的对象存活率低复制算法每次只需要复制少量存活对象效率高老年代对象存活率高复制算法会有大量复制开销所以用标记-清除或标记-整理。6.3 类加载过程与双亲委派模型类加载机制的考题通常不会太难但非常容易被忽视。三个关键知识点加载、验证、准备、解析、初始化这五个阶段双亲委派模型的工作流程为什么要用双亲委派。双亲委派模型的核心思路是当一个类加载器收到类加载请求时先把请求委派给父加载器去加载只有父加载器无法完成加载时子加载器才会自己尝试加载。这样做的好处是保证 Java 核心库的类不会被随意替换比如你自己写一个 java.lang.String也不会被加载因为引导类加载器已经优先加载了 JDK 自带的 String。笔试中如果遇到问答题问“能不能自己写一个 java.lang.String 类”答案是可以写编译也能通过但是不会被加载会收到 SecurityException。这个题目能直接区分出你是背了概念还是真正理解了类加载机制。7. 其他高频基础考点String、包装类、equals 与 hashCode7.1 String 不可变性相关的经典题String 是 Java 基础中的基础但考起来千变万化。B 卷里最常见的题是String、StringBuilder、StringBuffer 的区别以及字符串拼接的性能比较。String 是不可变类每次拼接都会产生新的 String 对象StringBuilder 是可变的适合单线程下的字符串拼接StringBuffer 是线程安全的方法都加了 synchronized但性能比 StringBuilder 差。笔试里如果给你这样一段代码问创建了几个对象String s new String(abc);这道题的标准答案是两个对象。一个是abc这个字符串字面量在类加载时创建并放入常量池另一个是 new String 在堆上创建的新对象。如果常量池中已经有abc那就只创建一个对象。7.2 包装类的缓存机制与比较陷阱包装类相关的题目在笔试题中的出现率非常高核心考点是 Integer 缓存机制。Integer 默认缓存 -128 到 127 之间的值所以在缓存范围内的两个 Integer 用 比较是相等的超出范围就不相等。Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 200; Integer d 200; System.out.println(c d); // false这里 c d 是 false因为 200 不在缓存范围内每次自动装箱都会 new 一个新的 Integer 对象用 比较的是对象引用。笔试的坑在于很多人记住了缓存范围但忽略了 Integer 的缓存范围是可以通过 JVM 参数 -XX:AutoBoxCacheMax 调整的。面试时如果能主动说出这一点会显得你阅读过源码细节。7.3 equals 与 hashCode 的约定equals 和 hashCode 的约定是 Java 基础里最重要的契约之一。两个对象 equals 相等则 hashCode 必须相等两个对象 hashCode 相等equals 不一定相等。这个契约在 HashMap、HashSet 等散列集合中直接决定了行为的正确性。笔试里经常考重写 equals 但不重写 hashCode会有什么问题。答案前面已经提过HashSet 会把 equals 相等的对象当成不同元素导致重复添加。另外如果你用对象作为 HashMap 的 key重写了 equals 但没有重写 hashCode还会导致无法正确通过 key 从 Map 中取出 value因为 hashCode 变了定位的桶都变了。我在复习时会自己写一个 Person 类分别测试“只重写 equals”“只重写 hashCode”“两个都重写”三种情况下的 HashSet 行为这个实验做完基本就理解了。强烈建议你也动手敲一遍比单纯背结论牢固得多。8. 笔试实战策略考场上的时间分配与答题顺序8.1 拿到试卷先做“会做的”校招笔试的题量通常不小JAVA 基础类 B 卷一般是选择题加少量简答题总共 60 到 90 分钟。我的策略是拿到试卷不按顺序做先把所有题目快速浏览一遍标记出“一眼就知道答案”的题优先做完。这些送分题包括运算符优先级、基本数据类型取值范围、String 拼接结果、重载与重写的判断。把送分题稳稳拿到手心里就有底了后面遇到复杂题也不会慌。一套笔试题拉开差距的不是最难的题而是简单题的准确率。很多人栽在简单题上不是因为不会而是因为粗心比如没看清题干问的是“不正确的一项”还是“正确的一项”或者没注意到代码里有个细节是静态方法而非实例方法。这种丢分是最可惜的。8.2 代码题先写正确再谈优化B 卷里可能会有一两道手写代码的题常见的是手写单例模式、手写冒泡排序、手写快速排序或者手写一个线程安全的计数器。从相关热词里也能看到“冒泡排序 java”“快速排序 java实现”是高频搜索词说明这是很多人实际遇到的考题。手写代码时我有一条最重要的经验先把功能写对再考虑性能。比如手写快速排序如果你对双指针的边界处理不熟宁可使用更容易写对的递归版本也不要背一个自己都讲不清楚的优化版本。笔试判分是人工的代码能跑通、逻辑清晰比背一个花哨但解释不清的写法得分高。还有一个细节写完代码后自己用简单数据在脑子里跑一遍比如排序算法就用 [5, 3, 8, 1] 走一遍流程检查边界条件有没有越界。这个方法能帮你发现大约一半的笔误。8.3 简答题结构化回答别只写关键词B 卷中如果有简答题比如“说说 HashMap 的底层原理”千万不要只写一两句话。判卷老师看的是你思维的完整性。我建议按这个结构答先说整体结构数组加链表JDK 8 之后引入红黑树再说 put 流程计算 hash、定位桶、插入节点、扩容再说 get 流程计算 hash、定位桶、遍历链表或红黑树查找最后说线程安全不是线程安全的并发场景要用 ConcurrentHashMap这样回答哪怕语言朴实也能体现你已经建立起完整的知识链路。如果只写“底层是数组加链表”虽然没错但很难和其他候选人拉开差距。9. 刷题之后的进阶从笔试到面试的自我提升清单9.1 建立自己的知识点自测清单刷完一套题后我建议你整理一份知识点自测清单按下面这个模板来拿到一个知识点能不能不看资料用自己的话讲清楚能不能举出一个该知识点在实际项目中的应用场景知不知道这个知识点常见的坑有哪些比如你学了 synchronized 和 volatile 的区别不能只背“synchronized 保证原子性和可见性volatile 只保证可见性”还要能说出一个实际场景比如双重检查锁单例里为什么要用 volatile就是为了防止指令重排导致拿到未初始化完成的对象。这种“知识点 实际场景”的复习方式效果远好于一遍遍刷题。9.2 从高频搜索词反推复习重点翻看最近的 Java 热词有一个很有意思的现象“java面试八股文”“java八股文面试题”这些关键词热度一直很高说明大家还是在用死记硬背的方式来准备面试。但真正有效的准备方式是把八股文里的每个知识点都变成自己写过、调试过的东西。我当年为了准备笔试把 HashMap 的 put 流程自己实现了一遍虽然实现粗糙但做完之后再看八股文里的总结理解完全不一样。类似地为了搞懂类加载机制我写了一个自定义类加载器实现了加载指定路径下的 class 文件这个过程让我彻底理解了双亲委派模型。这也是为什么我不建议大家只看别人的总结一定要动手写代码。9.3 笔试不是终点而是面试的预演最后说一个很多人忽略的认知校招笔试内容基本上是面试提问范围的下限不是上限。你笔试中遇到的每一个考点都可能成为一面、二面中追问的起点。比如笔试考了 HashMap 底层原理面试官可能就会追问“红黑树和链表的分界点为什么是 8”“负载因子为什么默认是 0.75”。所以刷题之后一定要对每个考点做深度延展。这里我给大家一个非常实用的建议每做完一套笔试题把错题对应的知识点整理成一个 Word 文档每个知识点下面写三个层次的内容——第一层是基础结论第二层是底层原理第三层是实际应用场景。这样到面试前复习的时候你只需要看这个文档不需要再翻几百页书。我自己当年就是这样准备欢聚时代笔试的。虽然最后没有去这家公司但准备过程中沉淀下来的知识体系在后面几场面试里帮了大忙。这也是为什么我始终觉得与其纠结一套旧题的答案不如借一套旧题逼自己把 Java 基础重新系统地过一遍。基础这东西什么时候补都不亏。
返回列表