前阵子整理旧硬盘,翻出一份自己当年整理的欢聚时代2017校招笔试题目(JAVA基础类)C卷的复盘笔记。这套题放到现在看,依然很有嚼劲。它没有堆砌冷门偏题,反而把Java开发天天会碰的基础概念老老实实考了一遍,却在细节上做足了文章。后来我带新人,也会拿其中几道题去问问刚入职的同学,看看他们基本功到底牢不牢。今天就把这套C卷的考点、答题思路和相关经验完整拆一遍,不管你是准备校招,还是想自测一下Java基础,都值得花十分钟看看。
1. 试卷总体印象:为什么一套四年前的题还能当镜子用
1.1 从题型结构看校招出题思路
那年的笔试C卷整体分三块:单选题、简答题、手写编程题。单选题考的是语法和API细节,简答题考察对面向对象、集合、异常这些核心概念的理解深度,最后一两道编程题则是直接看代码功底。
题型分布大致可以套成下面这个结构,各校招批次可能会有调整,但大方向差不多:
| 题型 | 大致分值占比 | 考察重点 |
|---|---|---|
| 单选题 | 30%-40% | 基本语法、运算符、常用类、异常机制 |
| 简答题 | 25%-35% | 面向对象、集合原理、设计思路 |
| 编程题 | 30%-40% | 手写算法、单例、字符串处理 |
C卷相比A卷、B卷,题目难度稍微温和一点,但对细节的要求一点没放松。尤其是单选题里那几个“看起来很简单,稍不注意就掉坑”的题目,非常能拉开差距。
1.2 Java基础类笔试的真正筛选逻辑
很多同学以为笔试就是要考倒你,其实不是。校招笔试的时间有限,公司真正想从一张卷子里看到的是三件事:你有没有系统学过Java,代码习惯好不好,遇到模糊概念是猜过去还是深究过。
我后来参与过一些简历筛选和面试,发现笔试分数高的同学,往往不是刷题最多的,而是对“为什么”有执念的人。比如很多人知道String是不可变的,但问到他“为什么设计成不可变”,就答不上来。而当年C卷里那种“概念加上细节”的组合题,筛选的正是这种对基础有真实理解的人。
所以这套题真正考的不是记忆力,而是你有没有把Java当成一门需要理解运行机制的语言,而不是背一背API就完事。这也是它到现在还能当镜子用的原因。
2. 核心考点拆解:这套C卷里反复出现的Java基础题
2.1 面向对象:从“是什么”到“怎么用”
面向对象基本是每张Java笔试试卷的C位,C卷也不例外。抽象类和接口的区别、方法重载和方法重写的区别、多态是怎么实现的,这几组概念几乎年年出现。
先说抽象类和接口。两者的核心区别不在于语法上谁有构造方法、谁能多继承,而在于设计意图:
- 抽象类描述“是什么”,适合表示同一类事物的共性,子类和它之间是is-a关系。
- 接口描述“能做什么”,适合定义一组能力契约,实现类和它之间是can-do关系。
C卷里有一类简答题,会给你一个业务场景,比如“要设计一个动物园管理系统中鸟类和飞行能力的关系”,让你选择用抽象类还是接口。如果平时只背了“接口可以多实现,抽象类只能单继承”,碰到这种题就会卡住。正确思路是先判断语义:企鹅是鸟类,但不会飞,所以“鸟”适合做成抽象类,“飞行能力”适合做成接口。
再说重载和重写。重载发生在同一个类里,方法名相同、参数列表不同;重写发生在父子类之间,方法签名要完全一致,返回类型可以协变。笔试里容易挖坑的是重载的静态绑定和重写的动态绑定:
class Parent { public void say() { System.out.println("parent"); } } class Child extends Parent { public void say() { System.out.println("child"); } } Parent p = new Child(); p.say(); // 输出 child,这是动态绑定但如果是静态方法,就不存在重写,只有隐藏:
Parent p = new Child(); p.staticSay(); // 调用的是 Parent 的静态方法这类细节,C卷的选择题里很喜欢给一段代码让你猜输出。我当时就吃过亏,后来总结了一句话:只有实例方法才参与动态绑定,静态方法看引用类型,实例方法看实际对象类型。
2.2 运算符、表达式和流程控制:细节决定成败
Java基础类笔试题里,运算符和表达式是性价比最高的得分点,也是最容易因为粗心丢分的点。C卷里出现了不少这类题目,典型的就是自增运算的求值顺序。
比如这段代码:
int i = 0; i = i++; System.out.println(i);输出是0,不是1。原因很简单:i++这个表达式的值是自增前的0,完成运算后,再把0赋值给i,覆盖了自增的结果。如果写成i = ++i,输出才是1。
这种题一出来,很多人会骂“出题人无聊”,但它在真实开发里偶尔就是会坑你一下。我在代码评审里见过有人信手写list.remove(list.size() - 1)然后拿返回值判断,结果因为对表达式的副作用理解不清,除了一堆bug。
还有短路运算符&&和||,也是高频考点。&&左边为false时,右边不会执行;||左边为true时,右边不会执行。这意味着你可以在右边放心写可能抛出异常的代码,但左边的条件一定要写对顺序。
C卷里还出现过三目运算符的类型转换问题:
Object result = true ? new Integer(1) : new Double(2.0); System.out.println(result);这里输出的是1.0,不是1。因为三目运算符的两个分支会尝试做类型统一,Integer会被提升为Double。这种细节单看很偏,但恰恰是“基础扎不扎实”的试金石。
2.3 常用类与枚举:出镜率最高的API
String类几乎是每张卷子必考的重点。C卷里围绕String展开的选择题,无外乎是这几种:
- String、StringBuilder、StringBuffer的区别
- String不可变性的理解
==和equals的区别- 字符串常量池的引用指向
有一道经典变形题,给出一堆字符串拼接代码,问创建了几个对象。要答对这种题,必须理解字符串常量池、编译期优化和运行时拼接的区别。比如String a = "a" + "b" + "c";在编译期会直接优化成"abc",而String b = new String("abc")会额外创建一个堆对象。
我当年就在这种题上栽过,后来形成了一套判断方法:
- 先看字面量是否直接拼接,是则查常量池;
- 再看是否用了
new,是则必有堆对象; - 最后看变量拼接,运行时会产生新的实例。
这种题不是死记结论,而是要在脑子里模拟JVM的字符串处理流程。
枚举类在基础卷里也经常出现。C卷对枚举的考察不算难,但很典型,比如枚举可以有自己的字段和构造方法,可以定义抽象方法,switch支持枚举类型,以及EnumSet和EnumMap的适用场景。
我后来在项目里用枚举替代了很多魔法数字,比如状态机流转、错误码定义。一个体会是:枚举不是简单的常量集合,它能把类型安全和业务行为组合在一起。比如定义订单状态时,可以让每个枚举都带上“能否变更为下一个状态”的逻辑,比外面写一堆if判断干净得多。
2.4 异常处理与数组越界:笔试里的隐藏陷阱
异常机制是Java基础题里比较有区分度的部分。C卷里常考 try-catch-finally 的执行顺序,以及异常被抛出后finally到底跑不跑。
一个很容易搞错的点:
try { return 1; } finally { System.out.println("finally"); }很多人以为有return,finally就不会执行了。实际上,只要程序不是被System.exit(int)强行终止,finally一定会执行。执行时机是在return表达式计算完之后、方法真正返回之前。所以上面这段代码会先打印"finally",再返回1。
还有受检异常和非受检异常的区分。考试时容易混淆的点是:RuntimeException及其子类是非受检的,编译不强制处理;IOException、SQLException这类是受检的,要么throws,要么try-catch。
数组越界属于非受检异常中的IndexOutOfBoundsException,在C卷的选择题里出现频率不低。比较有迷惑性的写法是遍历时用了i <= list.size(),或者对空数组取第一个元素。这种题考的不只是异常类名称,更是你有没有养成防御性编程的习惯:访问数组前先判断下标范围、遍历集合时优先用增强for或迭代器。
我记得有一道简答题问的是:“一个方法捕获取了所有异常,但外层还是出了问题,你会怎么排查?”那类题的答题思路不是写代码,而是讲清楚异常链和日志定位。常见做法是保留原始异常,不要用NEW_EXCEPTION.initCause()覆盖,更不要e.printStackTrace()后吞掉异常。这类习惯,笔试能看出来,面试更能问出来。
2.5 集合与排序算法:基础中的算法影子
Java集合框架是基础卷里的大头,但C卷考得不算深,集中在:
- ArrayList和LinkedList的区别及各自适用场景
- HashMap的底层结构、put和get的大致流程
- HashSet如何实现去重
ArrayList和LinkedList的经典对比,答题时不能只背“数组和链表”。要补充容量扩展、随机访问复杂度、插入删除复杂度,以及内存分布上的差异。真实开发里,List的默认选择几乎都是ArrayList,LinkedList往往只在“频繁头尾插入删除”且需要队列语义时才值得考虑。
HashMap在2017年的考察重点还是“数组加链表”,以及equals和hashCode的约定。有一道题是通过自定义对象做key,问为什么重写了equals还必须重写hashCode。这个逻辑想通了,HashMap和HashSet的去重原理就一起通了:先根据hashCode定位到桶,再用equals比较链表中已有的元素。
排序算法也是笔试编程题的常客。C卷里要么让你手写冒泡排序,要么让你写快速排序,有的还会加一步问时间复杂度。
冒泡排序的写法很简单,但容易在边界条件上出错:
public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } 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; } } }加一个swapped标记就能在数组已经有序时提前退出,这是简单优化,但很多笔试卷子上没写出来。
快速排序手写时最翻车的不是主逻辑,而是递归出口和partition的边界。我建议平时就把快排的写法固定成自己熟悉的版本,考场直接默写,不要现推。
3. 手写代码题的答题策略:怎样在卷面上多拿分
3.1 单例模式:五种写法怎么选
C卷的编程题里出现过要求写单例模式的题目。单例的写法很多,饿汉式、懒汉式、双重检查锁、静态内部类、枚举单例,各有适用场景。
笔试时最怕的不是写不出来,而是写了一版看起来对、实际有问题的代码。经典的反面教材就是双重检查锁没有加volatile。由于指令重排序,线程可能拿到一个“构造到一半”的对象。所以完整的双重检查写法是:
public 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; } }如果题目没有明确要求,我通常直接写静态内部类版本,既懒加载又线程安全,代码也干净:
public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }这种题在阅卷时讲究“踩分点”:构造方法私有化、线程安全、懒加载,看到这几个关键词,分数就稳了。要是只写一个饿汉式,也能拿基础分,但扩展性上会被扣一点。
3.2 冒泡排序和快速排序:手写时最容易翻车的点
排序算法作为校招笔试的常客,C卷的编程题大概率会出现。快速排序虽然理论复杂度更优,但如果平时练得少,很容易在partition算法上卡壳。
我来分享一个我自己习惯用的快速排序写法,左右指针法,思路清晰,不容易乱:
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--; } while (i < j && arr[i] <= pivot) { i++; } if (i < j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } arr[left] = arr[i]; arr[i] = pivot; quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); }注意两个内层while里都要写i < j,否则指针会越界。笔试考场上一紧张,很容易丢掉这个条件。考官看代码时,也会特别关注边界和递归出口,这两个地方写对,即使最后结果有小bug,印象分也会好很多。
3.3 字符串处理题:先想清楚再动笔
C卷还有一类编程题是字符串处理,比如反转字符串、统计字符出现次数、判断回文、去掉重复字符。这类题看着简单,但非常考验基本功。
拿“反转字符串”来说,最简单的方式是用StringBuilder:
String s = "hello"; String reversed = new StringBuilder(s).reverse().toString();但如果题目要求“不借助额外空间”或“手写实现”,就要用双指针。又比如判重,用HashSet能轻松搞定,但需要说清楚为什么它能去重,这又回到hashCode和equals了。
我的建议是,做题前先在草稿纸上写两步:输入是什么,输出是什么;边界条件有哪些。比如字符串为空、只有一个字符、全是重复字符,这些情况要不要处理。把边界想清楚再动笔,写出来的代码会完整很多。
4. 从笔试到面试:Java八股文该怎么背才不砸锅
4.1 高频考点速查表
C卷给我的另一个启发是:校招笔试和面试的考点高度重叠。我后来整理过一张速查表,把高频考点和对应的答题关键词放一起,复习效率很高。
| 考点 | 核心答题关键词 | 容易踩的坑 |
|---|---|---|
| String不可变 | final数组、不可被继承、常量池 | 和StringBuilder混淆 |
| ==与equals | 引用比较 vs 值比较 | 忘记重写equals的影响 |
| HashMap原理 | 数组+链表、扰动函数、扩容 | 认为是线程安全的 |
| 抽象类与接口 | is-a vs can-do | 只背语法,不答设计意图 |
| 异常处理 | finally执行时机、受检/非受检 | 忽略System.exit的特殊性 |
| 数组越界 | 下标从0开始、length边界 | 遍历时使用<= |
| 单例模式 | 私有构造、线程安全、懒加载 | 双重检查锁缺失volatile |
这张表不是我发明的新东西,而是从C卷和后续面试题里提炼出来的“最小复习集”。先保证这些点都能用一两句话说清楚,再往外扩展。
4.2 别当复读机:把概念用自己的话讲清楚
很多同学背八股文,背得很熟,但面试官换个问法就露馅。比如C卷里写“简单介绍HashMap”,你可能背得很顺。但如果面试官问“为什么HashMap的容量要设计成2的次幂”,你如果只背结论,不会推导,就很难接住。
我的办法是,每个概念都强迫自己回答三个层次:
- 是什么:一句话说清定义。
- 为什么:解决什么问题,为什么这么设计。
- 怎么用:日常开发中有哪些场景。
比如HashMap容量设计成2的次幂,是为了让(n - 1) & hash能替代取模运算,同时下标分布更均匀。这样讲出来,面试官能明显感觉到你是真的懂,而不是背的。
4.3 基于2017C卷延伸的学习路线
如果现在让我给准备校招的人划重点,我会建议沿着这套C卷的考点往外延伸:
- 先把面向对象、集合、异常、String这些基础过一遍;
- 再去理解JVM内存模型、类加载机制、GC基础;
- 然后看并发编程,synchronized、volatile、线程池;
- 最后补一点JVM调优和常见工具的使用。
这套路线看起来很长,但主体仍然是当年C卷那些基本面。基础题不会因为年份变了就失去意义,反而会因为框架越来越多,面试官更要靠这些基础题来筛人。
5. 常见问题与避坑经验:校招笔试现场实录
5.1 环境与编译问题
虽然不是C卷本身的内容,但笔试现场很多人挂在环境问题上。遇到过的情况包括:
- 本机Java环境变量没配好,编译器找不到javac;
- 代码在IDE里能跑,放到在线OJ上就报错,大多是没导入包或者主类名不对;
- 内存不足异常,比如
java.lang.OutOfMemoryError,一般出现在堆内存设置过小或代码里无限递归。
我的建议是,笔试前把本地Java环境配好,至少会用命令行编译和运行一个HelloWorld。别嫌简单,真到现场能省下很多时间。代码里不要写依赖特定IDE特性才能编译的语法,尽量用标准Java。
5.2 时间分配和检查清单
C卷题量不算少,如果时间分配不好,编程题容易写不完。我自己的策略是:
- 单选题控制在每题1分钟左右,别纠结;
- 简答题每题控制在5-8分钟,先写核心点,再补细节;
- 编程题至少留出30分钟,先用15分钟理思路、写框架,剩下时间补边界和检查。
还有一个习惯,就是留出最后5分钟整体检查:题目编号有没有填错、类名和方法名有没有拼错、有没有把main写成mian。这些低级错误拿分最容易,丢分也最冤枉。
5.3 细节错误排查:写代码时最容易犯的十个坑
我把C卷和自己平时笔试遇到的坑整理成了一份私人清单:
- 数组下标越界,遍历时用
< length而不是<= length; - 字符串用
==判断内容是否相等; - 不使用泛型,编译后一堆强制类型转换警告;
- try-catch里把异常吞掉,只打了一条printStackTrace;
- 用float或double做金额计算;
- HashMap在遍历时直接remove元素,导致并发修改异常;
- 集合判空直接调
size(),没有判null; - 重写equals但不重写hashCode;
- 在循环里不断拼接String,没考虑性能;
- 写递归时不考虑结束条件,导致栈溢出。
这份清单放到任何一场Java笔试里都实用。很多东西只有在真实项目里踩过坑,才会形成条件反射,所以我会建议新手宁可多写几个极端用例测试,也不要只满足于“能跑就行”。
6. 这套C卷留给我的三个“后遗症”
事到如今,我还会时不时翻翻当年那套C卷的笔记。它留给我的不是具体答案,而是三个“后遗症”。
第一个后遗症是看代码时先找边界条件。无论是自己写还是帮别人评审,我都会下意识问一句:数组会不会是空的?下标会不会越界?这条判断如果放在前面会不会短路?当年那些选择题教会我的不是语法规则,而是对代码缺陷的敏感。
第二个后遗症是喜欢追问“为什么”。很多Java基础概念,表面上一句话能说完,但往深里挖一层,往往能串起一片知识。比如String为什么要设计成不可变,串起来就是线程安全、常量池、hashCode缓存、安全性和性能。这种串知识点的方式,比死记硬背有效得多。
第三个后遗症是面试新人的时候,我总爱从基础题切入。不管简历上写了多少微服务、中间件,我都会先问几道类似C卷的选择题,看看对方能不能把最朴素的Java概念讲清楚。基础可能不是决定一个人天花板的唯一因素,但一定是决定下限的那块木板。
如果你正为校招笔试发愁,不妨也把这类“基础类”题目当成一次技术体检。刷题不是目的,把每一个“为什么”弄明白,才值回票价。