1. 为什么2017年的Java笔试题放到今天仍然值得啃
1.1 一份“老卷”背后的筛选逻辑
2017年秋天,科陆集团面向应届生的Java工程师岗位出了一套笔试题。放在当时看,它和绝大多数互联网公司的校招试卷没有本质区别:选择题考语法细节,问答题考框架理解,编程题考手写代码能力。但如今回头再看这套题,我发现一个很有意思的现象——Java笔试的核心考点,其实已经很多年没有大变过。
科陆的主营业务是智能电表、储能系统和能源物联网,属于典型的“传统制造业+物联网软件”复合型公司。它招Java工程师,并不需要你上来就会写分布式中间件,也不需要你手撕红黑树。它的试卷瞄准的是三件事:基础牢不牢、工程思维有没有、碰上没见过的场景会不会查资料。这也是为什么2017年的试卷放到2025年依然有参考价值——面试官想确认的底层能力没有变。
很多准备校招的同学容易陷入一个误区:拼命追最新技术栈。今天看到Spring Boot 3出就赶紧学,明天看到GraalVM讨论度高又去查,结果最基础的HashMap扩容原理却说不清楚。这份2017年的试卷恰恰反映了校招的底层逻辑:对于初级工程师,公司不指望你什么都会,但要求你学过的都扎实。扎实就意味着,集合、多线程、JVM、SQL这些老生常谈的领域,你必须能扛住“八股文”式的追问。
1.2 这些考点现在依然是面试高频
我把当年试卷涉及的知识点梳理了一下,放到今天的面试场景里逐一对照,发现重合度极高:
| 2017年考点 | 2025年面试热度 | 说明 |
|---|---|---|
| Java基础语法(标识符、运算符、类型转换) | 依然高频 | 选择题和电话面第一轮的最爱 |
| 面向对象三大特性 | 依然高频 | 从概念题变成了“请你手写一个多态场景” |
| 集合框架(ArrayList/HashMap) | 热度更高 | 现在几乎必问HashMap底层 |
| 多线程与synchronized | 热度更高 | 面试深度从“用过”升级到“实现原理” |
| JVM内存模型与GC | 热度持平 | 老牌八股文考点,长盛不衰 |
| 冒泡/快速排序 | 热度略降 | 手撕算法的难度已经升级到树和DP了 |
| SQL与索引 | 热度持平 | 初级岗依然喜欢考 |
| Spring轻量级框架理解 | 热度升高 | 从SSH换成了Spring Boot |
这不是偶然。Java是一门生态极其庞大、但核心语法非常稳定的语言。十年过去了,HashMap的源码逻辑、ConcurrentHashMap的锁粒度设计、JVM的运行时数据区划分,这些底层的“道”没有变。变的是上层框架的“术”。所以,一份2017年的老试卷,反而能帮你剥离掉框架的噪音,聚焦到真正核心的Java知识体系上。
2. Java基础语法:那些“送分题”其实在筛人
2.1 标识符、运算符与类型转换
科陆这套卷子的选择题部分,开头几道基本都是Java语法基础题,比如标识符命名规则、运算符优先级、基本类型之间的隐式转换。看起来像是送分题,但很多人在这一块反而栽跟头。
拿标识符来说,校招里至少有三成的人会在这道题上犹豫。Java标识符的规则就三条:字母、下划线、美元符号开头,后面可以跟数字;不能是关键字;大小写敏感。看起来简单,但题目只要拐个弯,比如问int 123abc = 1;合不合法,或者int true = 0;合不合法,立刻会有人选错。前者错在数字开头,后者错在true是布尔字面量而非关键字——严格来说它不是关键字,但它是保留字,同样不能用作标识符。这种细节,恰恰是笔试想筛掉的“基础不牢”。
运算符和表达式这道题也很有代表性。常见考法:int a = 5; int b = a++ + ++a;,最后输出多少?很多人一看到自增自减就懵。我建议你记住一个判断方法:a++是先使用后自增,++a是先自增后使用。所以a++ + ++a在a=5时,先取a++的值为5,此时a变成6;再取++a的值,a先自增为7,表达式值是7,所以结果是12,a最终是7。这类题没有捷径,只能靠多练形成肌肉记忆。
类型转换也是高频考点。短整型转整型是隐式转换,没问题;但float f = 3.14;这种写法编译不过,因为3.14是double类型,直接赋值给float会精度丢失,必须写成3.14f。类似的还有byte b = 127;合法,但byte b = 128;直接报错,因为超出byte的范围-128~127。笔试卷子不会问“你怎么理解类型转换”,而是直接放一串代码让你判断对错,考察的就是你写代码时是否踩过这些坑。
2.2 异常机制与数组越界:一道题看出你写过多少代码
有一道让我印象深刻的题,配着一段代码:尝试访问长度为5的数组的第10个下标。选项里有ArrayIndexOutOfBoundsException、IndexOutOfBoundsException、NullPointerException,还有“编译错误”。很多新手选了IndexOutOfBoundsException,理由是不管数组还是集合越界,都是父类。但面试官想看到的是你对数组越界异常的精确定位能力。
数组越界在Java里抛的确实是ArrayIndexOutOfBoundsException,它是IndexOutOfBoundsException的子类。如果题目代码操作的是ArrayList,那越界抛出的是IndexOutOfBoundsException。这个区别在笔试里可能就是一道选择题,但在实际开发排错中却直接影响你查问题的速度。日志里如果只写了IndexOutOfBoundsException,你还要再想一下是数组还是List;如果直接是ArrayIndexOutOfBoundsException,你就可以直接去查所有[]访问的地方,排查范围立刻缩小一半。
异常这块还有一个隐藏考点,就是受检异常和非受检异常的区别。试卷里通常会给出一堆异常类,让你归类。IOException、SQLException属于受检异常,编译器强制你捕获或抛出;NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException属于运行时异常,不用强制处理,出问题直接抛。这个知识点难吗?不难。但能准确说是受检还是非受检的人,至少证明他是真的处理过这些异常,而不是只在书上看过。
2.3 枚举类型与常用类:细节题里的老演员
很多人在准备校招时,会把枚举类型当成一个“知道就行”的知识点,结果科陆当年的试卷里偏偏就给了几道关于枚举的题。比如问enum Color { RED, GREEN, BLUE },最后.values()返回的是什么?答案是Color[]数组,而不是List<Color>。这类题考察的是你有没有真正用过枚举。
枚举在Java里不是一个简单的常量集合,它本质上是一个继承自java.lang.Enum的类。所以你可以给枚举添加构造器、字段和方法。最经典的用法是用枚举实现单例,因为Enum类本身保证了线程安全和序列化安全。面试官如果顺着这道选择题追问,经常就是“枚举除了定义常量,还有什么实际应用”。如果你能说出状态机、策略模式配合枚举,甚至单例模式,那这道题就从送分题变成了加分题。
常用类方面,试卷里出现过Integer的缓存问题。Integer a = 127; Integer b = 127; a == b返回什么?答案是true,因为Integer缓存了-128~127。但如果赋值为128,a == b就变成了false,因为超出缓存范围,会各自new一个新的Integer对象。这个考点几乎年年都有,但依然年年有人答错。
还有equals和==的区别。==比较的是引用地址,equals默认也是比较地址,但String和包装类型都重写了equals,所以内容相同就返回true。笔试里经常出这种代码:两个内容相同的字符串,用==比较返回什么,用equals比较返回什么。只要记住“==比地址,equals比内容(前提是重写过)”,这类题就不会错。
2.4 环境变量与编译选项:笔试之外的隐藏“热身题”
说实话,当年我拿到科陆这份卷子的时候,第一页除了选择题,还有一道让我意外的题——关于环境变量配置的。大概是问JAVA_HOME和PATH分别起什么作用。当时我的第一反应是:“这不是笔试应该考的吧?”但后来在做Java开发的过程中,我越来越理解出题人的用意。
JAVA_HOME是给构建工具和IDE用的,它指向JDK的安装目录;PATH是让操作系统在命令行里能找到java和javac。如果你这两者都配置不好,说明你根本没法在命令行环境下编译运行Java程序,更别提部署服务器上的Java应用了。现在的笔试虽然很少直接考这些,但面试时偶尔会冒出一句“你遇到过Error: Could not find or load main class吗”,这就是在考察你对类路径和JVM启动机制的理解。
还记得热词里有一条“java: 警告: 源发行版 17 需要目标发行版 17”吗?这个问题在今天的IDE里太常见了。本质是编译器的--source和--target版本不匹配,或者IDE的Java版本和项目配置的JDK版本不一致。解决办法有两种:一是把项目pom.xml里的maven.compiler.source和maven.compiler.target调到和本机JDK一致;二是在IDE里统一Project Structure中的SDK和Language Level。如果你在笔试现场被问到这类问题,最安全的回答思路是:“先检查JDK版本和IDE配置,再检查Maven或Gradle的编译参数,最后确认环境变量JAVA_HOME指向的是不是同一个JDK版本。”这份排查思路比死记硬背答案更有价值,因为它背后是“编译链路”的意识。
3. 面向对象:从概念填空到代码实现的转换
3.1 封装、继承、多态,笔试里到底怎么考
科陆这份试卷里,面向对象部分占的比例不低。这类题最经典的考法是:给你一段代码,里面有继承关系,有方法重写,然后问你输出什么。比如定义一个父类Animal,一个子类Dog,运行时调用new Dog().makeSound(),问你底层是怎么找到子类方法的。
这里真正想考的其实是动态绑定。Java在方法调用时,如果方法是实例方法,JVM会在运行时根据对象的实际类型来决定调用哪个方法,而不是根据引用类型。所以Animal a = new Dog(); a.makeSound();只要Dog重写了makeSound(),调用的就一定是Dog的实现。这个知识点不难,但如果你只背了“方法重写是运行时多态”这句话,没有真正理解对象头里的类型指针,一旦题目换成“父类引用调用子类新增的方法会怎样”,你照样会卡住。答案是编译直接报错,因为编译器只认引用类型声明的方法。
封装这个概念,笔试很少让你解释定义,而是让你分析访问修饰符。public、protected、default、private的可见范围是必须背下来的。这里有个大家容易忽略的点:protected不只是同包和子类可访问。在不同包下,只有通过继承关系才能访问父类的protected成员,直接通过父类引用访问是不行的。这个细节经常出现在选择题的陷阱里。
3.2 String与字符串常量池:笔试必争之地
面向对象部分里,String类的题目是绝对的主角。科陆当年的试卷里有一道题,问String s1 = "abc"; String s2 = new String("abc");创建了几个对象。答案是两个——字符串常量池里的"abc",以及堆内存中new出来的那个String对象。但这个答案有一个前提:常量池里之前没有"abc"。如果之前已经出现过"abc",那么new String("abc")就只创建一个堆对象。
还有个更绕的考点:String s3 = "a" + "b" + "c";创建了几个对象?很多人以为每次+都会创建一个新对象,所以是三个。但Java编译器编译时做了常量折叠,"a" + "b" + "c"会在编译期直接变成"abc",所以如果常量池里没有"abc",那就只创建一个对象。但如果+两边有变量,比如String a = "a"; String b = a + "b";,此时a + "b"会在运行时通过StringBuilder拼接,相当于创建了一个StringBuilder对象、一个String对象,以及拼接时使用的字符数组。也就是说,变量参与拼接时,==比较的结果很可能是false,因为不能用常量池判断。这类题考得极多,建议你把《深入理解Java虚拟机》里字符串常量池那节反复看几遍,再配合几道练习题,才能真正掌握。
StringBuilder和StringBuffer的对比也是必考。StringBuffer的方法是synchronized修饰的,线程安全但性能差;StringBuilder非线程安全但性能好。笔试喜欢问“在单线程环境下的字符串拼接,应该选哪个”,答案是StringBuilder。同时,在循环里拼接字符串尤其要注意,str += "x"在循环里会不断创建新对象,性能极差,正确写法是用StringBuilder.append()。你把这个写成代码题,比如“用StringBuilder实现字符串反转”,就是一道德州扑克式的送分题。
4. 集合框架的考点地图:从ArrayList到HashMap
4.1 先搭一遍整体框架
集合框架是Java笔试的重头戏,科陆这份试卷也花了大量篇幅在集合上。我建议你复习的时候,先从整体框架入手,不要一上来就抠HashMap源码。Java集合大致分为两大阵营:Collection和Map。Collection下面有List、Set、Queue,Map是一个独立体系。
围绕这套框架,最基础也是最容易扣分的题是:哪些集合允许重复元素、哪些允许null、哪些是有序的。ArrayList允许重复且允许null,HashSet不允许重复但允许null,HashMap的key和value都允许null,但Hashtable和ConcurrentHashMap不允许key或value为null。TreeSet和TreeMap不允许null,因为它们在插入时就要排序。这些规则不需要背表格,理解每种结构的底层数据结构后自然就记住了。例如HashMap允许null是因为它有专门把null映射到0号桶的逻辑,ConcurrentHashMap不允许null是为了避免并发下的二义性问题。
4.2 ArrayList与LinkedList:不只是“数组和链表”的区别
有一道经典送分题:ArrayList和LinkedList的区别是什么?标准答案是ArrayList底层是数组,查询快、增删慢;LinkedList底层是双向链表,增删快、查询慢。这个答案没问题,但只答到这个程度,分数只能算及格。如果我是阅卷人,我更希望看到下面这批补充:
ArrayList的初始容量是10,每次扩容为原来的1.5倍(oldCapacity + (oldCapacity >> 1)),扩容需要Arrays.copyOf,是一次O(n)的数组复制。LinkedList的增删快是分位置的。如果从中间插入,它确实要遍历到指定位置,复杂度是O(n),但插入本身是O(1)。而ArrayList中间插入的代价不仅是移动元素,还可能要触发扩容。LinkedList实现了Deque接口,所以它可以当队列和双端队列用。这一点笔试很少直接考,但面试的时候可以主动提,证明你对接口体系有概念。
还有一个容易忽视的点:Vector。现在很多人根本没写过Vector,但笔试如果考“Vector和ArrayList的区别”,你至少要知道Vector是线程安全的(方法用synchronized修饰),性能比ArrayList差,扩容是翻倍。但这类老容器在实际开发中已经很少有人用了,优先用Collections.synchronizedList或CopyOnWriteArrayList。
4.3 HashMap的底层原理:从2017年到今天的核心考题
如果说集合框架里只能复习一个点,那一定是HashMap。2017年这份卷子里有关于HashMap的题,我记得很清楚:问的是为什么HashMap是非线程安全的。放在当时,能答出JDK 1.7中并发扩容可能形成环形链表导致死循环的人很少;能进一步答出JDK 1.8改为尾插法解决了链表死循环问题,但仍可能丢数据的人更少。
如果你的目标是现在的Java面试,HashMap的追问深度至少要覆盖到以下几条,这基本是当前互联网公司面试的标准链路:
HashMap底层数据结构是什么?JDK 1.8后是“数组+链表+红黑树”。链表长度超过8且数组长度大于等于64时,链表转红黑树。- 为什么链表长度为8转红黑树?官方注释里给了泊松分布的概率计算,负载因子0.75、长度8的情况下,链表长度达到8的概率低到千万分之一。
- 为什么数组长度必须是2的幂?因为计算下标用的
(n - 1) & hash等价于hash % n,但位运算更快,同时要求n是2的幂才能让结果均匀分布。 - 为什么负载因子默认是0.75?这是空间和时间的一个折衷,过大容易导致哈希碰撞增加,过小则浪费空间。
- 扩容流程是什么?先判断是否需要扩容,再创建新数组,然后重新计算每个节点的下标位置。JDK 1.8针对树的拆分和链表的拆分有不同的逻辑。
笔试不太可能让你把源码全写出来,但一定会用选择题考其中一两个点。我当时准备时,是把put()方法和resize()方法的JDK 1.8源码完整读了三遍,然后把每个分支对应到一道题上,比如“map.put(1, "a")执行时,如果数组下标位置为空,会发生什么”。答案是直接new Node并插入。看似简单,但这就是源码阅读能力的体现。
4.4 Comparable与Comparator:排序题的隐藏考点
热词里有java comparator.comparing 将某元素值放第一个,这其实对应了Java排序的一个高频考法。笔试题经常这样出:有一个Student类,有name和age字段,让你先按年龄升序,如果年龄相同按姓名降序,写出排序代码。
使用Comparable的话,让Student实现compareTo方法;使用Comparator的话,用匿名内部类或Lambda表达式。注意Lambda写法的优先级:Comparator.comparing(Student::getAge).thenComparing(Student::getName, Comparator.reverseOrder())。或者用Comparator.comparing(Student::getAge).thenComparing(Student::getName, (a, b) -> b.compareTo(a))。
如果你要用Comparator.comparing把某个元素排到第一位,比如把年龄为0的元素排到最前面,可以这样写:
Comparator<Student> comparator = Comparator.comparingInt(s -> s.getAge() == 0 ? 0 : 1);这个写法的核心思路是通过映射函数把元素分成“特殊组”和“普通组”,特殊组排在前面。很多人在面试时喜欢炫这种写法,但在笔试里我建议你老老实实写清楚Comparator的实现逻辑,避免阅卷人一眼看不出你的意图。
5. 多线程与并发:区分初级与中级的分水岭
5.1 synchronized与Lock:从“用过”到“实现原理”
科陆2017年的试卷里,多线程题主要停留在synchronized关键字的使用层面。比如问synchronized加在静态方法和实例方法上的区别,前者锁的是Class对象,后者锁的是当前实例对象。这个知识点即便到了今天依然是基础中的基础。
但要准备现在的面试,你需要深入一层。synchronized在JDK 1.6之后经过了大量优化,锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁。锁会根据竞争情况从偏向锁逐渐升级到重量级锁,但一般不会降级。面试官如果问你“synchronized和Lock的区别”,你可以从这几个角度展开:
synchronized是关键字,自动释放锁;Lock是接口,必须手动unlock,通常放在finally里。synchronized在JDK 1.6后性能已经和Lock相差无几,但Lock支持可中断、可超时、可公平锁,功能更丰富。synchronized不可中断,Lock.lockInterruptibly()可以响应急中断。ReentrantLock默认非公平锁,可以通过构造器参数改为公平锁。但公平锁性能较差,因为线程切换更频繁。
笔试不会让你写一个完整的并发程序,但可能会让你判断一段代码是否存在线程安全问题。如果你能看到自增操作count++那道题,必须答出这一步不是原子操作,它分为读取、加1、写回三步,在并发环境下会丢失更新。正确的做法是使用AtomicInteger或者ReentrantLock,或者简单粗暴加synchronized。
5.2 volatile:可见性问题的一道必答题
volatile几乎是多线程题里每卷必有的考点。最经典的例子是双端检锁的单例模式:
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }为什么instance要加volatile?因为new Singleton()在字节码层面不是一个原子操作,大致分为三步:分配内存、执行构造方法初始化、将引用指向内存地址。在没有volatile的情况下,JVM可能进行指令重排序,导致第二个线程拿到一个“已经分配内存但还没执行构造方法”的对象,从而在使用时出错。volatile禁用了重排序,保证了“先初始化再赋值”的语义。
从笔试答题角度,volatile还有两个特性要背熟:保证可见性、不保证原子性。volatile让变量每次读取都从主内存中读,每次修改都立即刷新到主内存,从而解决了多线程下的可见性问题。但它解决不了复合操作的原子性问题,比如count++。凡是答“volatile能保证线程安全”的,都会被扣分。
5.3 线程池:面试官最喜欢“顺手一问”的点
科陆当年的试卷里有没有线程池的题,我不太记得清了,但即便没有,这个点也是Java工程师面试绕不开的。笔试如果考,通常就是问线程池的几个核心参数:核心线程数、最大线程数、空闲存活时间、工作队列、线程工厂、拒绝策略。
我推荐你记一个标准答案模板,方便笔试简答题直接套用:线程池的核心线程数corePoolSize表示即使线程空闲也会保留的线程数量;maximumPoolSize表示线程池允许的最大线程数;keepAliveTime表示非核心线程空闲多久后被回收;workQueue用于存放等待执行的任务;RejectedExecutionHandler定义任务太多无法执行时的处理策略,常见的四种策略是AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最旧任务)。
如果你看到“线程池为什么不能直接使用Executors.newFixedThreadPool()”这类题,原因是Executors创建的固定线程池使用的是无界LinkedBlockingQueue,高峰期任务可能越积越多,最终导致OOM;Executors.newCachedThreadPool()可创建的线程数上限是Integer.MAX_VALUE,也可能OOM。所以生产环境推荐手动通过ThreadPoolExecutor构造函数创建线程池,并指定有界队列和拒绝策略。
6. JVM内存与调优:OOM类题目的正确打开方式
6.1 运行时数据区域的划分
JVM相关题目,在科陆这套2017年的试卷里占的篇幅不算多,但必考。热词里有个java: outofmemoryerror: insufficient memory,这其实是很多人跑项目时不理解堆内存日志导致的问题。笔试的经典考法是让你写出JVM运行时数据区有哪些部分,以及每部分的作用。
标准答案是:程序计数器、虚拟机栈、本地方法栈、堆、方法区。其中程序计数器是私有区域,存放当前线程正在执行的字节码指令地址;虚拟机栈存放栈帧,每个方法调用对应一个栈帧入栈和出栈;堆是最大的内存区域,存放对象实例;方法区存放类的元数据、静态变量、常量池(JDK 1.8后方法区由元空间实现,不再用永久代)。
这里有一个经常被问到的细节:StackOverflowError什么时候抛?当递归调用的层数太深,导致虚拟机栈无法分配新的栈帧时抛出。OutOfMemoryError什么时候抛?当堆内存无法为新对象分配空间时抛出,常见于大对象过多或内存泄漏。
6.2 垃圾回收:从标记算法到经典收集器
垃圾回收算法是笔试的另一个高频点。你需要答出三个基本算法:标记-清除、复制、标记-整理。然后说清楚它们在新生代和老年代分别怎么用。
新生代采用复制算法,因为新生代对象大部分“朝生夕灭”,存活率低。Eden区和两个Survivor区的比例默认是8:1:1,每次Minor GC后存活对象复制到Survivor区,到了一定的年龄阈值(默认15)就晋升到老年代。老年代对象存活率高,不适合复制算法,因此采用标记-清除或标记-整理。
笔试如果能答到CMS和G1的区别,基本就是加分项了。G1引入了Region的概念,把堆划分成多个大小相等的Region,可以预测停顿时间,JDK 9之后是默认垃圾回收器。2017年的试卷大概率还在问Serial、Parallel和CMS,但今天你最好把G1和ZGC也了解一下。
6.3 面对OOM报错,应该怎么排查
面试题经常给一个场景:系统运行一段时间后抛出OutOfMemoryError: Java heap space,问你如何排查。这题属于典型的过程题,只看你有没有真正的排障经验。
第一步,启动时加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof,这样OOM发生时自动导出堆快照。第二步,用jmap -dump:format=b,file=heap.hprof <pid>手动导出当前堆快照。第三步,用MAT或VisualVM分析工具打开hprof文件,看内存中被哪些对象占满,特别关注BigDecimal、byte[]、char[]这类大对象。第四步,在代码层面排查是否存在未关闭的连接、静态集合不断增长、ThreadLocal没有清理等问题。
笔试没有条件让你实操,但你可以把步骤写出来,然后给一个判断标准:如果内存快照里同类型对象有海量实例,基本可以判定为内存泄漏;如果没有,只是整体内存占用过高,那就是内存不足,需要调大堆内存或优化对象结构。这条把“内存泄漏”和“内存溢出”区分开的回答,能在阅卷时给你提分。
7. 编程题:算法不是难点,规范才是
7.1 冒泡排序:不丢分也不是靠硬背
科陆这套试卷的编程题,结合热词来看,冒泡排序和快速排序是重点。编程题在笔试阅卷时,一般三步走:对不对、优不优、规不规范。所以哪怕你会写,也要注意代码风格。
冒泡排序的典型写法:
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 temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) { break; } } }这里的优化有两个:一是每一轮排序后,最后一位已经是最大值,所以内层循环范围不断缩小;二是用一个swapped标志位,如果某一轮没有任何交换,说明数组已经有序,提前退出。这个优化能体现你对算法复杂度的理解,而不是只会背模板。
复杂度方面,最坏和平均时间复杂度都是O(n²),最好情况下(数组已经有序)经过优化后是O(n)。空间复杂度是O(1)。稳定排序,因为相邻元素交换不会改变相等元素的相对顺序。笔试只要问“冒泡排序是否稳定”,你就答“稳定”,理由是只有>才交换,遇到等于不交换。
7.2 快速排序:边界条件决定成败
快速排序在笔试手写题里的地位比冒泡更高,因为面试官能从你写的边界条件看出你对递归和指针的理解。我建议你背熟下面这个版本:
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 pivot = arr[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; }这个版本采用Lomuto分区方案,逻辑简单,笔试中不容易写错。关键点在递归的终止条件left >= right。很多人在笔试时忘了写这个,导致无限递归栈溢出。另一个容易出错的地方是quickSort(arr, left, pivot - 1),这里必须是pivot - 1,因为pivot已经落在正确的位置上了,再排它会死循环。
快速排序的时间复杂度平均是O(n log n),最坏情况下(几乎有序时选到最值做pivot)退化到O(n²)。空间复杂度是递归栈的深度,平均O(log n),最坏O(n)。不稳定排序。笔试把这些写清楚,编程题分数基本能拿全。
7.3 编程题的答题规范:别让阅卷人猜你的代码
编程题哪怕写对了,代码不规范也很吃亏。我总结了几条校招笔试的码风建议:
- 变量命名要有意义。不要写
int a = 0,要写int count = 0。 - 括号对齐。考试系统没有IDE的自动格式化,自己写的时候每行缩进固定为4个空格。
- 把关键步骤写注释。比如冒泡排序里写一句
// 本次循环是否发生交换,若未发生则数组已有序,能让阅卷人一眼看出你的思路。 - 优先用标准库。如果题目没限制不能使用API,排序直接用
Arrays.sort(),但前提是你要在注释里说明Arrays.sort()底层使用的是DualPivotQuicksort或TimSort,复杂度是多少,这比强行手写一个不稳定的算法更能拿分。 - 不要留空。即使不会,也要写下暴力法或解题思路,写“先遍历,再用HashMap记录出现次数”也算分。
8. 数据库与框架:初级Java工程师的另一半技能
8.1 SQL基础与索引的几种考法
科陆这套试卷还涉及不少数据库题目,这是Java后端笔试的标配。虽然试卷没有明确给出SQL语句,但高频考点无非是三块:SELECT查询语法、多表连接、索引。
多表连接里有三个最容易混淆的点:INNER JOIN、LEFT JOIN、RIGHT JOIN。笔试最常见的形式是给你两张表的数据,问LEFT JOIN之后的结果有多少行。你只要记住:LEFT JOIN会保留左表的所有行,右表没有匹配的记录就用NULL填充。这个规则听起来简单,但我当年考试时因为没注意“左表有一行在右表没有匹配”,结果少算了一行。
索引相关的题,我会从概念题和场景题两个方向准备。概念题问索引的数据结构,答案是B+树,并说明为什么用B+树而不是B树:B+树所有数据都存在叶子节点,且叶子节点之间有链表指针,范围查询只需遍历链表,效率远高于B树。场景题给你一条SQL,比如SELECT * FROM user WHERE age > 20 AND name = '张三',问你如何建索引。正确答案是(name, age)联合索引,原则是“等值查询的列放前面,范围查询的列放后面”。
8.2 Spring与MyBatis:框架题不能只写“用过”
2017年的Java笔试卷,框架部分大概率还是围绕SSM来出题,也就是Spring、Spring MVC和MyBatis。虽然现在很多公司已经转向Spring Boot,但Spring的核心思想没有变。
如果笔试问Spring两大特性,记得回答IoC(控制反转)和AOP(面向切面编程)。IoC是容器帮助我们管理对象的创建和依赖关系,AOP是把日志、事务、权限等横切逻辑从业务代码中剥离出来。后面可以紧跟一个例子:声明式事务就是AOP在Spring中的典型应用,当方法上标注@Transactional时,Spring会通过代理在方法执行前开启事务,执行后提交或回滚。
MyBatis部分的经典题目是#{}和${}的区别。#{}是预编译占位符,会生成PreparedStatement的?,可以有效防止SQL注入;${}是字符串拼接,直接替换SQL语句,存在注入风险。笔试如果问你“排序字段要动态传入,应该用#{}还是${}”,答案是${},因为排序字段不能作为参数值预编译,但必须做白名单校验,防止传入非法字段名。这个细节能体现你真实写过MyBatis动态SQL。
9. 我当时是怎么复习这类试卷的:一份实操复盘
9.1 三轮复习法:不要一上来就刷真题
针对科陆这套试卷以及同类校招Java笔试题,我复习的思路比较简单粗暴,一共分三轮。第一轮是系统过知识点,用的资料是《Java编程思想》里集合、异常、并发相关的章节,加上一份网上整理的很完整的Java面试题汇总。这一轮的目的是把所有可能考到的考点过一遍,不懂的地方先记录,不深究。第二轮开始做真题和模拟题,把错的题整理成错题集,重点关注反复出错的类型,比如字符串比较、集合扩容。第三轮是模拟实战,给自己限时90分钟,按照真实笔试的感觉做一整套卷子,然后集中复盘。
这个三轮复习法的核心逻辑是“先建立知识体系,再通过刷题找漏洞,最后通过模拟训练考试节奏”。很多人备考时会犯一个错误:一上来就狂刷题,刷到哪算哪,结果知识点之间是断裂的,考试时碰到一道融合了集合和并发的综合题,就不知道从哪下手了。
9.2 笔试时间分配:90分钟的试卷,编程题要留45分钟
科陆这种试卷,一般题量在30道左右,选择题和简答题占一半,编程题两三道。我的建议是:选择题控制在20分钟内做完,不会的立刻标记跳过,不要在一个语法细节上纠结。简答题控制在25分钟内,每题写三到五行,关键点写全即可。编程题至少留45分钟,因为编程题不仅要写代码,还要留时间检查边界条件。
选择题如果遇到完全不会的题,我的策略是“先排除最不可能的选项”。比如题干问“哪个集合是线程安全的”,先看有没有CopyOnWriteArrayList,如果有,大概率是它;如果没有,再在Vector和Hashtable里选。这种排除法虽然不能保证全对,但能显著提高蒙对的概率。
9.3 给备考者的一点真实建议
写了这么多,最后说几句实在的。一份2017年的试卷,放到今天看,考察的知识点覆盖率依然很高,因为Java工程师的核心知识栈不像前端框架那样日新月异。如果你正在准备校招,不必被网上铺天盖地的新框架名词吓到,先把集合、多线程、JVM、SQL这四个大件吃透,再拿几套历年真题练手,效果比盲目追热点好得多。
我当时把这份卷子里每一道错题都做了拓展,比如错了HashMap非线程安全的题,就顺手把ConcurrentHashMap的JDK 1.7分段锁和JDK 1.8 CAS+Synchronized的实现区别也整理了;错了String创建的题,就顺手把String常量池、intern()方法、字符串变量拼接的字节码全部过了一遍。这套“由一道题带出一个知识块”的复盘方法,我受用至今。面试官最看重的不是你背了多少答案,而是你能不能举一反三——这一点,无论在2017年还是现在,都从未变过。