
每年到了三四月份各种校招Java笔试就像雪花一样飘过来。我身边不少准备跳槽的同事也经常拿往年的真题练手。有赞这份2019年的校招Java笔试A卷虽然时间过去几年了但每次拿出来看都觉得它把校招Java该考的东西考得非常“正”——不偏不怪全是基本功而且很多题目放到现在依然是面试高频题。今天我就以这份试卷为线索把Java校招笔试里那些绕不开的考点、解题思路和容易踩的坑从头到尾梳理一遍。这份试卷适合两类人看一类是准备参加校招的应届生想系统过一遍Java笔试的知识点另一类是工作两三年想查漏补缺的开发者看看基础有没有真正吃透。我会按笔试的实际题型分布来讲遇到代码题直接给可运行的范例遇到理论题帮你把背后的原理拆明白争取你看完不只是会做题而是真的懂为什么。1. 笔试整体风格与考点分布1.1 有赞Java笔试的命题思路有赞是做电商SaaS的这类公司的笔试一般不会出特别偏门的题但会很注重考察候选人对Java基础的理解深度和代码实现能力。A卷的整体风格给我的感觉是选择题考概念辨析简答题考原理表述编程题考基本功和边界处理。没有那种“面试官自己都没搞懂的八股题”也没有特别刁钻的智力题整体偏向实用。这里有个很重要的点很多同学以为笔试就是刷题刷得越多越好。实际上像有赞这类公司笔试更看重的是你能否用Java准确表达一个逻辑以及你对集合、并发、JVM这些核心知识的理解是否到位。你会发现试卷里出现的考点和网上流传的“Java面试八股文”高度重合但它的问法更灵活比如会给你一段代码让你判断输出结果而不是直接问“HashMap的底层原理是什么”。所以平时的积累一定要做到能举一反三。1.2 核心考点权重分析根据我对A卷的回忆和同类真题的横向对比大致可以把考点分成这么几块每块的比重大概是考点模块预估占比典型题型Java语法与面向对象30%选择题、简答题集合框架与泛型15%选择题、读代码题JVM与内存管理10%选择题、简答题多线程与并发10%简答题、代码题数据结构与算法20%编程题数据库与框架10%简答题、选择题场景设计与其他5%开放题从这个权重也能看出来Java基本功和算法是笔试的两条大腿数据库和框架虽然占比不高但属于拉开差距的部分。后面我就按这个顺序把每个模块的核心考点和解题套路详细展开。2. Java核心语法与面向对象题解析2.1 面向对象设计原则不只是背定义笔试里关于面向对象的题表面看是考“封装、继承、多态”的定义但实际阅卷时面试官更看重你能不能结合场景说清楚。我在实际开发中的理解是封装就是把变化隔离在内部对外只暴露稳定的接口继承要慎用因为继承打破了封装子类对父类的实现细节有强依赖多态是面向对象最精彩的部分它让代码可以针对抽象编程而不是针对具体实现编程。例如一道常见的改编题设计一个商品促销系统有满减、折扣、秒杀三种活动。如果只让你用if-else写确实很快但后续每加一种活动就要改一次核心逻辑。正确姿势是定义一个PromotionStrategy接口三种活动各自实现再用一个工厂根据类型返回对应策略。笔试中遇到这种题最好是画出简单的类图然后写出核心接口和实现类的骨架代码这比空谈“面向对象六大原则”得分高得多。2.2 重载与重写最容易被绕进去的送分题重载和重写的区别几乎是Java笔试的必考题但很多人只是背了概念落在题目上就翻车。我见过最多的错误是认为“重载是根据返回值类型区分的”这是错的。JVM在编译阶段就能确定调用哪个重载方法只看方法名和参数列表和返回值无关。而重写要求子类方法的签名必须和父类完全一致返回值可以是父类方法返回值的子类型访问修饰符不能比父类更严格也不能抛出比父类更宽的受检异常。实际笔试里这道题通常会以“写出以下代码的输出结果”的形式出现class Parent { public void show(String s) { System.out.println(Parent String); } public void show(Object o) { System.out.println(Parent Object); } } class Child extends Parent { public void show(String s) { System.out.println(Child String); } } public class Test { public static void main(String[] args) { Parent p new Child(); p.show(hello); p.show(new Object()); } }结果分别是Child String和Parent Object。这里考了两个点第一个是重写p的静态类型是Parent但实际对象是Child调用show(String)时动态绑定到子类第二个是重载show(Object)在子类没有被重写所以走父类版本。这类题只要记住“编译看左边运行看右边”基本就不会错。要注意的是如果子类定义的是show(Integer)那p.show(hello)还是走父类的show(String)因为这个新方法只是重载不会覆盖父类的同名方法。2.3 equals与hashCode为什么必须一起重写这道题在笔试里的出现率接近100%。很多同学知道重写equals必须重写hashCode但说不清为什么。我用一个生活例子解释hashCode就像图书馆里一本书的编号equals是确认两本书内容是否完全一致。你去图书馆找书第一步是按照编号定位到书架第二步才是逐本比对。如果两个对象在逻辑上是相等的equals返回true那它们的编号hashCode也必须一样否则就违背了“同一本书放在同一个位置”的约定。更具体地说HashMap在put的时候先算key的hashCode定位到桶再在桶里用equals找有没有相同的key。如果你只重写equals不重写hashCode两个equals为true的对象可能被放到不同的桶里导致HashMap里出现两个逻辑上相同的key这就是严重bug。反过来只重写hashCode不重写equals会导致hashCode相同但equals不同的对象被放进同一个桶退化成链表性能暴跌。笔试中如果让你手写一个User类的equals和hashCode建议这样写Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return age user.age Objects.equals(name, user.name); } Override public int hashCode() { return Objects.hash(name, age); }这里有个加分项如果你用getClass()而不是instanceof来判断类型说明你考虑到了对称性是一个很严谨的习惯。2.4 String不可变与字符串常量池String的不可变性是Java笔试里的常客经常和字符串常量池结合来考。String类用final修饰内部用private final char value[]存储字符所以一旦创建就无法修改。这个设计有几个好处一是安全String经常被用作参数、类名、文件路径不可变可以防止被篡改二是线程安全不可变对象天然可以在多线程环境下共享三是可以做缓存这就是字符串常量池存在的基础。笔试里常见的输出题String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true都指向常量池里的同一个对象 System.out.println(s1 s3); // falses3指向堆里的新对象 System.out.println(s1.equals(s3)); // true内容相同很多人会混淆和equals这里再强调一次比较的是引用地址equals比较的是内容。还有一个高频进阶题String s4 a b c; String s5 abc; System.out.println(s4 s5); // true原因是在编译期常量字符串的拼接会被编译器直接优化为一个常量abc。但如果是这样String a a; String b a b; String s6 ab; System.out.println(b s6); // false变量参与的拼接在编译期无法确定结果运行时实际是new StringBuilder(a).append(b).toString()生成的是新对象。这个考点在笔试选择题里非常经典理解了编译期优化和运行期拼接的区别基本就不会再错。3. 集合框架与JVM考点精讲3.1 HashMap的底层原理与扩容机制如果说Java笔试有一个题必考那一定是HashMap。有赞A卷里对HashMap的考查属于“知其然更要知其所以然”的级别不会只问你底层是数组加链表而是会追问为什么链表长度超过8要转红黑树为什么扩容是2倍为什么负载因子是0.75我先说结论HashMap底层是数组加链表加红黑树。put的时候先对key的hashCode做一次扰动计算然后通过(n - 1) hash定位到数组下标。如果这个位置已经有元素就头插或尾插进链表或红黑树。当链表长度超过阈值8并且数组长度大于等于64时链表转为红黑树目的是把查找时间从O(n)降到O(log n)。当数组中的元素数量超过容量 * 负载因子时触发扩容容量变为原来的2倍元素重新散列到新数组。为什么容量要是2的幂次因为hash % length取模运算在length是2的幂次时等价于hash (length - 1)位运算比取模快得多而且这样散列更均匀。负载因子为什么是0.75这是时间复杂度和空间复杂度之间的折中。负载因子太高比如1.0空间利用率是上去了但碰撞概率增加链表变长查询效率下降负载因子太低比如0.5碰撞是少了但浪费空间频繁扩容也消耗性能。0.75是官方经过大量测试得到的均衡点。面试中还会追一个问题HashMap不是线程安全的多线程环境下会怎样JDK 1.7的头插法在并发扩容时可能出现环形链表导致get死循环这是非常著名的线上事故原因。JDK 1.8改成了尾插法解决了死循环问题但并发put仍可能丢数据。所以多线程场景请用ConcurrentHashMap。3.2 ConcurrentHashMap的线程安全实现ConcurrentHashMap是并发笔试的高频题很多人的理解停留在“分段锁”这个老概念上。实际上JDK 1.8的ConcurrentHashMap完全抛弃了分段锁改用CAS加synchronized。具体来说put时先计算key的哈希定位到桶如果桶为空就用CAS直接插入不需要加锁如果桶不为空就对桶头节点加synchronized锁然后再插入。锁的粒度从Segment级别降到了桶级别并发度大大提升。这个演进思路很值得在笔试里展开讲讲因为面试官想听的是你对并发性能演进的思考。JDK 1.7的分段锁虽然也提高了并发度但Segment数量固定扩容时要锁住整个Segment效率一般。JDK 1.8的粒度更细而且使用CAS乐观锁避免了线程阻塞的开销。需要注意的是size()方法在高并发下很难精确所以ConcurrentHashMap的size()只能保证弱一致性这在很多业务场景下是可以接受的。3.3 JVM内存区域与对象创建过程JVM相关的题有赞A卷里占比不高但几乎每年都会考一个选择题或简答题集中在内存模型和GC算法上。JVM运行时数据区分为五块程序计数器、虚拟机栈、本地方法栈、堆、方法区。其中程序计数器、虚拟机栈、本地方法栈是线程私有的堆和方法区是线程共享的。这样的设计很好理解线程私有区域随线程创建和销毁栈帧随方法调用压栈出栈生命周期很清晰而堆里放着所有对象实例需要GC统一管理。关于对象创建过程笔试可能让你按顺序排列类加载检查、分配内存、初始化零值、设置对象头、执行构造方法。我建议你把对象头里的Mark Word也提一下它记录了对象的哈希码、GC分代年龄、锁状态标志等信息是synchronized锁升级的基础。能把这些串起来讲说明你对JVM的理解是系统性的而不是背了几个名词。3.4 OOM场景分析与参数配置热词里出现了java: outofmemoryerror这正好是JVM专题的高频考点。笔试和面试一般会问有哪些常见的OOM场景怎么排查怎么通过参数控制常见的OOM有四类。第一是Java堆溢出错误信息是java.lang.OutOfMemoryError: Java heap space通常因为对象太多或太大或者存在内存泄漏。第二是虚拟机栈溢出错误信息是java.lang.StackOverflowError最常见的原因是递归没有终止条件。第三是方法区或元空间溢出错误信息是java.lang.OutOfMemoryError: Metaspace常见于加载了太多的类。第四是直接内存溢出错误信息是java.lang.OutOfMemoryError: Direct buffer memory常见于NIO操作中使用了DirectByteBuffer但没释放。排查OOM有一个我很推荐的实操路径先用jmap -heap看堆内存使用情况再jmap -dump导出堆转储文件然后用MAT或VisualVM分析大对象和引用链定位到具体的业务代码。如果线上问题紧急可以先用jstat -gcutil看GC频繁度配合jstack看线程状态基本能判断是内存泄漏还是内存不足。笔试里如果让你写一个堆溢出的Demo最简单的写法是这样的Listbyte[] list new ArrayList(); while (true) { list.add(new byte[1024 * 1024]); }这里有个技巧可以把-Xmx设置小一点比如-Xmx20m这样溢出更快。能顺手写出启动参数和运行效果说明你是真的动手做过实验的而不是光背概念。3.5 垃圾回收算法与分代收集JVM选择题里GC算法也是常客。基础的五种引用计数法、标记清除、复制算法、标记整理、分代收集。引用计数法现在基本不用了因为解决不了循环引用问题标记清除算法有碎片化问题复制算法没有碎片但浪费一半空间标记整理算法没有碎片但移动对象开销大。实际商用JVM用的是分代收集结合了前面几种算法的优点。新生代对象存活率低用复制算法分为Eden和两个Survivor区比例是8:1:1。Minor GC时把存活对象从Eden复制到Survivor经过多次GC仍然存活的对象晋升到老年代。老年代对象存活率高用标记清除或标记整理。这个设计的核心思想是“不同年龄段的对象用不同的回收策略”很多公司面试官喜欢问年龄阈值怎么设置默认是15可以用-XX:MaxTenuringThreshold调整。4. 多线程与并发编程考点4.1 线程生命周期与状态转换多线程这块笔试基本围绕线程状态、锁、线程池三件事展开。线程的状态有六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。很多人会把RUNNABLE和运行中混为一谈其实RUNNABLE包含了就绪和运行两个状态因为在Java里没法严格区分线程是不是正在使用CPU。WAITING和TIMED_WAITING的区别也常考前者是无限期等待需要notify或signal唤醒后者是设置了时间超时自动唤醒比如sleep、wait(long)、join(long)。笔试或面试常让写一个线程交替打印的例子比如两个线程交替输出奇数和偶数。这里有个关键点用wait和notify时判断条件必须用while不用if防止虚假唤醒。这个细节非常能看出候选人有没有写过多线程代码。4.2 synchronized与Lock对比synchronized和Lock的对比是必考题我见过最多的错误说法是“synchronized是重量级锁性能不如Lock”。这句话在JDK 1.6之后就不对了。JDK 1.6对synchronized做了大量优化引入了偏向锁、轻量级锁、重量级锁的升级过程而且锁可以随着竞争加剧逐步升级却不能降级。所以在低竞争场景下synchronized的性能和Lock差距很小而且它用法简单不需要手动释放锁出现异常的容错性更好。Lock的优势在于更灵活tryLock支持非阻塞尝试获取锁lockInterruptibly支持中断ReadWriteLock支持读写分离。笔试如果问“什么时候用synchronized什么时候用Lock”我会这么答synchronized适合锁的持有时间短、竞争不激烈、代码简单的情况Lock适合需要超时控制、可中断、多条件变量、读写分离的复杂场景。另外要说一下Lock的典型用法必须放在try-finally中finally里unlock这一点几乎是必考的。4.3 volatile与内存可见性volatile这个关键词笔试里经常和synchronized对比来考。volatile有两个核心语义一是保证变量在多线程之间的可见性二是禁止指令重排序。但它不保证原子性volatile int count的count不是线程安全的因为count在字节码层面是读、加、写三步操作volatile只能保证每一步的可见性不能把三步合成一个原子操作。很多同学不理解“可见性”到底指什么。简单说线程对共享变量的修改如果加了volatile会立即刷新到主内存其他线程读取时会直接从主内存读而不是从自己的工作内存读。你可以类比成volatile变量像一块公共白板任何线程写上去其他线程立刻能在白板上看到。笔试常考的经典示例是DCL单例模式为什么单例的instance要用volatile修饰核心原因是instance new Singleton()不是原子操作它可以拆成三条指令分配内存、初始化对象、把引用指向内存。如果不加volatile指令重排序后可能先执行第三步再执行第二步另一个线程拿到的是一个未初始化完成的对象。volatile禁止了这种重排序保证对象一定初始化完成之后才暴露引用。能把这个讲清楚基本就能拿到这道题的分数了。4.4 线程池核心参数与执行流程线程池在笔试中属于必考的大题但很多人只记了七个参数的名字没有理解执行流程。先看参数corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime非核心线程空闲存活时间、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。执行流程是这样的一个任务提交给线程池时先判断核心线程是否都活着如果有空位创建核心线程执行如果核心线程已满判断任务队列是否已满没满就进队列排队如果队列也满了再判断当前线程数是否达到最大线程数没达到就创建非核心线程执行如果已经达到最大线程数触发拒绝策略。这个流程我建议你用一道生产环境的题来理解假设某个电商系统的瞬时请求量暴增corePoolSize5maxPoolSize10队列容量100。如果来了200个请求前5个立即被核心线程执行接下来100个进入队列再接下来5个被非核心线程执行最后90个被拒绝。很多同学只看参数不清楚“队列满之后才创建非核心线程”这个顺序导致答错。拒绝策略有四种AbortPolicy直接抛异常、CallerRunsPolicy让提交任务的线程自己跑、DiscardPolicy默默丢弃、DiscardOldestPolicy丢弃队列里最老的任务。实际业务中CallerRunsPolicy通常是比较稳妥的选择因为它能起到背压的效果但要注意如果任务本身很耗时会阻塞业务线程。4.5 电商场景并发题秒杀库存扣减有赞是做电商系统的所以笔试里如果出现并发场景题八成和库存、订单有关。一个经典的场景题是秒杀活动库存只有10件但瞬间来了1万请求怎么保证不超卖这类题考察的是你对并发控制技术的综合运用能力而不是某一个知识点的背诵。我一般会分层次回答。第一步用Redis预扣库存利用Redis单线程和Lua脚本的原子性做扣减可以在入口层挡住绝大多数的无效流量。第二步请求进入MQ异步削峰真正落到数据库的量只有几十几百个避免数据库被打垮。第三步数据库层面通过UPDATE stock SET count count - 1 WHERE goods_id ? AND count 0的乐观锁方式扣减利用数据库行锁和条件判断保证不超卖。第四步对于那些扣减成功但未支付的订单设置超时自动释放库存。这种回答方式之所以在笔试里拿高分是因为它展示了完整的链路思维而不是只会说“用 synchronized 锁一下”。笔试和面试喜欢看候选人能不能把一个方案落到具体的技术栈和具体的数据结构上。5. 数据结构与算法编程题5.1 冒泡排序写出完整版比背快排更重要算法题是Java笔试的硬骨头有赞A卷的编程题不会超纲基本都是手写排序、链表操作、二叉树遍历但会对代码质量和边界处理有较高要求。先说冒泡排序这几乎是所有笔试的第一道送分题但你要注意写出完整可运行的版本并把优化做好。public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } int n arr.length; for (int i 0; i n - 1; i) { boolean swapped false; for (int j 0; j n - 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是一个很好的加分项它表示如果某一轮没有任何交换说明数组已经有序可以提前终止。笔试时很多人会漏掉这个优化虽然不影响正确性但给面试官的感觉就是“你写代码没有性能意识”。边界条件的判断也很重要arr null || arr.length 2这一行能看出你有没有防御性编程的习惯。5.2 快速排序手撕代码的必考题快速排序是校招笔试中出现频率最高的排序算法没有之一。它难在partition这一步的理解以及递归边界的处理。我推荐写Hoare版本或者Lomuto版本Lomuto版本更简洁不容易写错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[right]; int i left; // i 指向第一个大于等于 pivot 的位置 for (int j left; j right; j) { if (arr[j] pivot) { swap(arr, i, j); i; } } swap(arr, i, right); return i; }笔试时如果你能解释清楚partition的循环不变量会是一个非常强的加分动作。具体来说i左边都是小于pivot的元素i到j之间都是大于等于pivot的元素j右边是未扫描区域。每次发现一个小于pivot的元素就把它交换到左边去i右移。最后把pivot交换到i的位置这样pivot左边都小于它右边都大于它。平均时间复杂度O(n log n)最坏O(n^2)最坏情况发生在数组已经有序时因为每次分区极其不均匀。为了避免这种情况可以用随机选择pivot或者三数取中法。5.3 链表操作反转链表与环检测链表题在笔试题中也是常客尤其喜欢考单向链表反转和单链表环检测。反转链表的递归写法非常简洁public ListNode reverseList(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseList(head.next); head.next.next head; head.next null; return newHead; }初次看到这个递归可能有点绕我的理解方式是递归的目的是先反转head.next开始的子链表反转完成后newHead是子链表的新头而head.next变成了子链表的尾节点。此时head.next.next head让尾节点指向head相当于把head接到反转后的链表末尾最后置空head.next。笔试时如果忘记置空head.next链表会形成环这是一个非常隐蔽的失误。链表环检测用的是快慢指针快指针每次走两步慢指针每次走一步如果链表有环二者一定会相遇。如果笔试让你证明为什么快慢指针一定会相遇你可以说当慢指针进入环后快指针相对于慢指针每走一步就追近一步所以必然追上。5.4 二叉树遍历递归写法与迭代写法二叉树的遍历题笔试一般会让你写前序、中序、后序以及层序遍历。递归写法很简单但有些公司会要求你写迭代版本考察你对栈和队列的理解。我建议你至少掌握前序和中序的迭代写法因为它们逻辑相似可以用一套模板public ListInteger inorderTraversal(TreeNode root) { ListInteger result new ArrayList(); DequeTreeNode stack new ArrayDeque(); TreeNode cur root; while (cur ! null || !stack.isEmpty()) { while (cur ! null) { stack.push(cur); cur cur.left; } cur stack.pop(); result.add(cur.val); cur cur.right; } return result; }这个模板的核心是“先一路向左入栈再弹栈访问然后转向右子树”。前序遍历只需要调整访问时机为入栈时访问就能复用同样的结构。层序遍历一般用队列实现每层先记录当前队列长度再循环处理该层节点这是一个经典的BFS模板。笔试时如果时间紧张我建议优先把递归写法写对不要冒险写迭代版然后卡在边界。但如果你平时练熟了迭代版会给面试官留下“代码能力强”的印象。5.5 编程题的通用答题模板与时间分配我知道很多同学笔试挂不是因为不会做而是因为紧张导致边界条件考虑不全或者时间分配出现问题。我总结一个编程题的答题顺序实测下来非常管用第一先读题把输入输出示例抄下来手动推演一遍确认自己理解有没有偏差。第二设计算法先用一两句话描述思路和复杂度写在草稿上。第三写代码先写核心逻辑再补充边界条件。第四检查尤其检查数组越界、空指针、输入为空这三种常见情况。第五如果还有时间提供一版优化思路比如“如果数据量再大一倍我会改用堆来处理前K个问题”。时间分配上我自己的经验是选择题和简答题控制在总时间的40%以内编程题留60%。很多同学喜欢在简答题上长篇大论导致最后编程题没时间写这是最可惜的。编程题哪怕只写出暴力解法也要比空着强因为至少能通过部分测试用例。6. 数据库与框架高频题6.1 SQL优化索引失效场景与执行计划Java笔试里数据库题不会特别难但会问到SQL优化和事务隔离级别。SQL优化最常见的考法是给一段慢SQL问你如何优化或者问哪些情况下索引会失效。我整理一个实际开发中最常见的索引失效清单对索引列使用函数、隐式类型转换、like以%开头的模糊查询、or连接非索引列、联合索引不满足最左前缀原则、索引列参与计算。这六种情况只要沾上一种索引就废了。笔试里如果有执行计划的题一定要会看explain输出的几个关键列type最好是const或refrange也能接受如果是all就说明全表扫描了rows是预估扫描行数越小越好extra里面出现Using filesort或Using temporary通常意味着性能隐患。这些术语能说准确面试官就知道你是真用过not just背概念。6.2 事务隔离级别与MVCC事务这块必考隔离级别和脏读、不可重复读、幻读的区别。四个隔离级别从低到高是读未提交、读已提交、可重复读、串行化。MySQL默认是可重复读但通过MVCC机制已经能很大程度避免幻读。很多人分不清不可重复读和幻读。我的理解是不可重复读是指同一行记录的内容前后读取不一致因为被其他事务update了幻读是指同一条件下两次读取的行数不一致因为被其他事务insert或delete了。在可重复读隔离级别下MVCC通过快照读保证了普通select的一致性但当前读select for update、update、delete仍然可能读到新插入的行所以需要间隙锁来解决幻读。这一块能讲到MVCC和当前读的区别笔试基本就是高分答案了。6.3 Spring IoC与AOP核心概念框架题在Java笔试里会占到5%到10%重点还是Spring的IoC和AOP。IoC的核心思想是把对象的创建和管理交给容器而不是在代码里手动new。这样做的好处是解耦你只需要声明依赖关系容器负责注入。笔试里如果问IoC和DI的区别我会说IoC是设计思想依赖注入是它的实现方式。AOP的核心是面向切面把日志、事务、权限这些横切逻辑从业务代码里抽出来。笔试里常问Spring AOP和AspectJ的区别Spring AOP基于动态代理只支持方法级别的切点运行时织入AspectJ是编译期织入功能更强大。实际开发中如果目标类实现了接口Spring默认用JDK动态代理它是基于接口的生成的是接口的实现类代理如果目标类没有实现接口用CGLIB代理它是通过生成子类来实现的所以目标类不能是final的。这个区别几乎是Spring面试的必考送分点。6.4 Spring Boot自动配置与启动流程有赞的技术栈里Spring Boot是很常用的所以笔试里也可能出现Spring Boot自动配置的题。自动配置的核心是EnableAutoConfiguration注解它通过AutoConfigurationImportSelector读取META-INF/spring.factories里的配置类全限定名然后按条件装配。这里的ConditionalOnClass、ConditionalOnMissingBean等条件注解起到了“有某个场景才装配某个配置”的作用。比如当classpath下有DataSource类和HikariCP的jar包时Spring Boot会自动配置一个数据源除非你已经手动定义了一个数据源的Bean。这个“自动”的背后是大量的条件判断。笔试如果让你解释Spring Boot为什么能“开箱即用”就把这套机制讲清楚就好。6.5 MyBatis #{}与${}的区别MyBatis的#{}和${}区别在Java笔试里出现的概率非常高答案比较固定#{}是预编译生成?占位符由参数化查询传入值能有效防止SQL注入${}是字符串拼接直接把值拼进SQL里存在SQL注入风险。所以能用#{}就尽量用#{}但表名、列名、排序字段这种不能使用占位符的场景只能用${}这时必须对传入参数做严格白名单校验。有赞这种电商公司查询场景非常多这道题大概率会结合一个实际的Mapper XML来考。你可以准备一个简短回答排序字段用法最典型因为ORDER BY后面不能绑定参数只能用${}但要在代码层做白名单比如只允许传asc或desc而不是直接拼接用户输入。这个细节能给面试官留下安全意识强的印象。7. 常见问题与避坑实录7.1 笔试中的典型失误与扣分点我在帮人改笔试代码和访谈面的过程中总结了几个最常见的失误点。第一个是手写代码时不判空比如链表题里head为null直接空指针异常第二个是数组边界没想清楚比如排序的右边界该不该减一第三个是HashMap遍历时边遍历边修改抛ConcurrentModificationException第四个是使用了不存在的JDK版本特性比如源码里编译版本是1.8你用了个Java 11的API虽然本地能跑线上编译直接失败。特别是最后一种我建议写代码前先看一眼试卷要求的JDK版本不要默认都是Java 8。有时候题目给出“源发行版17需要目标发行版17”这种报错其实就是编译环境没对齐笔试时遇到这种报错不要慌看看是不是环境变量或IDE设置的问题。还有一个常见的丢分点是代码写完了但不写注释或者代码命名一塌糊涂。笔试阅卷一般是人看的面试官看你的代码就像看你的工程习惯。变量名用a、b、c和用list、map、result给人留下的印象完全不同。我建议笔试时用有意义的命名核心逻辑加一行注释这些习惯真的会加分。7.2 时间分配与做题顺序建议做笔试最大的敌人不是题难而是时间不够。结合A卷的题型结构我的建议是先用5到8分钟快速扫一遍所有题目把会做的、没把握的、完全不会的分成三类。选择题特别是概念题不要纠结太久很多题第一直觉就是对的反复纠结反而容易改错。简答题控制在每道8到10分钟以内写清楚要点就行不要编论文。编程题一定放在前面做至少先做你最有把握的那道。编程题的评分往往有部分通过所以哪怕是暴力解法、部分用例通过也要把代码写上去。最怕的是你觉得自己写得不够完美就一直犹豫最后交了白卷。有输出就有一半的分数这是笔试的生存法则。7.3 笔试环境与JDK版本问题每年笔试都有不少同学因为环境问题翻车。我曾经见过有人在牛客网上写快排本地IDE能跑提交就是编译错误。排查了半天发现笔试题里的类名是Main但自己写成了public class QuickSort。这里提醒一下在线笔试平台的类名和代码结构是有固定要求的通常必须用Main作为主类名不能带package声明输入输出要用System.in读取。JDK版本也是重灾区。本地是JDK 17笔试平台是JDK 8写着写着用了var或者新API一编译就报错。我建议笔试前先确认平台的JDK版本平时练习就用对应版本约束一下自己。还有编码问题中文注释或字符串在部分平台可能乱码如果题目本身不涉及中文建议尽量用英文注释。7.4 笔试后如何高效复盘笔试结束不是终点复盘才是提升的关键。我自己的复盘方法是考完立刻把不会的题记下来不要等因为过两天就忘了。然后把每一道做错的题分门别类整理成“知识点卡片”每张卡片写清楚三个东西题目问了什么、我的错误答案是什么、正确思路是什么。这样做的好处是你的复习不会东一榔头西一棒子而是围绕自己真实的薄弱点展开。比如你发现自己总是混淆强引用、软引用、弱引用说明JVM这块理解不够系统那就花半天把引用类型彻底搞懂而不是再去刷十道类似的选择题。笔试面很广但每次做完题你的知识图谱都会更完整这才是做真题的最大价值。8. 个人经验与额外建议文章最后我想分享几个我实际带应届生的体会以及我这些年回顾笔试真题得出的经验。第一点不要迷信“八股文”。网上各种Java面试题大全、面试八股文确实能帮你快速过一遍知识点但笔试真正考的是你能否把知识点和实际场景连起来。比如你背会了HashMap的扩容机制但题目问“为什么链表长度超过8才转红黑树而不是6或者10”如果你没想过这个问题就只能干瞪眼。我在辅导应届生时一直强调“不要背结论要推导过程”。第二点做题时保持代码洁癖。笔试代码虽然只有几十行但你的变量命名、边界判断、注释习惯都会被阅卷人注意到。我见过很多同学明明算法思路完全正确但因为没判空、没考虑特殊情况被扣分这是非常可惜的。哪怕是笔试也当成一次code review来对待。第三点如果时间允许准备一个“Java异常处理手写模板”放在脑子里。很多编程题不是算法难而是输入输出处理容易出错比如判空、处理空行、处理超大输入。你用BufferedReader加StringTokenizer或split效率更高代码也更稳。这种细节在笔试中的价值一点都不比背一个冷门知识点低。我当年自己做校招笔试的时候也栽过不少跟头。最深的体会就是Java笔试考的不是你记住了多少知识而是你在有限时间内、有压力的情况下能写出多少清晰有效的代码。这份有赞A卷之所以值得反复研究是因为它很典型不偏、不怪、不炫技它就是一面镜子让你照见自己基础是不是真的扎实。希望你刷完这套题不只是会做题而是真的把Java的地基打得更实。