公司动态

2015网易Java笔试题详解:HashMap、JVM与并发编程核心考点

📅 2026/8/29 5:33:06
2015网易Java笔试题详解:HashMap、JVM与并发编程核心考点
1. 2015年网易Java笔试题的考察风向基础、深挖、实战我最近在整理以前跳槽时留下的面试复习资料翻到一套2015年网易校招的Java工程师笔试题。说实话重新做了一遍之后最大的感触是当年这套题放到今天依然能打甚至比现在很多公司的八股文刷题更有嚼头。2015年移动互联网正处于高速扩张期网易作为老牌互联网公司校招笔试已经形成了相当成熟的出题体系。那年的Java笔试题型基本是选择题、填空题、简答题加两道编程题。选择题覆盖Java基础语法、集合框架、JVM内存、多线程简答题喜欢考设计模式和场景设计编程题则集中在排序算法和字符串处理。为什么说这套题值得反复看因为它考察的不是“面经背诵能力”而是真正理解语言底层的洞察力。比如集合类的题它不会直接问你HashMap和Hashtable的区别而是给一段多线程操作HashMap的代码让你判断会出什么问题、为什么出问题、怎么规避。这种出题方式在今天Spring Boot一把梭、微服务满天飞的环境下反而成了稀缺品。对正在准备校招或跳槽的Java工程师来说这份试卷的参考价值不在“押题”而在于它划出了Java工程师的底层能力边界语言基础、集合与泛型、JVM与内存模型、并发编程、算法手写能力。这五个方面至今依然是大厂面试的核心盘。我下面按当年的考点分类结合我自己做题时的思路和后来工作中的验证把这份卷子值得好好琢磨的题目类型拆开讲一遍。2. 基础语法题里的隐藏坑自增、字符串与数组越界2.1 自增运算和表达式求值一道题筛掉一半人这套卷子的选择题第一梯队里必然有这类题给你一段int i 0; i i;然后问 i 的最终值是多少。很多人脱口而出“i自增了那肯定是1”。但正确答案是0。原因是i这个表达式的求值过程分三步先把 i 当前值0压入操作数栈然后 i 自增变成1最后把操作数栈里保存的旧值0赋值回 i。所以 i 经历了一次从0到1又回0的过程。这种题考的不是你会不会写代码而是你知不知道Java虚拟机底层是怎么处理自增表达式的。同理还有int i 5; int j i i;这种组合如果要算j的值必须自己手动模拟一遍操作数栈的压栈和赋值顺序稍不留神就算错。我当年的做法是在做这类题时强行在草稿纸上画一个简单的栈模型把每次自增前的压栈值和自增后的局部变量值分别记下来按顺序推进。这样做虽然慢但正确率能保证。后来在面试别人时我也喜欢拿这种题当试金石因为能答对的人至少说明他把JVM规范里关于字节码执行的那部分知识啃过一遍。2.2 String的不可变性与字符串常量池字符串题也是那套笔试卷的常客而且出题角度很多样。它会给你几行代码问创建了几个对象。String s1 new String(hello); String s2 hello; String s3 s2.intern();这里面涉及的考点有几个字符串常量池、new关键字创建对象的机制、intern方法的作用。hello这个字面量首次出现时会在常量池中创建一个对象new String(hello)会在堆上再创建一个对象引用指向堆中的新对象s2直接引用常量池里的对象s3调用intern后如果常量池已有hello直接返回常量池中的引用。所以三行代码执行完s1、s2、s3的引用地址各不相同但内容相同。这套题背后真正想考察的是你是否理解Java字符串的设计取舍。String被设计成不可变是为了实现常量池复用、保证hashCode稳定、避免多线程下数据被篡改。代价是字符串拼接会产生大量中间对象所以有了StringBuilder和StringBuffer。一个合格的Java工程师必须能把这些讲清楚而非仅仅背出“String不可变”这句话。我当时拿到这类题会额外追问自己一个问题StringBuilder和StringBuffer的区别到底在哪synchronized加在什么粒度上。答案是StringBuffer的方法都加了synchronized因此线程安全但性能稍差StringBuilder没有加锁单线程下性能更好。这个知识点后来在线上日志拼接、SQL动态组装等场景里直接影响了代码性能和并发安全确实是值得吃透的基础。2.3 数组越界异常的排查思路热词里出现了“java中数组越界异常”这让我想起卷子里有道程序阅读题给定一个排序算法实现其中有一处循环边界写成了for (int i 0; i arr.length; i)让你找出bug并修正。这道题的正确答案当然是把改成。但出题人的真正意图在于你能不能通过异常堆栈快速定位越界访问的位置。ArrayIndexOutOfBoundsException是运行时异常编译器不会报错只有跑到那个索引时才会炸。如果程序在循环里先访问再自增甚至会出现“时好时坏”的诡异表现因为最后越界的那次访问发生在数据已经处理完的边界上。实际工作中我排查这种问题有一个固定套路先看异常堆栈里是哪个文件哪一行然后看当前循环变量和数组长度再检查边界条件是否用了或length - 1用错。多数是二分查找的退出条件、分页遍历的起止位置、或者字符串split后数组长度比预期小导致的。这类题当年在笔试卷里出现其实就是提前告诉面试者基础不牢后续排查线上问题会寸步难行。3. 面向对象与集合框架不止背特性还要懂机制3.1 继承、多态与重载的题目套路2015年网易这套卷子里面向对象部分的题量不小。常见的考法有两种一种是给出一组父子类让你输出方法调用结果另一种是给出重载方法的参数类型列表让你判断某个调用的方法匹配顺序。多态调用那道题我印象很深基类有个public void print()方法子类 override 了它然后代码里用基类引用指向子类对象调用print方法。问输出的是子类的实现还是基类的实现。答案是子类因为Java方法调用遵循“编译看左边运行看右边”的原则。编译阶段检查基类是否定义了该方法运行阶段实际执行的是堆中对象真正所属类型的版本也就是动态绑定。重载的匹配顺序就更有意思了。基本数据类型会按byte - short - int - long - float - double的顺序进行自动类型提升如果定义了多个重载版本编译器会优先选择不需要转型就能匹配的那个实在没有才考虑自动提升再考虑装箱最后是可变参数。这套规则在笔试卷里常以“以下哪个方法会被调用”的形式出现坑点在于很多人分不清自动提升和装箱的优先级。我当时复习这部分时没有死记这些优先级而是画了一张“重载方法匹配优先级”的流程图精确匹配 自动类型提升 自动装箱/拆箱 可变参数。这张图直到今天面试别人时还在用。3.2 HashMap的实现原理与并发问题集合框架里的重点无疑是HashMap。2015年的笔试卷中针对HashMap的考察已经非常深入了。选择题会问HashMap的默认初始容量是多少、加载因子是多少、什么时候触发扩容。这些标准答案不难难的是后面的简答题为什么当HashMap的元素数量达到阈值时会重新哈希这种机制在大规模缓存场景下会有什么问题。HashMap的put过程大致是先对key的hashCode进行一次扰动运算然后根据数组长度计算桶下标如果桶位上是空直接放入如果非空则遍历该桶位上的链表或红黑树查找key若找到则覆盖没找到则插入。当链表长度超过8且数组长度达到64时链表会转换成红黑树把最坏情况下的查询复杂度从O(n)降到O(logn)。但这里有个笔试常考的隐患HashMap并不是线程安全的。在多线程环境下两个线程同时触发rehash可能出现链表的环形引用之后get操作会进入死循环。JDK 1.7的resize实现确实存在这个经典问题JDK 1.8改成尾插法后死循环问题得到缓解但数据覆盖丢失仍然存在。我当时在这道题旁边记了一句话并发场景优先用ConcurrentHashMap它的实现从JDK 1.7的Segment分段锁演进到JDK 1.8的CAS加synchronized锁桶粒度更细并发度更高。后来做诸如用Map做本地热点缓存、统计指标聚合等场景时这句话直接帮我避免了好几次生产事故。3.3 Comparable与Comparator排序里的“值传递”排序相关的内容在笔试中占了不小的比重毕竟排序算法本身就是常考点。有个容易被忽略却考过好几次的知识点是Comparable和Comparator的区别。Comparable是自然排序接口定义在实体类内部实现compareTo方法后对象自身就具备比较能力Comparator是外部比较器可以声明一个独立的比较逻辑适合在不修改原有类代码的情况下临时改变排序规则。如果既实现了Comparable又传入了Comparator后者优先级更高。我记得这套卷子里有一道场景题有一个学生类要求先按成绩降序成绩相同的按学号升序。用Comparator实现时需要这样写ComparatorStudent comparator (s1, s2) - { if (s1.getScore() ! s2.getScore()) { return s2.getScore() - s1.getScore(); } return s1.getId() - s2.getId(); };这道题里有三个考点基本类型不建议用减法的潜在溢出问题、降序升序的返回值约定、Lambda表达式的简洁写法。热词列表里的java comparator.comparing 将某元素值放第一个也属于这个范畴很多人会在排序时希望把某个特殊值排到最前面用comparing加自定义Comparator就能实现。这类写法在真实的报表导出、列表排序需求里很常见远比单纯背API有价值。4. JVM与内存当年就是深水区今天仍是分水岭4.1 OutOfMemoryError的经典分类与排查路径热词里有个非常扎眼的java: outofmemoryerror: insufficient memory这道题在2015年的网易卷里是以简答形式出现的请列举你遇到过的OutOfMemoryError类型并说明各自的产生场景和排查手段。这道题考察的是你对JVM运行时数据区划分的熟悉程度。Java堆空间不足会产生java.lang.OutOfMemoryError: Java heap space通常是因为对象创建量超过堆大小方法区JDK 1.8后是元空间不足会产生Metaspace错误线程栈不足则出现Unable to create new native thread这类错误最隐蔽往往不是代码问题而是操作系统层面限制了进程线程数。针对 heap space 的排查我当时的答题思路是三步先通过jmap -dump:formatb,fileheap.bin pid导出堆快照再用MAT或JProfiler分析大对象和内存泄漏链最后修正代码或调整启动参数。这套思路今天依然适用而且几乎每个线上OutOfMemoryError都可以按这个顺序定位。有个细节值得提醒java: outofmemoryerror: insufficient memory这个特定报错信息有时候并不是JVM的标准错误而是操作系统在分配直接内存或本地内存失败时产生的可能和容器的内存限制、服务器的 swap 配置有关系。排查时不能只盯堆参数还要看宿主机资源。如果笔试题里出现这种报错答题方向也要分两层一层是JVM层另一层是操作系统层这样才完整。4.2 垃圾回收机制从基础概念到G1的卡片笔试选择题里一定会有GC相关题目核心围绕JVM内存分为哪几块、哪些区域是线程共享的、哪些是线程私有的、Minor GC和Full GC的区别、常见的垃圾回收器有哪些。当时卷子里有道题是新生代采用什么垃圾回收算法老年代采用什么算法。答案分别是复制算法和标记-整理或标记-清除。复制算法的优势在于逻辑简单、没有内存碎片缺点是会浪费一部分内存用于存活对象的复制空间老年代对象存活率高标记-整理比复制更合适能减少对象移动开销。更深一点简答题可能会问G1垃圾回收器与CMS有什么区别。G1把堆划分为多个大小相同的Region优先回收价值最大的Region因此可以做到可预测的暂停时间CMS则是以最小停顿为目标但它会产生内存碎片且浮动垃圾只能等下一次Full GC处理。从2015年到今天G1已经成了绝大多数JDK 1.8之后版本的主流选择这道题放到现在依然是大厂高频题。我的经验是复习这类题时最好配合参数一起记-Xms和-Xmx设置堆大小-Xmn设置新生代大小-XX:MaxMetaspaceSize限制元空间-XX:PrintGCDetails打印GC日志。笔试不一定会直接问参数但JVM调优简答题里能加上参数细节整体作答的含金量会高很多。5. 多线程与并发synchronized、volatile和死锁的博弈5.1 synchronized和volatile到底解决了什么问题多线程是Java工程师笔试的必考板块网易2015年的这套卷子也不例外。选择题常考volatile关键字能保证什么、不能保证什么。volatile保证的是可见性和有序性不保证原子性。可见性指一个线程修改了共享变量后其他线程能立即看到最新值因为volatile变量在读写时会插入内存屏障防止指令重排原子性指的是一个操作不可中断比如count这种读改写操作多线程下仍然会互相覆盖。synchronized则不同它以监视器锁monitor lock的形式保证同一时刻只有一个线程执行临界区代码所以在线程安全上更重。但它也有代价获取锁、锁竞争、阻塞唤醒都有开销。所以并发编程的选择原则是能用无锁原子类AtomicInteger、LongAdder解决的问题不用锁能用volatile保证可见性的场景不轻率加synchronized。这套卷子里有道经典程序题多线程循环打印A、B、C。解法很多我当时用的是synchronized加状态标志加wait/notify虽然能答对但代码可读性和性能一般。后来学会了用Semaphore、LockSupport、CompletableFuture等更优雅的方案。如果笔试时间充裕能在传统解法之外补充一种更现代的解法会让阅卷人觉得你有广度。5.2 死锁的四大条件与线上排查案例关于死锁笔试题常考两条死锁的四个必要条件是什么以及如何避免死锁。四个条件分别是互斥、占有并等待、不可剥夺、循环等待。避免死锁的思路就是打破这四个条件中的任意一个实际工程中最常用的手段是保证多个锁的获取顺序一致避免循环等待。我记得当年的简答题里有一道两个线程分别持有锁A和锁B然后互相等待对方释放锁会造成什么现象如何用jstack排查。这个问题的排查路径是先执行jps找到目标Java进程的PID再执行jstack pid把线程堆栈打印出来搜索deadlock关键字JVM会直接提示检测到了死锁并列出哪些线程持有哪些锁、在等待哪些锁。配合堆栈信息就能快速定位到出问题的代码行。这个技能我后来在线上真的用到了。有一次凌晨报警接口超时jstack一抓就发现两个线程互相持有数据库连接池的锁和业务缓存锁导致整个线程池被打满。堆栈清晰到连方法名都标出来了改掉加锁顺序后问题消失。笔试当年如果只懂理论遇到真实现场会慌但纸上得来的知识未必没用关键是你要知道实际工具怎么用。5.3 从Callable到线程池线程创建方式与最佳实践笔试卷里还喜欢问Java创建线程有哪几种方式。标准答案是继承Thread类、实现Runnable接口、实现Callable接口并通过FutureTask包装、使用ExecutorService线程池。但答题时不能到这里就停因为阅卷人想听的是区别Callable能返回执行结果且能抛出受检异常Runnable不能线程池是对线程生命周期的统一管理避免反复创建销毁线程的开销。线程池的核心参数也是个高频点核心线程数、最大线程数、空闲存活时间、任务队列、拒绝策略、线程工厂。实际生产中我会重点提醒队列该用有界队列而不是无界队列因为无界队列会让线程池里最大线程数形同虚设任务堆积过多时可能导致内存溢出。拒绝策略默认是AbortPolicy直接抛RejectedExecutionException如果业务上允许丢弃可以用DiscardPolicy但大部分场景更推荐CallerRunsPolicy让提交任务的线程自己去跑那个任务这是一种自然的背压机制。笔试题里给一个线程池配置让你判断任务在多线程下的执行顺序这种题我见过好几次。关键要搞清楚核心线程满了进队列队列满了才创建非核心线程再满了才触发拒绝策略。这个顺序记熟选择题基本不会错。6. 算法题冒泡、快排与手写代码的考场策略6.1 手写冒泡排序不光写出来还要会优化算法编程题在2015年网易Java笔试题中占的分值很可观一般会给一两道题要求手写代码。冒泡排序是最基础的题目但阅卷人会看细节。标准写法是这样的public static void bubbleSort(int[] arr) { for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }这版代码的亮点在于swapped标志位如果某一轮循环中没有发生任何交换说明数组已经有序可以提前终止。这个优化能让近乎有序的数组从 O(n²) 降到接近 O(n)在面试官的印象分上是一个加号。很多人只写两重循环看着对但缺少提前终止的判断遇到边界情况时会做很多无用功。6.2 快速排序从递归框架到随机基准值快排在热词里出现了也是当年笔试编程题的重头戏。出题形式可能是让你实现快排的partition过程也可能是给你一段不完整的快排代码让你补全。以下是我比较推荐的快排实现使用随机基准值来避免对已有序数组排序时退化为 O(n²) 的极端情况public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot 1, right); } private static int partition(int[] arr, int left, int right) { int randomIndex left (int)(Math.random() * (right - left 1)); int pivot arr[randomIndex]; swap(arr, randomIndex, 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; }这里有两个容易踩的坑。第一随机基准值的下标必须在递归区间内不能直接取arr[0]否则对有序数组每轮都在最左边切分递归深度变成 n。第二partition返回的位置是pivot元素的最终位置之后的递归区间要排除掉这个pivot所以左半部分是[left, pivot - 1]右半部分是[pivot 1, right]。很多手写快排报错或者栈溢出就是边界没处理好。我当时在笔试时写完快排还顺手加了一段注释说明时间复杂度和适用场景平均O(nlogn)、最坏O(n²)、原地排序、不稳定。这个习惯让阅卷人一眼看出我是真懂算法而不是背代码。6.3 算法题的考场答题顺序和调试技巧笔试题时间有限算法题怎么分配时间很关键。我的建议是按分值分配如果编程题占30分那至少要留出三分之一的总考试时间给算法题。先做有把握的那道再做难度高的。如果遇到不会的题至少写出思路和伪代码哪怕不能跑通也要让阅卷人看到你的分析方向。手写代码容易出现细节错误比如数组下标越界、循环结束条件写错、返回值类型不匹配。建议在草稿纸上先列几个测试用例空数组、只有一个元素的数组、完全逆序的数组、包含重复元素的数组。用这四类用例过一遍逻辑基本能排除90%的低级错误。代码规范也很重要。类名和方法名不要用汉语拼音变量命名要有语义方法要加注释。网易这类公司很看重代码习惯因为笔试不只是考知识点也是提前模拟入职后的代码评审。7. 这套卷子带给今天的三点启示从真题到实战能力7.1 基础题永远不过时背八股不等于懂原理重新做完2015年的网易Java笔试我最强烈的感受是越基础的东西越能区分工程师的水平。现在大家调侃的“面试八股文”确实覆盖了很多知识点但八股文通常只回答“是什么”而这套笔试题会继续问“为什么这么设计”“换一个场景会怎样”。比如HashMap八股文会告诉你“1.8之后引入了红黑树”但这套卷子的问法是为什么链表长度超过8才转红黑树而不是5或者10。这个问题的答案和泊松分布有关在随机hashCode下链表长度达到8的概率只有约千万分之一所以 8 是一个平衡时间和空间的阈值。这种题背八股是背不出来的必须理解设计者的意图。所以我的复习建议是每看到一个核心知识点就自己追问三个“为什么”然后尝试用一段简短的文字回答答不上来的再查资料。这个方法虽然费时间但每弄懂一个点知识点之间的连接就会更结实一些。7.2 结合实战工具别让知识停在纸上这套卷子里涉及的jstack、jmap、线程池、内存溢出排查单靠看题很难形成真实操作能力。我强烈建议在本地装一个Linux虚拟机或直接用云服务器自己造几个问题场景写一段死锁代码用jstack看堆栈写一段内存泄漏代码用jmap导出堆快照写一个多线程累加用压测工具看并发误差。只有亲手操作过笔试里的知识点才会长成肌肉记忆。环境配置也是一个绕不开的话题热词里的java环境变量配置、vscode运行java报错乱码、源发行版 17 需要目标发行版 17都说明很多人在入门阶段被环境绊倒过。如果笔试机试时要编译运行Java代码环境变量没配好或者编码不对再好的算法也白搭。建议提前把JDK、Maven、开发工具的编译编码都配置成UTF-8并保证命令行能直接javac、java跑通最小代码。7.3 真题的价值不在押题而在建立知识体系一个很现实的问题是2015年的题目不太可能原封不动出现在今天的笔试中。但考点背后的知识体系没有变。Java的集合框架、JVM内存、并发工具、经典排序算法这些内容在十多年间几乎没有本质变化。如果把知识体系比作一棵树语言基础是根集合和并发是干框架和中间件是枝叶。现在的学生习惯先学Spring Boot遇到底层问题时反而没有抓手。倒过来做效果更好先用笔试题把根基打牢再去学框架时很多配置和实现原理就有了依据学起来会快得多。我建议准备面试的朋友可以按这套卷子的结构自己列一个检查清单能否在白板上手写快排、能否说清volatile的内存语义、能否画出线程池的执行流程、能否分析一段死锁代码。对照清单查漏补缺比盲目刷几套模拟题高效得多。最后再分享一个小技巧做题时遇到不懂的知识点不要直接看答案先试着用自己的话解释一遍哪怕解释得不对也比直接记答案有价值。因为面试官追问的时候你说出的内容是不是经过自己消化的几句话就能听出来。我当年能通过笔试靠的正是把每道错题都当成一篇小型博客来整理这套方法到现在依然管用。