ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

凹凸科技Java笔试题复盘:从基础语法到JVM与并发核心考点

凹凸科技Java笔试题复盘:从基础语法到JVM与并发核心考点 我拿到凹凸科技2017秋招Java工程师笔试卷的时候第一反应是有点意外。这套卷子没有像很多大厂那样搞一堆偏题怪题整体走的是基础扎实度代码硬功夫的路线但恰恰是这种看似朴素的卷子最容易暴露一个Java开发者的真实水平。那份卷子我做完之后对了对答案选择题错了三道编程题虽然都写出来了但有两处不够优雅的地方复盘之后收获很大。今天就把这套卷子相关的知识点、题目思路和我踩过的坑完整拆开讲讲希望能帮你少走一些弯路。这套卷子覆盖的知识面很典型Java基础语法、面向对象、集合框架、JVM基础、并发编程、Lambda表达式、还有手写排序算法。不管你是正准备秋招的应届生还是想检验自己Java功底的在职开发者这份复盘都值得一看。我尽量按真实笔试的节奏来还原每道题背后涉及的原理也会一并说透。1. 试卷总览与考察风向2017秋招的java岗到底在考什么1.1 试卷结构与我的做题顺序这份卷子整体分为四块单选题大概15道、多选题5道、简答题3道、编程题2道。我拿到卷子后的做题顺序是先花10分钟快速扫一遍所有题目心里预估一下每道题的时间成本。选择题里那些一眼就能看出答案的优先做拿不准的先标记跳过简答题放在选择题之后集中作答最后留足40分钟给编程题。很多人笔试失败不是因为不会而是因为时间分配失衡在简答题上磨太久结果编程题没时间写完这是最亏的。整体来看这套卷子里基础语法和面向对象相关题目占比最高大约能占到40%左右集合和JVM内容占30%并发和Lambda占20%剩下10%是零散的综合题。这个比例其实也反映了2017年前后Java岗位面试的整体风向——基础为王框架反而考得不多。1.2 当年java笔试的命题特点2017年这个时间节点很有意思。Java 8已经发布三四年了Lambda表达式和Stream API已经进入生产环境但很多学校的课程还停留在Java 6/7的时代导致不少应届生在笔试里遇到Lambda相关的题目直接懵住。同时Spring Boot开始普及微服务概念开始热起来但笔试考察的重点依然在语言本身而不是框架。这套卷子还有一个特点很喜欢考看起来简单、实则暗藏坑的题目。比如运算符优先级、Integer缓冲池、ArrayList扩容机制这类都是那种你觉得自己会但一不留神就错的知识点。这提醒我们一件事——刷题不能只盯着高深的技术基础语法和JDK源码层面的细节同样值得反复咀嚼。2. 基础语法与面向对象部分最不该丢分的送分题2.1 标识符、运算符与枚举的那些坑卷子第一道单选题就很经典下面哪个是合法的Java标识符选项里有2variable、my-variable、$variable、class这类迷惑项。正确答案是$variable。原因是Java标识符只能以字母、$、_开头不能以数字开头不能包含短横线也不能是关键字。class虽然是关键字但有些人会忽略my-variable里的短横线会被识别成减号这两个都是高频错误选项。这道题本身不难但它背后其实是一个更值得注意的点编码规范。很多公司有自己的命名规范比如阿里规约就明确禁止用$作为变量名开头因为$在有些代码生成工具和内部类机制中有特殊含义。笔试考的是合法性实际工程里考的是规范性这是两码事。还有一道运算符优先级的题核心是int a 1; int b a a;问b的值。答案是4。我当时差点选了3因为很多人会简单算成1 2 3但实际上a是先返回a当前值1然后a变成2a是先把a变成3再返回3。所以结果是1 3 4。这类题没有捷径最好考前把优先级表过一遍特别是自增自减、三目运算符、位运算这些容易混淆的地方。枚举类型也出了一道题定义一个枚举Color { RED, GREEN, BLUE }问Color.valueOf(green)会怎样。正确答案是抛出IllegalArgumentException因为枚举的valueOf是大小写敏感的green和GREEN不匹配。这个知识点在switch语句配合枚举使用时特别容易踩坑尤其是从数据库或前端传值过来做枚举转换的场景。2.2 面向对象三座大山重载、重写与多态简答题里有一道很经典重载Overload和重写Override有什么区别多态的实现原理是什么重载是编译期多态同一个类里方法名相同、参数列表不同重写是运行期多态子类对父类方法重新实现方法签名必须一致返回值类型可以协变访问修饰符不能比父类更严格不能抛出比父类更宽泛的受检异常。多态的实现原理这个点得往深了说。JVM在类加载的时候会给每个类在方法区生成一个方法表vtable里面存着该类的实例方法实际入口地址。当代码里写Animal a new Dog(); a.shout();的时候编译期只验证Animal类型有没有shout()方法运行期通过a引用指向的实际对象类型去Dog类的方法表里找对应的入口。这就是动态绑定的过程。我当时在试卷上把这个过程写得很细然后补了一句正是因为方法表的存在方法调用的开销比字段访问大所以在热点代码里尽量减少不必要的多态调用。这个补充其实挺加分的因为它显示出我不只知道概念还思考过性能层面的影响。2.3 数组越界与异常处理看似简单实则高频编程题里有一道用for循环遍历数组并打印每个元素要求写出代码。这题考察的核心是ArrayIndexOutOfBoundsException。很多人随手就会写成这样int[] arr {1, 2, 3, 4, 5}; for (int i 0; i arr.length; i) { System.out.println(arr[i]); }这里的问题就是导致了越界。正确应该是。还有一个常见错误是前置判断应该先判断数组是否为空否则空指针一样炸。我当时把边界判断写成了防御式编程的样子public void printArray(int[] arr) { if (arr null || arr.length 0) { return; } for (int i 0; i arr.length; i) { System.out.println(arr[i]); } }异常处理那题问的是try-catch-finally中finally的执行时机以及如果finally里也有return会怎样。答案是只要JVM不退出finally一定会执行但如果在try或catch中执行了System.exit()finally就不会执行如果finally里有return它会覆盖try或catch里的return。这个知识点在真实项目里太容易踩坑了尤其是资源关闭场景。我见过一个线上问题某团队在finally里关闭连接后顺手加了个return false结果把try里判断成功的return true给覆盖了导致接口永远返回失败。3. 集合框架与JVM笔试中的分水岭3.1 HashMap的底层原理与加载因子这套卷子的多选题里有一道HashMap相关的题选项涉及底层数据结构、加载因子、扩容机制、线程安全性。这题几乎是把HashMap的核心考点一网打尽。Java 7和Java 8的HashMap实现差异很大。Java 7是数组链表哈希冲突时用头插法Java 8改成了数组链表红黑树链表长度超过8且数组长度超64时转红黑树冲突插入方式也改成了尾插法。为什么改因为头插法在多线程扩容时可能出现环形链表导致get操作死循环Java 8的尾插法能缓解这个问题但HashMap依然不是线程安全的。加载因子0.75是时间和空间的一个折中。简单说加载因子越大空间利用率越高但哈希冲突概率变大查询变慢加载因子越小越费内存但冲突少、查询快。0.75是大量实验统计下来的一个经验值。扩容是变成原来的两倍并且所有元素要重新计算哈希、重新分布代价很高所以如果能预估数据量初始化的时候最好直接指定容量。我在这道题上栽了一个跟头——多选题里有个选项说HashMap允许null键和null值我当时犹豫了一下没选因为脑子里突然闪过Hashtable不允许null结果把HashMap也带进去了。实际上HashMap是允许null键和null值的Hashtable和ConcurrentHashMap才不允许。这种对比型考点考前一定要单独整理一份对照表。3.2 JVM内存区域与垃圾回收的必考套路简答题第二道是JVM相关描述JVM运行时数据区以及各个区域可能抛出的异常。这题的覆盖范围其实很大但核心是这样一张逻辑图程序计数器、虚拟机栈、本地方法栈、堆、方法区。程序计数器是唯一不会OOM的区域虚拟机栈和本地方法栈会抛出StackOverflowError和OutOfMemoryError堆和方法区都会OOM。题目里专门问到了StackOverflowError的场景那就是递归调用没有终止条件或者递归太深。我在试卷上写了一个实际例子public void recurse() { recurse(); }这个没等递归多少次就会栈溢出。我调过的一个线上案例是某个导出功能里递归遍历目录树目录层级深到一定程度直接栈溢出最后改成非递归的方式才解决。垃圾回收这里考的是可达性分析。为什么不用引用计数因为引用计数解决不了循环引用的问题——两个对象互相引用但已经不是GC Roots可达了引用计数始终是1永远无法回收。可达性分析从GC Roots出发往下扫描对象引用链没有被链到的对象会被标记为可回收。GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、JNI引用的对象等。3.3 你被栈这个词骗过吗——那些容易混淆的运行时概念这套卷子有一道选择题问int[] arr new int[10]中arr存在哪里数组对象存在哪里答案是arr这个引用变量在虚拟机栈上如果是局部变量数组对象本身在堆上。这道题迷惑性很强因为很多人看到数组就条件反射地觉得数组应该在栈上但数组也是对象对象实例都是分配在堆上的。那为什么局部变量arr在栈上因为局部变量是方法的内部状态方法调用结束就销毁放到栈里最合适。这个区分在内存分析时特别重要——new出来的都在堆里栈里只放引用。还有一个容易混淆的知识点String和new String()的区别。String s1 abc;在Java 7之后abc这个字面量会去堆里的字符串常量池找如果已有就复用String s2 new String(abc);无论如何都会在堆里创建一个新对象。笔试里经常拿来比较这两个s1 s2结果是false。但String的equals比较的才是内容所以equals结果是true。这道题我倒是没做错但我见过不少人在评论区争论不休所以还是值得单独拎出来提醒一下。4. 编程题实战排序算法与字符串处理4.1 冒泡排序的优化笔试必写但很少有人写得优雅编程题第一道是冒泡排序。说实话手写冒泡排序不难难得是写得有点东西。最基础的版本长这样public static void bubbleSort(int[] arr) { for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }但如果只是写这个只能拿基础分。我当时加了一个优化设置一个swapped标志位如果某一轮遍历下来没有任何交换说明数组已经有序直接跳出循环。public static void bubbleSortOptimized(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 temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }这个优化在数组接近有序的时候能把时间复杂度从O(n^2)直接降到O(n)。面试官看到这一步通常会觉得你不是死记硬背而是理解了这个算法的本质。再往上还可以做鸡尾酒排序双向冒泡但笔试里到这个程度已经够用了。我在试卷上写代码的时候特意在注释里标注了最优情况O(n)最坏情况O(n^2)这其实是在向阅卷人传递一个信号我清楚这个算法的复杂度边界。4.2 快速排序的实现与优化从能跑到优雅第二道编程题是快速排序。这题我当时写了十几个方法调用链写完发现有点长但胜在思路清晰。快排的核心是分治选一个基准值把小于等于基准的放左边大于基准的放右边然后递归处理左右两个子区间。我采用的是经典的双指针挖坑法public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } if (i j) { arr[i] arr[j]; i; } while (i j arr[i] pivot) { i; } if (i j) { arr[j] arr[i]; j--; } } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }快排有个特别经典的坑当数组已经有序时如果每次基准都选第一个元素快排会退化成O(n^2)。我在试卷上专门提了一句说可以采用三数取中来选基准取左端、中间、右端三个数的中位数或者随机选基准来尽量避免最坏情况。关于递归深度JDK里Arrays.sort()对基本类型用的是双轴快排对对象类型用的是TimSort。笔试的时候不用扯这么深但如果你能顺带提一句工业界的排序实现会针对小数组转用插入排序因为插入排序在小数据量下常数因子更小会是一个很好的加分项。4.3 字符串反转与回文判断这类小大题除了排序卷子还有一道字符串相关的编程题判断一个字符串是否是回文字符串同时要求忽略大小写和非字母数字字符。我的思路是用双指针从两端往中间走遇到非字母数字就跳过然后比较忽略大小写后的字符是否相等。核心代码是这样public static boolean isPalindrome(String s) { if (s null) { return false; } int left 0; int right s.length() - 1; while (left right) { while (left right !Character.isLetterOrDigit(s.charAt(left))) { left; } while (left right !Character.isLetterOrDigit(s.charAt(right))) { right--; } if (Character.toLowerCase(s.charAt(left)) ! Character.toLowerCase(s.charAt(right))) { return false; } left; right--; } return true; }这道题考察的点很综合字符串遍历、指针移动、Character类的方法使用、边界条件处理。我见过很多人只处理了字母忘记了数字也算有效字符还有人做成了先过滤再反转的写法代码长了效率还差。笔试里这类字符串题目看起来不大但其实是区分度的关键——细节控的人在这里拿分粗糙的人在这里丢分。5. 并发与Lambda面试官真正想听的部分5.1 线程安全的几种实现方式简答题第三道是关于线程安全的说出几种Java中实现线程安全的方式并说明各自的适用场景。我当时写了几种从易到难排了一遍synchronized关键字JVM内置锁。可以锁方法、锁代码块、锁对象。优点是简单可靠缺点是锁的粒度不好控制竞争激烈时性能会下降。volatile关键字保证可见性和有序性但不保证原子性。适合状态标志位的场景。volatile最经典的例子就是boolean flag做线程间的运行状态控制。Lock接口与ReentrantLock比synchronized更灵活支持公平锁、非公平锁、可中断、可以设置超时、可以用Condition做精确的通知等待。原子类AtomicInteger等基于CAS实现的无锁线程安全。ThreadLocal每个线程独享一份变量副本以空间换时间避免竞争。不可变对象String就是一个经典例子状态不可变天然线程安全。每一种的适用场景说起来都有很多话。比如synchronized适合临界区代码量很少、并发冲突不高的场景ReentrantLock适合需要尝试获取锁、超时等待的场景ThreadLocal在Spring的Transactional里就大量使用因为每个数据库连接需要绑定到当前线程。这道题我写得很满因为并发知识是面试官区分会用Java和懂Java的核心分水岭。笔试往往只是敲门砖后面技术面还会继续深挖所以这一块值得花大力气准备。5.2 从匿名内部类到Lambda表达式Java 8的灵魂考点卷子里有一道编程题很好玩有一个ListString要求过滤掉长度小于等于3的字符串然后把剩下的转成大写最后按字典序排序输出。Java 7时代的写法会长这样ListString result new ArrayList(); for (String s : list) { if (s.length() 3) { result.add(s.toUpperCase()); } } Collections.sort(result);但2017年的笔试明确要求用Java 8的Lambda和Stream API来写。正确是这么写ListString result list.stream() .filter(s - s.length() 3) .map(String::toUpperCase) .sorted() .collect(Collectors.toList());这个写法一出来代码量立刻少了一大半。像我之前说的2017年刚好卡在Java 8普及的节点上很多应届生还停留在传统写法能够在笔试里熟练用Stream的会给面试官留下很深的印象。Lambda表达式的原理也值得说一下。Lambda其实是函数式接口的匿名实现类的一种简化写法。所谓函数式接口就是只有一个抽象方法的接口比如Runnable、Comparator、Function等。Lambda表达式在编译后会被翻译成一个被invokedynamic指令调用的动态方法而不是简单地在编译期生成匿名内部类。这也是为什么Lambda在性能上通常比匿名内部类要好一些——它延迟到运行期才生成真正的实现有机会复用同一个实现实例。还有一个高频考点Lambda表达式捕获的局部变量必须是事实上不可变的effectively final。也就是说在Lambda里引用一个局部变量这个变量在初始化后不能再被修改。我当时在试卷里写了一个反例用一个int[]数组来变相修改值就是这个思路int[] counter {0}; list.forEach(item - counter[0]);这种写法能跑通但它违反了effectively final的本意真实项目里不太推荐。更好的做法是用AtomicInteger或者直接使用Stream的count()。6. 复盘与应对策略这套卷子给我的三点教训6.1 知识体系上的盲区做完这套卷子我最大的感受是笔试题目本身不算难但覆盖面很广任何一个知识点掌握得不牢都会在选择题里露出马脚。我那三道错题一道是HashMap的null键值问题一道是运算符优先级还有一道是JVM方法区的运行时常量池位置。这三道题恰好对应了我当时的三大盲区集合类的细节对比、基础语法的边界情况、JVM规范里的精确划分。后来我针对这些盲区做了三件事把Java集合框架里每个类的特性做了一组对比表格把JVM运行时数据区画了十几遍直到烂熟于心把网上的Java基础面试题汇总重新刷了两遍。这个过程虽然枯燥但效果非常明显——之后我笔试的正确率提升了一大截。6.2 时间分配与答题策略还有一个很有意思的复盘发现选择题我用了20分钟简答题用了30分钟编程题用了40分钟最后剩下10分钟检查。这个节奏是合理的编程题时间占比一定要最高因为手写代码是最能拉开差距的环节。检查的时候我重点做了一件事把所有写过的for循环边界都重新过一遍。为什么因为笔试中80%的编程题低级错误都是边界问题写成或者反过来i 1忘了加数组长度为0的时候没有特殊处理。只要把边界条件检查一遍正确率就能从能跑提升到稳过。6.3 给后面备考人的建议如果你正在准备Java笔试我的经验是基础语法、面向对象、集合、JVM、并发、Java 8新特性这六块是绝对的主干搞定了它们大多数公司笔试题的60%~70%你都能从容应对。编程题方面排序算法要能盲写字符串处理题要熟练使用双指针技巧边界条件必须养成直觉性检查的习惯。更重要的是笔试不只是考知识储备还在考你在时间压力下的抗压能力。平时练习的时候建议给自己卡时间模拟真实笔试的场景。我后来每次练习都会开着计时器一开始很紧张练多了就习惯了。这个习惯让我在真正的笔试里心态稳了很多看到不会的题也不会慌先跳过回头再来处理。最后说一句笔试其实是一个很诚实的检验方式——你平时积累了多少考场上就会暴露多少。与其指望考前突击不如把功夫下在平时吃透每一个基础知识点多动手写代码这样无论拿到什么卷子心里都有底。
返回列表