
前几天整理旧资料翻出一份恒生公司2015年秋招开发类的笔试题。10年前的笔试题放到今天看很多考点依然眼熟——手写链表反转、String比较、SQL优化、Linux查日志。这年头大家张口闭口都是agent开发、AI应用开发好像不会点新框架就不好意思投简历但真正到了笔试环节考的还是那些底层的、不随框架迭代而失效的东西。这篇文章不是要逐题报答案而是想借恒生这套题把开发类笔试的考察逻辑、核心考点和准备方法完整拆一遍。不管你是准备校招的应届生还是打算跳槽的工程师只要目标是开发岗这套基本功的复习框架都值得对一遍。我会按题型逐类展开补充完整解题思路和踩坑点确保你看完能直接拿去用。1. 一份2015年的恒生笔试题为什么值得现在翻出来看1.1 恒生这家公司想招什么样的人恒生的业务主要围绕金融IT展开证券、银行、基金、保险这些核心交易与清算系统很多都跑在他们家的软件上。这类系统的共同特点是不能宕机、不能算错账、高并发下还要保证数据一致。所以恒生笔试的开发者筛选逻辑非常明确——先看基础功底过不过关再看有没有工程意识最后才看你会不会某个具体框架。这跟互联网大厂的海量算法题路线不太一样。金融IT更看重你对内存、并发、事务、数据一致性这些概念的理解深度。2015年的笔试题里就已经大量出现线程安全、锁、数据库隔离级别这类题目因为在真实的交易系统里一个并发处理不当就是真金白银的损失。从这层业务背景出发你就能推断出一份合格笔试考卷的选题倾向。基础数据结构和算法一定会有但占比不会像互联网公司那么极端操作系统、计算机网络、数据库、Linux命令这些“硬功夫”会占相当大的篇幅Java或C的语法陷阱题也是标配用来筛掉那些只背过框架API的简历型选手。1.2 这套卷子的题型分布与考察逻辑一份典型的开发类笔试卷通常由选择题、填空题、简答题、程序输出题、手写代码题和SQL题组成。2015年恒生的这份卷子基本也是这个结构。我按记忆里的常见分布归纳如下你可以当成一张复习地图题型大致占比考察目标选择题20%-30%基础语法、概念辨析、边界条件填空题10%-15%关键结论的准确记忆如死锁条件、TCP状态程序输出题15%-20%代码执行过程的推演能力手写代码题20%-25%数据结构与算法基本功SQL/数据库题10%-15%事务、索引、查询优化简答题10%-15%系统设计意识与知识整合格局很多同学复习时喜欢死磕算法题但这类笔试里真正拉开差距的往往是那些“看起来简单”的程序输出题和SQL题。你以为是考语法实际上考的是你平时写代码时有没有真正理解内存模型、执行顺序和异常处理机制。这些内容在IDE里跑一遍看不出差别但一到笔试就容易翻车。2. 从金融IT业务场景反推考察重点并发、事务与基础功底2.1 为什么并发和锁是必考中的必考金融系统的核心场景是交易。成千上万个用户同时买入卖出同一个账户可能被多个请求同时操作。如果账户余额的扣减没有做好并发控制轻则数据错乱重则资金事故。所以恒生笔试里出现并发相关题目几乎是必然的。2015年那会儿Java 8刚普及不久ConcurrentHashMap、ThreadLocal、synchronized与ReentrantLock的区别这类问题就已经是高频考点了。放到今天来看这些问题依然是Java岗笔试的常客。还有一个特别经典的手写题用两个线程交替打印奇偶数考察wait/notify配合synchronized的熟练度。这类题目的考察重点不是你会不会调API而是三个层次能不能说清楚并发问题的本质——可见性、原子性、有序性能不能给出正确的同步方案——锁、 volatile、CAS、并发容器分别解决什么问题能不能识别出错误方案——比如用double check却忘了加volatile拿账户扣款这个场景举例很多人的第一反应是给方法加synchronized。这确实能保证线程安全但在高并发下性能堪忧。更合理的方式是使用乐观锁通过CAS或版本号机制在更新时校验数据是否被修改过。这个思考过程本身就是一道隐形的加分题笔试不一定直接问但答题时的设计思路会体现在字里行间。2.2 数据库事务的考察在金融IT笔试中的分量交易系统的另一个核心诉求是数据一致性。转账、对账、清算这些环节都依赖数据库事务。因此数据库部分在恒生这类公司的笔试题里比重明显高于一般互联网公司。我印象里最常考的题型是给一个并发转账场景问你不同事务隔离级别下会出现什么问题。脏读、不可重复读、幻读这三个概念表面上看起来差不多实际考察的是你对锁和MVCC的理解深度隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交不会可能可能可重复读不会不会可能InnoDB下通过间隙锁基本可避免串行化不会不会不会笔试作答时加一句“MySQL InnoDB在可重复读级别下通过MVCC解决了快照读的幻读问题但当前读仍需依赖间隙锁”这个深度一下就拉起来了。这比单纯背表格要加分太多因为它说明你真看过底层实现。3. 数据结构与算法题高频手写代码的完整得分思路3.1 单链表反转一道题测出指针功底单链表反转是手写代码题里出场率最高的题目之一因为它短小精悍却能把一个人的指针操作基本功暴露得干干净净。笔试考场上时间紧张很多人一紧张就把链表指针指来指去指乱了。迭代版是最稳的写法思路就一句话遍历链表时把当前节点的next指向前一个节点。代码如下public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode nextTemp curr.next; curr.next prev; prev curr; curr nextTemp; } return prev; }这段代码有几个笔试中特别容易扣分的点必须先保存curr.next否则改完指针后链表就断了循环结束条件是curr ! null不是curr.next ! null最后返回的是prev不是curr此时curr已经是null递归版本也经常被要求写虽然实际工程中不常用但考察的是递归思维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.next指向head”这句话能体现你对同一问题的多角度理解。3.2 二分查找变体边界条件才是真正的考点直接考二分查找有点太简单所以笔试更常见的是变体题。比如“在一个有序数组中查找第一个等于目标值的下标”或者“查找最后一个小于等于目标值的元素”。这类题的难点不在二分思想本身而在边界条件的处理。我总结的通用模板是public int firstEqual(int[] nums, int target) { int left 0, right nums.length - 1; int result -1; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { right mid - 1; } else { left mid 1; } if (nums[mid] target) { result mid; } } return result; }写这类题时我建议每次循环都手动走一遍mid的计算用left (right - left) / 2避免left right溢出当前值大于等于target时把右边界收到mid - 1找到等于target的元素后不急着返回继续往左找循环结束后检查result是否被更新过没更新说明数组里没有目标值考场上最容易犯的错是找到第一个等于target的元素就直接return结果忽略了“第一个”这个限定条件。这恰恰是出题人设置的陷阱。3.3 用两个栈实现队列数据结构联动的经典设计题这道题考察的是你对栈和队列本质特性的理解。栈是后进先出队列是先进先出怎么用两个栈模拟出先进先出的效果思路其实很巧妙入队时直接往stack1里push。出队时先把stack1里的元素全部弹出并push到stack2里再pop stack2的栈顶。这样stack2的栈顶就是最先入队的元素。class MyQueue { private DequeInteger inStack; private DequeInteger outStack; public MyQueue() { inStack new ArrayDeque(); outStack new ArrayDeque(); } public void push(int x) { inStack.push(x); } public int pop() { if (outStack.isEmpty()) { while (!inStack.isEmpty()) { outStack.push(inStack.pop()); } } return outStack.pop(); } }注意一个关键优化只有当outStack为空时才需要把inStack的元素倒过去。如果outStack里还有元素直接pop即可。这个“懒惰倒腾”的思路能让均摊时间复杂度降到O(1)。这道题还喜欢延伸问两个队列怎么实现栈反过来想就行——入栈时把元素放到非空队列出栈时把前n-1个元素移到另一个队列剩下的就是栈顶。这类题目没有背诵价值但理解了“用数据结构特性去模拟另一种数据结构”的思路后现场推演也不怕。4. 语言的陷阱题String、集合与JVM内存那些“送命题”4.1 String、StringBuilder、StringBuffer到底怎么选这道题几乎年年出现在Java岗笔试试卷里恒生2015年的卷子也没放过。很多人能背出结论String不可变、StringBuilder线程不安全但快、StringBuffer线程安全但慢。但笔试问法通常更刁钻比如给一段字符串拼接代码问创建了几个对象。String s a b c;这行代码在编译阶段就会直接优化成abc运行时不会额外创建对象。但如果写成String s ; for (int i 0; i 10; i) { s i; }那就会在循环里创建大量String对象。因为字符串拼接的本质是每次new一个StringBuilderappend后再toString。循环多少次就创建多少个临时对象。这个执行过程笔试简答题如果让你分析性能瓶颈标准答案就是循环内字符串拼接应直接使用StringBuilder。4.2 、equals和hashCode三个概念串起一整套约定这类题出成程序输出题时迷惑性特别强Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); System.out.println(c d);很多人一看Integer是对象觉得比较的是引用应该都是false。但正确答案是true和false。原因在于IntegerCache默认缓存了-128到127之间的Integer对象所以a和b指向同一个缓存对象c和d则各自new了新对象。这个知识点不只在笔试里有用实际开发中同样容易踩坑。比如用比较两个Integer变量在127以内没问题超过127就会出错。这也是为什么阿里巴巴Java开发规范里明确要求所有整型包装类对象之间值的比较全部使用equals方法。hashCode与equals的约定也是必考项。核心要点就一句话两个对象equals相等hashCode必须相等hashCode相等equals不一定相等。重写equals时必须重写hashCode否则HashMap、HashSet这些依赖hash的集合就会出现逻辑错误。4.3 HashMap的底层结构演变与扩容机制HashMap在2015年的笔试里就已经是常客了。那会儿JDK 8刚发布数组链表红黑树的结构刚引入不久很多人还没反应过来。放到现在这个问题基本上已经成了Java笔试的必问项。核心考点有三个维度数据结构数组链表链表长度超过8且数组长度超过64时转为红黑树put流程先计算key的hash定位到数组槽位如果该位置为null直接放入否则遍历链表/红黑树判断key是否存在存在则覆盖不存在则新增扩容机制默认负载因子0.75容量达到阈值时扩容为原来的两倍还有一个容易被问到的点为什么HashMap允许key为null而ConcurrentHashMap不允许因为HashMap本身不是线程安全的它可以将null映射到数组的0号槽位而ConcurrentHashMap的并发控制依赖于key的hash值null的hash无法参与计算同时设计上也不希望在并发场景下出现歧义。4.4 抽象类、接口与final/finally/finalize这道题在选择题里出现频率极高但很多人只记住了“抽象类可以有构造方法接口不能”这类表面结论。更深入的考察方向是JDK 8之后接口增加了默认方法和静态方法这个设计是为了解决什么问题答案是接口的演进需要兼容性。在JDK 8之前给接口加一个新方法意味着所有实现类都必须同步实现否则编译报错。新增默认方法后可以在不破坏现有实现的情况下给接口增加新能力。这和Java 9的模块化、Java 17的密封类一样都是Java在向后兼容和向前演进之间的取舍智慧。final/finally/finalize这三个长得像的兄弟也是选择题的常客。final是修饰符finally是异常处理的关键字finalize是Object类里的一个方法。笔试里常挖的坑是finally块里的return会不会覆盖try块里的return。答案是会但正确做法是不要在finally里写return因为这会吞掉try块里的异常信息。finalize方法从JDK 9开始就被标记为废弃了不建议依赖它做资源释放。5. 数据库、操作系统与网络简答题的底层理解模板5.1 索引为什么用B树而不是红黑树这道简答题在开发类笔试里出现的频率极高恒生这类做交易系统的公司尤其爱问因为索引设计直接影响数据库查询性能。从底层来讲数据库索引需要存储在磁盘上而磁盘I/O是性能瓶颈。B树的每个节点能存储多个键值树的高度更低一次查询需要访问的磁盘块更少。更关键的是B树的叶子节点通过指针串联成有序链表非常适合范围查询和排序操作这正对交易系统里常见的时间区间查询场景。回答这道题的完整层次是数据库数据量大无法全部载入内存必须考虑磁盘I/O次数树的高度决定查询时需要访问的磁盘块数量B树的矮胖结构优势明显叶子节点有序链表让范围查询只需要顺序遍历避免回溯非叶子节点只存储键不存储数据单节点能容纳更多键进一步降低树高如果你能再补一句“红黑树更适合内存中的场景比如TreeMap和JDK 8的HashMap树化”这个对比就更立体了。5.2 TCP三次握手为什么不是两次网络题在开发类笔试里主要考TCP/IP协议栈。最经典的就是三次握手的“为什么”。标准答法是三次握手可以防止已失效的连接请求报文段突然又传到服务器避免服务器创建无意义的连接并浪费资源。如果你不想让答案显得像背课文可以加一句更本质的解释三次握手的核心目的是让通信双方都确认自己的发送能力和接收能力是正常的。第一次握手服务器确认客户端的发送能力第二次握手客户端确认服务器的发送和接收能力第三次握手服务器确认客户端的接收能力。这样双方就完成了双向通信能力的确立。还有一个高频考点是TIME_WAIT状态。主动关闭连接的一方会进入TIME_WAIT等待2MSL时间后才真正关闭。原因是一是为了保证最后一个ACK报文能够到达对端如果丢了对端会重发FIN二是为了让本连接内所有延迟的报文在网络中自然消失避免干扰新的连接。做高并发服务端开发的同学对TIME_WAIT导致端口耗尽的问题应该有切身体会。5.3 死锁的必要条件与避免策略操作系统里的死锁题也是简答题常客。四个必要条件得背熟互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。笔试如果考简答题除了列条件最好再各加一句解释。更进阶的考察方式是给一段并发代码问你有没有死锁风险或者直接让你设计一个避免死锁的方案。在实际工程里最常见的做法是破坏循环等待条件——对多个资源进行排序所有线程都按相同顺序加锁。这个方案简单高效代价是需要程序员在编码时严格遵守约定。比如一个转账场景A账户转给B账户B账户同时转给A账户。如果两个线程分别先锁A再锁B、先锁B再锁A就很容易形成死锁。解决办法是无论从A转B还是B转A都先锁账户id较小的那个再锁较大的那个。这样两个线程的加锁顺序一致循环等待就被打破了。6. Linux命令与日志排查笔试中的实操题拿法6.1 面试官通过几道命令题筛掉“简历型选手”不管你是面后端开发还是做嵌入式开发Linux命令几乎都是必考项。恒生这类维护大量服务器的金融科技公司更看重工程师能否在Linux环境下快速定位线上问题。笔试中常见的考察形式是给你一个线上故障场景让你写出排查命令。这些题目没有太高技术含量但没在服务器上实战过的人很容易卡壳。我整理了一张高频命令速查表可以直接当复习提纲场景命令查看进程占用CPUtop然后按P排序查找某个端口被谁占用netstat -tunlp | grep 8080 或 ss -lntp查找进程并杀掉ps -ef | grep java再kill -9 pid查看日志末尾tail -f / tail -n 100按关键字搜索日志grep -n ERROR app.log统计数据行数wc -l去重统计sort | uniq -c | sort -nr定时任务管理crontab -l / crontab -e6.2 三个典型的日志排查笔试场景场景一线上接口突然变慢怎么定位。标准思路是先top看CPU和内存再用jstack看Java线程状态再用jstat看GC情况。笔试不需要写jstack的具体用法但能把jstack、jstat、top、free这几条命令按排查顺序列出来就已经合格了。场景二统计日志中某个接口的调用次数。假设日志格式是每行一条包含接口路径和状态码2025-01-01 10:00:00 INFO /api/order/create success 2025-01-01 10:00:01 INFO /api/order/create success 2025-01-01 10:00:02 ERROR /api/order/query error统计接口调用次数可以这样写grep /api/order/create app.log | wc -l统计每个接口的调用次数并排序awk {print $4} app.log | sort | uniq -c | sort -nr这类题目考察的是你对awk、sort、uniq这些文本处理工具的掌握程度。不需要背复杂脚本但基本组合用法得熟练。场景三定时清理日志。这个在开发同学眼里很基础但动不动就有人因为磁盘被日志写满导致线上故障。笔试如果问“如何每天凌晨3点清理/tmp下7天前的日志文件”答案是0 3 * * * find /tmp -type f -mtime 7 -exec rm -f {} \;这个命令的关键点是-mtime 7表示修改时间超过7天-exec配合{}和;实现对每个文件执行删除操作。笔试填空中容易漏掉最后的反斜杠分号。7. 笔试现场的时间分配与拿分策略7.1 发卷后前5分钟决定整场心态拿到卷子后别急着动笔先花三到五分钟把整张卷子浏览一遍。重点看三件事各题型的数量与分值分布确定主攻对象有没有一眼就能答对的送分题先有个心理预期手写代码题有几道、复杂度高不高在心里排个优先级我个人的策略是选择题和填空题先快速过一遍遇到不会的做个标记不恋战。程序输出题需要静心推演放到第二梯队。SQL题如果思路明确就顺手做掉。手写代码题留最后但务必保证有充足时间。关于时间分配以一份90分钟、满分100分的卷子为例我建议这样拆题型时间预算选择题20-30分15-20分钟填空题10-15分5-10分钟程序输出题15-20分15分钟SQL/数据库题10-15分10分钟手写代码题20-25分25-30分钟检查与补漏10分钟7.2 手写代码的“可读性优先”原则笔试手写代码和平时在IDE里写代码最大的区别是没有人帮你编译也没有自动补全。阅卷老师看的是你的思路是否清晰而不是语法是否完美。我强烈建议遵循三个原则第一变量命名要见名知意。就算题目里用的是单字母变量你也要在代码里写出duplicateCount、currentNode这种有语义的名字。阅卷老师看到这种代码第一印象就会好很多。第二宁可多写中间变量不要追求“一行流”。比如交换两个变量值老老实实写temp临时变量比用异或操作三行并成一行要清晰得多。在阅卷场景里可读性远大于炫技。第三算法题写完核心逻辑后一定补一句注释说明思路。比如在循环前加一行“// 先找到数组中间位置”或“// 快指针先走n步”。这会让阅卷老师确信你不是背代码而是真的理解解法。7.3 程序输出题手动推演比凭感觉靠谱程序输出题是笔试里最容易丢分也最容易拿分的题型。说它容易拿分是因为答案就藏在代码执行过程里说它容易丢分是因为考生经常凭第一印象直接写答案忽略了执行顺序和边界条件。我的做题习惯是拿一张草稿纸把涉及的关键变量全部列成表格一行一行模拟执行。比如遇到for循环里有i和i混用或者if条件里出现了短路运算就老老实实把每次循环后各变量的值写出来。这个过程看似麻烦但正确率极高。另外要特别注意异常处理结构。try-catch-finally块里如果有returnfinal块里的代码是否还会执行答案是会。如果在catch块里再次抛出异常finally块还会不会执行答案也是会。但finally里如果包含了return它会覆盖之前的return值。这些输出题里常设的坑提前了解就不会掉进去。7.4 不会的题怎么“抢分”笔试卷子上的每一道题都有分值哪怕完全不会也不能空着。这一点对于开发类笔试格外重要因为很多简答题和设计题都是按点给分你写对关键概念就能拿到部分分数。举个具体例子简答题问“简述你对索引的理解”。如果只背过定义可以这么写“索引是一种帮助数据库高效获取数据的数据结构。底层主要是B树它能降低查询的磁盘I/O次数。索引虽然能加速查询但会降低增删改速度因为每次修改数据时还要同步维护索引结构。在实际使用中应该为高频查询字段创建索引避免对低区分度的字段加索引。”这段话能拿到基础分而且第二、三句已经展示了你对索引代价的理解。如果还能补一句“联合索引要遵循最左前缀原则”得分又会再上一个台阶。这个答题策略的核心是把自己掌握的所有相关知识都写进去让阅卷老师看出你的思维过程。7.5 复盘考完试比考完试更重要我个人刷完一套笔试题后最值的动作是花时间做复盘。不只是对着答案改对错而是把每道错题对应的知识点重新梳理一遍找到盲区的根源。比如手写快排时忘了处理数组为空的边界条件说明对“防御式编程”的意识不够程序输出题里漏看了变量自增顺序说明读代码时不够细心SQL题里没选对索引列的顺序说明对最左前缀原则理解不透。每一次错题背后都对应着一个具体的、可修正的习惯问题这才是笔试真正要筛选的能力。2015年的恒生笔试题放到今天题目本身可能过时了但背后的考察逻辑和复习方法依然是有效的。技术栈会变框架会更新但并发控制、数据结构、操作系统、网络协议这些底层能力才是决定一名开发工程师能走多远的根本。这既是对校招生的提醒也是对我自己的一次复盘。