尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

2017斐讯Java校招笔试题复盘:核心考点与答题思路

2017斐讯Java校招笔试题复盘:核心考点与答题思路 如果你参加过2017年前后的校招应该对斐讯的秋招试题有所耳闻。当时这套Java试卷在好几个求职群里被转来转去不是因为题目有多刁钻而是它把Java基础里该考的点基本都过了一遍。我记得自己考完的第一反应是选择题有坑简答题有深度编程题不算难但写完整不容易。现在回头看这套卷子其实代表了那个年代Java校招的典型风格重基础、重原理、重代码规范。我把当时涉及的核心知识点和答题思路整理出来也算给准备Java面试的朋友一份可以对着练手的参考资料。1. 试卷整体结构与出题逻辑1.1 为什么这份卷子值得复盘斐讯的这套Java笔试试卷并不是那种一上来就让你手写红黑树的“劝退型”题目。相反它更像是一次系统的基础能力体检。题型覆盖了选择题、填空题、简答题和编程题知识点从面向对象、集合框架、异常处理到JVM内存和常见算法都有涉及。这种结构的优势在于它不会因为某一道偏题导致整个考核失真而是能通过多个维度判断一个候选人是否具备扎实的Java功底。当时很多同学把精力放在刷各种“高深”问题上结果栽在了最基础的部分。比如选择题里有一道关于String对象创建的题目考的是常量池和堆内存的关系还有一道关于HashMap在JDK1.7和JDK1.8底层差异的题目如果不了解链表头插和尾插的区别很容易选错。这套卷子给我的感觉是出题人并没有追求冷门而是想看看你有没有把基础原理真正搞懂。现在再回头看这套卷子的很多考点到今天依然是面试中的常客。只是如今的面试题更像流水线产品“Java八股文”味道越来越浓。而2017年的这套题至少在编程题部分还保留着对编码能力的直接考察。所以它特别适合两类人参考一类是正在准备校招、想系统自测Java基础的同学另一类是已经有工作经验、想巩固底层原理的开发者。1.2 考点分布与题型比例我根据回忆把整套试卷的题型和考点整理成了下面这张表基本能还原当时的情况。题型主要考点难度选择题final/static、String、集合、异常、JVM内存中等填空题运算符、基本类型转换、类加载顺序简单简答题抽象类与接口区别、重载与重写、HashMap原理中等偏上编程题冒泡排序、快速排序、二分查找、单例模式中等从分值占比来看基础语法和面向对象相关内容的比重最大大概占了四成集合与JVM相关的题占三成算法和编程题占三成。这个比例很符合当时主流互联网公司的校招风格也说明出题人想找的不是“只会背书的人”而是能真正理解并动手写代码的人。1.3 从当年试题看校招出题逻辑为什么这份卷子能流传得比较广因为它把“校招出题”这件事做到了一个比较典型的基准线上。它不会为了难而难而是用几个常见的“陷阱”来筛选候选人对细节的把控程度。比如static代码块、构造块、构造方法的执行顺序这种题不需要你背八股文只要你平时认真写过类观察过输出结果就一定能答对如果只是生硬地背结论遇到稍微变形的题目还是容易翻车。另一个值得注意的点是2017年的Java面试还没有像现在这样疯狂追求“源码级”深挖。当时能说清楚HashMap的底层结构、ArrayList扩容机制、异常处理的基本规则就已经算是不错的候选人了。而现在面试官更喜欢追问“为什么加载因子是0.75”“ConcurrentHashMap在JDK1.8中如何保证线程安全”。所以我在复盘这套卷子的时候也会在对应考点后面补充一些今天的延伸问题这样大家既能应对传统笔试也不至于被现在的追问打个措手不及。2. 基础语法与面向对象题目拆解2.1 final、static老生常谈但总有人错选择题里有一道题问的是下面这段代码的输出顺序。class Parent { static { System.out.println(parent static block); } { System.out.println(parent instance block); } public Parent() { System.out.println(parent constructor); } } class Child extends Parent { static { System.out.println(child static block); } { System.out.println(child instance block); } public Child() { System.out.println(child constructor); } } public class Main { public static void main(String[] args) { new Child(); } }正确输出顺序是parent static block - child static block - parent instance block - parent constructor - child instance block - child constructor。这里的关键点在于静态代码块在类加载阶段执行且只执行一次实例代码块和构造方法在每次创建对象时都会执行。创建子类对象前会先加载父类所以父类的静态代码块先执行然后加载子类执行子类静态代码块。真正实例化时会先执行父类的实例代码块和构造方法再执行子类的实例代码块和构造方法。我当年写完这道题之后特意把执行顺序总结成了一个口诀“先静态、再父类、后自己”。用多了之后就很难再记混。这里还要提醒一下final和static如果组合在一起比如static final int NUM 100;它就是一个编译期常量在类加载时就会确定值但如果是引用类型的static final则要区分情况。这类细节也经常出现在填空题里。2.2 String、StringBuilder、StringBuffer这套卷子关于字符串考了好几道。最经典的一道是String s new String(abc)会创建几个对象答案是1个或2个。如果字符串常量池中已经存在abc那么只会在堆中创建一个String对象如果常量池中没有abc则会在常量池中创建一个字符串实例再在堆中创建一个String对象总共两个。很多人只回答了“两个”忽略了前提条件这就是丢分点。另一道题是关于字符串拼接的String a a; String b b; String c a b;a b在编译过程中会被优化成new StringBuilder().append(a).append(b).toString()所以c是运行期生成的新对象与直接写String c ab;的情况不同。笔试里还会顺带问StringBuilder和StringBuffer的区别重点就是StringBuffer用synchronized修饰方法是线程安全的但性能略低StringBuilder是线程不安全的但单线程环境下性能更高。这类题目看起来简单但能直观区分出有没有认真读过源码。如果只是背结论遇到类似“第一个字符串用final修饰后的拼接结果”这种变形题就容易出错。比如final String a a;时编译器会直接做常量替换a b等同于a b结果就是常量池中的ab。我在复盘时就把这些情况都列在了一起方便对比记忆。2.3 抽象类与接口概念辨析加典型例题简答题里有一道经典问题抽象类和接口有什么区别分别在什么场景下使用这种题几乎每一年的Java校招都会出现2017年也不例外。我从三个层面来回答这个问题。第一语法层面。抽象类可以有构造方法、成员变量、具体方法和抽象方法接口在Java 8之前只能有常量和抽象方法Java 8允许默认方法和静态方法Java 9之后还允许私有方法。第二设计层面。抽象类体现的是“is-a”的关系适合用来抽象一类事物的共性接口体现的是“can-do”的关系适合用来定义行为规范。比如狗是一种动物可以继承抽象类Animal狗会看门可以实现Guard接口。第三实际选择层面。如果多个类有共同的代码逻辑抽出抽象类可以减少重复代码如果只是需要定义某种能力或者一个类已经继承了其他类只能用接口。笔试里还有一个常见变形抽象类能不能没有抽象方法能不能被final修饰答案分别是“能”和“不能”。抽象类没有抽象方法时存在的意义主要是防止被直接实例化final修饰的类不能被继承而抽象类存在的意义就是被继承所以两者矛盾。2.4 重载与重写笔试陷阱集中地关于重载和重写的题目选择题里出现了好几次。核心区别其实很简单重载是编译期的多态发生在同一个类中方法名相同、参数列表不同重写是运行期的多态发生在继承关系中子类重新实现父类的方法方法签名必须一致。但笔试不会只问定义它一定会设置陷阱。比如返回值不同但参数列表相同算不算重载不算因为编译器无法仅凭返回值区分调用的是哪个方法。再比如静态方法能不能被重写答案是“不能”严格来说子类中写一个和父类静态方法签名一样的方法叫“隐藏”并不是重写。还有一道题问private方法能不能被重写private方法只能在本类中访问子类无法感知所以也不存在重写的概念。我记得当时选择题里有一道很经典的坑给出两个重载方法分别是print(String s)和print(Object o)然后调用print(null)。很多人会犹豫到底调用哪个。实际上Java编译器会选择参数类型最具体的那个也就是print(String)。因为String是Object的子类更具体。如果这时再增加一个print(Integer i)那么print(null)就会产生歧义直接编译报错。这种题目特别适合用来考察候选人对重载机制的理解深度。2.5 枚举类型当时已经出现的新考点2017年Java枚举在笔试试卷里出现频率已经很高了。有一道填空题让我印象很深定义一个枚举Direction包含UP、DOWN、LEFT、RIGHT四个值要求每个枚举值有一个中文描述。答案可以写成这样public enum Direction { UP(上), DOWN(下), LEFT(左), RIGHT(右); private String desc; Direction(String desc) { this.desc desc; } public String getDesc() { return desc; } }这道题考察的是枚举本质。枚举其实是一个继承了java.lang.Enum的类枚举值就是这个类的静态常量实例。它可以有构造器、字段和方法但构造器必须是私有的因为枚举实例的创建被JVM严格管理外部无法通过new创建。这也就是为什么枚举经常被用来实现单例——它天然保证只有一个实例还能避免序列化破坏和反射攻击。如果你现在去面Java岗位枚举的题目已经延伸到了values()方法、valueOf()方法、枚举在switch中的使用以及如何用枚举消除if / else判断。这些都是当年那套试卷的进阶版本我觉得有必要在复习时一并掌握。3. 集合、异常与JVM基础题目3.1 HashMap从数组加链表到树化的基础题简答题里有一道HashMap的题目问的是HashMap的底层数据结构以及为什么查找效率高。这个问题放在今天看已经算基础中的基础但在2017年很多候选人只能答出“数组加链表”再深入一点就卡住了。我当时是结合源码去理解的。简单来说HashMap底层是一个Node数组每个Node可以是一个链表节点。put操作时先根据key的hash值计算数组下标如果该位置没有元素就放入如果已有元素就遍历链表用equals方法判断key是否已存在存在就覆盖value不存在就追加到链表尾部。当链表长度超过8且数组长度大于等于64时链表会转成红黑树降低查询时间复杂度从O(n)变成O(logn)。笔试现场可能会进一步问加载因子和扩容。默认初始容量是16默认加载因子是0.75。当元素数量超过容量乘加载因子时会触发扩容新容量是原来的2倍。0.75这个值其实是在时间复杂度和空间占用之间取了一个平衡。现在面试官更不会轻易放过HashMap他们往往会追问JDK1.7和JDK1.8的区别。1.7用的是头插法并发扩容时可能形成循环链表导致死循环1.8改成尾插法并引入红黑树就是为了解决这两个问题。但这道题在2017年的试卷里只要答清楚“数组链表链表过长转红黑树”基本就能拿满分。3.2 ArrayList与LinkedList的选择题陷阱选择题里有一道很经典的对比题ArrayList和LinkedList在查找、插入、删除操作上的时间复杂度分别是什么很多人一看LinkedList是链表就觉得它插入删除一定比ArrayList快所以选了“LinkedList所有操作都更快”这个选项是错误的。正确理解是对于随机访问ArrayList的时间复杂度是O(1)因为它底层是数组通过下标直接定位LinkedList是O(n)需要从头遍历。对于在尾部插入元素ArrayList只需要在数组末尾赋值均摊复杂度也是O(1)但如果在中间插入或删除ArrayList需要移动后续所有元素复杂度是O(n)LinkedList因为只需要修改前后节点的引用所以是O(1)前提是你已经定位到了目标位置。而定位本身又需要O(n)遍历所以实际使用中LinkedList并没有想象中那么有优势。ArrayList还有一个高频考点扩容机制。默认初始容量是10每次扩容成原来的1.5倍也就是oldCapacity (oldCapacity 1)。这道题在填空题里出现过我当时差点答成“翻倍”后来看了源码才纠正过来。这类细节没有太多技巧只能靠平时多撸代码、多看源码积累。3.3 异常体系受检异常与运行时异常试卷里关于异常的题目考得比较基础但容易漏点。选择题有一道问的是以下哪个不是RuntimeException的子类选项里有NullPointerException、ArrayIndexOutOfBoundsException、ClassNotFoundException、ArithmeticException。正确答案是ClassNotFoundException因为它是受检异常也就是编译器强制要求你处理的异常。这里需要梳理一下Java异常体系。Throwable是所有异常的父类下面分Error和Exception。Error表示系统级错误比如StackOverflowError、OutOfMemoryError程序无法恢复。Exception分为受检异常和运行时异常。受检异常包括IOException、SQLException、ClassNotFoundException等编译器会强制你捕获或声明抛出。运行时异常包括NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException等编译器不强制处理但在运行期可能随时发生。简答题里还有一道关于try-catch-finally的题问finally块中的return和try块中的return谁生效。答案是finally中的return会覆盖try中的return。但这种情况在实际开发中非常不推荐因为会掩盖异常信息。我在复盘时专门写了一个小Demo用System.out.println观察执行结果比单纯背结论要深刻得多。3.4 JVM内存区域与OutOfMemoryErrorJVM内存模型是当年试卷里比较有区分度的一部分。选择题问的是JVM运行时数据区中哪些区域是线程私有的答案是虚拟机栈、本地方法栈、程序计数器。堆和方法区是线程共享的。很快就能排除错误选项。这题只要记住“栈管运行堆管存储”的基本概念就不会错。还有一道填空题让你写出java.lang.OutOfMemoryError常见的触发场景。我当时写的是堆内存不足和元空间不足。如果是堆内存不足通常是因为创建了太多对象且无法回收比如死循环里不断new对象或者内存泄漏导致GC无法回收。元空间不足则可能是加载了过多的类特别是在反射和动态代理场景下。今天回看HotSpot虚拟机的实现已经和2017年有一些变化比如Java 8已经把永久代换成了元空间字符串常量池的位置也变了。但笔试的核心考点不变主要还是考察候选人是否理解“哪些数据放在堆、哪些放在栈、哪些放在方法区”。我在面试中很喜欢追问一句OutOfMemoryError能不能被try-catch捕获答案是可以因为它是Error的子类但一般不建议捕获因为就算捕获了程序也大概率无法恢复。3.5 类加载与双亲委派分水岭题目这套试卷的简答题里有一道题问了类加载过程。我印象很深刻因为当时很多同学都没复习到这块。类加载分为加载、验证、准备、解析、初始化五个阶段其中“准备”阶段会为静态变量分配内存并设置零值“初始化”阶段会执行静态代码块和静态变量的赋值操作。双亲委派模型也是一个高频延伸点。它的意思是当一个类加载器收到类加载请求时会先让父加载器去尝试加载父加载器加载不了才由自己加载。这样做的目的是保证Java核心类库的安全性比如你自定义一个java.lang.String也不会被程序员自己的类加载器加载进去而是由启动类加载器加载原始JDK里的String。这类题目在2017年算是中等偏难但放到现在几乎已经成了面试必问。我当时是画了一个层级图去记忆启动类加载器 - 扩展类加载器 - 应用类加载器。只要把这条链记清楚再理解“向上委托向下加载”的执行顺序基本就不会丢分。4. 算法编程题实战解析4.1 冒泡排序别以为简单就掉以轻心编程题第一道通常是冒泡排序。这题虽然简单但很容易在边界条件上出错。我建议写的时候先明确两点外层循环控制新一轮排序内层循环控制相邻元素比较和交换。下面是当时我写的标准版本public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } 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 tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } }写完之后面试官通常会追问能不能优化如果某一轮没有发生任何交换说明数组已经有序就可以提前退出。优化版本就是加一个boolean swapped标记public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } boolean swapped; for (int i 0; i arr.length - 1; i) { 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; } } }冒泡排序的时间复杂度是O(n^2)最好情况下能到O(n)空间复杂度O(1)。现在面试中直接让写冒泡排序的概率有所下降但偶尔会出现在笔试前几题中用来考察基本编码规范。写这道题时我习惯顺手加上“如果数组为空或长度不足2直接返回”的判断这个细节很容易加分。4.2 快速排序写对边界条件才是关键快速排序几乎算是Java校招编程题里的“常青树”。2017年这套试卷也有一道快速排序要求是写完整代码并说出平均时间复杂度和最坏时间复杂度。快速排序的核心是partition分区操作我当时用的是双指针左右扫描的方式public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivotIndex partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex 1, right); } private static int partition(int[] arr, int left, int right) { 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; return i; }很多同学在写partition时容易陷入死循环主要原因是边界条件没控制好。比如while (i j arr[j] pivot)这里一定要加i j否则分区时可能越界。另外交换方式建议用“挖坑法”也就是先保存pivot然后从右往左找比pivot小的值填到左边再从左往右找比pivot大的值填到右边最后把pivot放回中间。这样写出来的代码不容易出错。快速排序的平均时间复杂度是O(nlogn)最坏时间复杂度是O(n^2)发生在数组本来就有序且每次pivot都取到端点元素的情况下。空间复杂度是O(logn)主要消耗在递归栈上。笔试时如果时间紧张可以先写一个没有复杂优化的标准版本拿到基本分再口头说明可以使用随机pivot来规避最坏情况。4.3 二分查找细节决定成败二分查找是另一道容易“想当然”的题。题目通常给一个有序数组要求查找某个目标值并返回下标。大部分人能写出while循环版本的二分查找但写错循环条件的也不在少数。public static int binarySearch(int[] arr, int target) { int left 0; int right arr.length - 1; while (left right) { int mid left (right - left) / 2; if (arr[mid] target) { return mid; } else if (arr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }这里有三点值得注意。第一循环条件是left right不是left right。第二计算mid时用left (right - left) / 2可以避免left right溢出。第三更新left和right时一定要跳过mid也就是mid 1和mid - 1否则可能死循环。试卷最后还有一题是查找第一个等于目标值的下标也就是在数组中可能有重复元素时返回最左边的匹配位置。这个题目只需要在arr[mid] target时把right设为mid - 1继续往左找最后返回left。掌握了标准二分再变通一下就能应对。4.4 设计模式题双重检查锁单例编程题里还混合了一道单例模式题要求手写一个线程安全的单例。很多同学写完synchronized getInstance()之后就觉得完事了但这道题如果只写成这样一般只能得一部分分。更好的答案是双重检查锁加volatilepublic class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }volatile在这里非常重要。instance new Singleton()在字节码层面并不是一个原子操作它包含分配内存、初始化对象、将引用指向内存地址三个步骤。如果不加volatileCPU和编译器可能进行指令重排序导致另一个线程拿到一个已经分配内存但没有完成初始化的对象。加了volatile之后可以禁止指令重排序保证安全发布。当时笔试改卷时很多同学只写了最简单的懒汉式也就是整个方法加synchronized没有意识到性能和可见性问题。虽然也能保证线程安全但从代码质量上看双重检查锁无疑更优。如果你还能补充静态内部类方式和枚举方式面试官往往会眼前一亮。5. 高频易错题与面试避坑记录5.1 笔试中最容易丢分的三个陷阱我复盘这套卷子时整理出了三个最容易丢分的陷阱这些陷阱在如今的Java面试中也依然很常见。第一个是数组越界与循环边界问题。无论是冒泡排序还是二分查找很多错误都出在边界条件没有处理好。比如for (int i 0; i arr.length - 1; i)和i arr.length在大部分情况下等价但一旦循环体内对i 1进行取值就可能越界。写代码时建议多想一想循环变量的取值范围尤其是数组下标从0开始这一点。第二个是Integer的缓存问题。看下面这段代码Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 128; Integer d 128; System.out.println(c d); // false原因是Integer缓存了-128到127之间的对象在这个范围内直接复用同一个对象超出范围则每次新建对象。类似的还有Character缓存0到127等。笔试里经常出这种“看起来像基本类型比较其实是对象比较”的题需要特别注意。第三个是三目运算符的类型转换陷阱。比如Object obj true ? 1 : 2.0; System.out.println(obj);这里的结果是1.0而不是1因为三目运算符的两个分支会被提升为公共类型。如果写成true ? 1 : 2.0整数1会被转换成double类型1.0。很多人在这种题目上栽过跟头平时写代码时也要警惕。5.2 我当时怎么复盘这套卷子考完拿到卷子之后我做了一件很重要的事情把所有错题和不确定的题抄到一个本子上然后不看答案直接给自己讲一遍。讲不出来就翻书、翻源码直到能独立说清楚“为什么”。这个方法听起来笨但对巩固基础特别有效。比如HashMap的树化条件我一开始只记得“链表长度超过8”后来发现还有“数组容量大于等于64”这个前提。如果不看源码很难发现这个细节。再比如类加载顺序我起初容易背混后来自己写代码在main方法里打了很多日志观察实际输出记忆就牢固了很多。我会建议正在准备笔试的同学也试试“复盘三步法”第一步统计错题涉及的知识模块找出自己的薄弱点第二步针对薄弱点找3道同类题练习而不是只订正1道第三步隔一周不看笔记重新做一遍错题。这样做一轮下来基础会扎实很多。5.3 给准备Java面试的读者的建议从2017年到现在Java面试的趋势确实变了不少。最明显的变化就是“八股文”色彩越来越重面试官喜欢追源码、追底层原理但另一方面手写算法题的要求其实没有降低甚至越来越强调边界条件和复杂度分析。所以我的建议是“两条腿走路”基础原理要理解到能讲出来的程度算法题要练到能一次写对的程度。基础原理方面集合、JVM、并发、Spring是永恒的重点。不要只背结论可以尝试看一点源码比如HashMap的resize逻辑、ArrayList的grow方法。真到了面试现场你能说出“DEFAULT_CAPACITY 10”和“扩容1.5倍”这样的细节比空泛地背概念更有说服力。算法方面优先把冒泡、快排、二分、递归、贪心这些基础题写熟。我见过不少同学一上来就刷LeetCode困难题结果校招笔试时连快速排序都写不利索这就本末倒置了。笔试的时间有限能快速写出正确、规范、带边界检查的代码比追求冷门解法更实用。最后我想说这套2017年的斐讯Java试卷放到今天依然有很强的参考价值。它提醒我们Java校招考察的从来不是“知道多少新名词”而是“能不能用扎实的基础解决实际问题”。像final、static、字符串、集合、异常、类加载、排序和单例这些知识点看似简单却最能反映一个程序员的功底。我后来也参与过一些面试工作发现很多工作两三年的候选人面对同样的问题反而答不清楚。基础这东西什么时候开始补都不算早。
返回列表