聊到用友2018秋招Java笔试题(四),我印象最深的不是哪道题特别难,而是整套卷子的节奏感。前半小时你会觉得“就这?”,后半小时开始怀疑自己是不是漏看了什么条件,交卷前十分钟发现好几道题都是“看似基础,实际埋雷”。这套题是我当年秋招做过的Java笔试卷里,非常能代表企业级应用厂商考察Java工程师风格的一套:既有大量基础题刷掉背书党,又有几道需要真正写过代码才能答对的题,还有一两道能区分“背过八股”和“真懂原理”的题。
这篇不像网上那些只贴题目和答案的帖子,我会按我记忆里的卷面顺序,把高频考点、易错点、扩展追问全部串起来讲一遍。无论你是准备校招、社招跳槽,还是纯粹想把Java基础打扎实,这套题都值得认真过一遍。
1. 先把卷面摊开:这套题到底在考什么
1.1 题量与题型分布
用友这套秋招笔试题的卷面,我当时拿到的是纸质版(有的城市是机房在线答题),时间两个小时,总分100分。题型大致是这样的:
| 题型 | 题量 | 分值 | 我的感受 |
|---|---|---|---|
| 单选题 | 15题 | 30分 | 覆盖面广,JavaSE为主,少量数据库和网络 |
| 多选题 | 5题 | 15分 | 最恶心,少选多选都扣分 |
| 简答题 | 3题 | 15分 | 类和对象、集合、并发三选二 |
| 编程题 | 2题 | 25分 | 一道数据结构,一道简单业务逻辑设计 |
| SQL题 | 1题 | 15分 | 三表关联查询,还要求写优化思路 |
说实话,这个分布在一众互联网大厂里不算激进。头部的电商、社交大厂那时候已经开始狂问分布式、中间件、高并发场景题了,用友这张卷子还是偏“底子”的。但别小看这种卷子,它对细节的抠法,比直接问“Redis为什么快”要阴险得多。
1.2 明显送分题与拉开差距的题
送分题基本集中在单选前10题,比如“Java的基本数据类型有几种”“下列哪个关键字用于异常处理”“ArrayList和LinkedList哪个查询更快”。只要系统学过Java,这部分闭着眼也能拿分。
真正拉开差距的是后面几类:
- 多选题里经常出现“下列说法正确的是”,每个选项都长得像对的,实际至少有一个选项是错的。
- 简答题不直接问“了解集合吗”,而是问“HashMap在JDK 8中链表转红黑树的阈值为什么是8,为什么不是7或9”。
- 编程题不考“反转字符串”,而是考“从文件中读取订单数据,按金额排序并输出”,需要你写出完整可运行的代码,包括异常处理。
- SQL题除了一句查询外,还要求你写出索引建议,并解释为什么这样建索引。
如果你只是刷过别人的面经,没有真正自己敲过代码、看过源码,这套卷子能把你戳得很疼。
1.3 两个小时内怎么分配时间
我自己当时的时间分配是:选择题30分钟,多选题10分钟,简答题20分钟,编程题40分钟,SQL题15分钟,剩下5分钟检查。但实际做下来,选择题花了快40分钟,因为有几道题需要实际推算,比如二叉树深度、HashMap扩容后的下标位置。
如果你将来也要参加这类笔试,我的建议是:
- 选择题遇到不会的果断标记,不要恋战,一道题最多两分钟。
- 多选题宁可少选,不要多选,很多题目规则是“全部选对得满分,选对但不全得一半分,多选或错选不得分”。
- 编程题先写解题思路注释,再写代码,至少保证核心逻辑的分能拿到。
- SQL题务必用缩进把层次写清楚,阅卷人一眼能看到你的连接条件。
2. 逐题复盘:选择题里的高频考点与易错细节
2.1 集合框架:HashMap的容量、哈希碰撞、红黑树阈值
选择题里有一道,我记得特别清楚:HashMap默认初始容量是多少?选项有8、10、16、32。答案是16。但真正有水平的追问是后面那题:为什么是16,而不是10?为什么加载因子是0.75?
这里展开说。HashMap的容量被设计成2的幂次方,是为了能用(n - 1) & hash位运算代替取模,速度更快。16=2^4,所以默认容量是16。如果你传入一个不是2的幂次方的初始容量,比如19,HashMap内部会用tableSizeFor方法把它转换成大于等于19的最小2的幂次方,也就是32。
加载因子0.75是空间和时间的折中。调大了比如1.0,哈希冲突会更严重,查询变慢;调小了比如0.5,空间浪费明显。0.75这个数值在泊松分布模型下,桶内链表长度达到8的概率已经非常小,所以链表转红黑树的阈值是8,转回链表的阈值是6,避免在树和链表之间频繁切换。
这道题的延伸问到过:“JDK 8的HashMap和JDK 7有什么区别?”。你至少要能答出三点:
- JDK 8引入红黑树,JDK 7没有。
- JDK 8插入用尾插法,JDK 7用头插法。
- JDK 7扩容时会重新计算hash,JDK 8通过高位运算简化了计算。
这些都属于“你真正看过源码才答得出”的细节。我建议你把JDK 8 HashMap的put、get、resize三个方法的源码读透,因为后续技术面的追问基本都是从这里长出来的。
2.2 String与包装类:== 和 equals 的坑
单选题里有一道非常经典的:
String s1 = new String("abc"); String s2 = "abc"; System.out.println(s1 == s2); System.out.println(s1.equals(s2));第一个输出false,第二个输出true。这个大多数人都知道。但用友这道题又加了一层:
String s3 = "a" + "bc"; String s4 = "abc"; System.out.println(s3 == s4);答案是true,因为编译期常量折叠,"a" + "bc"会被编译器直接优化成常量"abc",所以s3和s4都指向常量池里同一个对象。
比这个更阴的是改个写法:
String s5 = "a"; String s6 = s5 + "bc"; System.out.println(s6 == s4);答案是false。因为s5是变量,变量的拼接在运行期通过StringBuilder完成,生成的是堆里的新对象。
同理,Integer的缓存范围也有类似的坑。Integer a = 127; Integer b = 127; a == b是true,但如果是128就走缓存了?不对,-128到127之间是缓存,128超出范围,会new两个不同对象,所以128那个比较是false。
这类题的本质不是考语法,而是考你对JVM内存模型和编译器优化机制的熟悉程度。我当时在这道题上多留了个心眼,把常量池、堆、栈之间的关系在脑子里又过了一遍,后面JVM的题答起来就顺多了。
2.3 并发编程:volatile、synchronized与线程池
有题问volatile关键字能保证什么。选项里有可见性、有序性、原子性。正确选项是可见性和有序性,它不能保证原子性。很多刚学Java的人都以为volatile和synchronized差不多,其实是两个层面的东西。
volatile解决的是“多线程之间某个变量的修改对其他线程可见”的问题。它通过内存屏障禁止指令重排序,读的时候强制从主内存读,写的时候强制刷回主内存。但count++这种操作分三步:读、加一、写回,volatile管不了这三步的原子性。
所以并发编程题里,凡是看到volatile int count然后多线程做自增的代码,问结果是多少,你都要明白答案是不确定的。
另一个常考的是synchronized和ReentrantLock的区别。我当时把要点写成了对比:
- synchronized是关键字,ReentrantLock是类。
- synchronized是隐式锁,自动释放;ReentrantLock需手动lock和unlock。
- synchronized是非公平锁;ReentrantLock默认非公平,但可设置为公平锁。
- ReentrantLock支持tryLock超时等待,支持多个Condition条件。
- JDK 6之后synchronized经过锁升级优化,性能已经不输ReentrantLock。
还有线程池的题,问ThreadPoolExecutor的核心参数里,workQueue放的是什么。答案是等待执行的任务。很多选择题把workQueue和threadFactory混淆,实际上threadFactory是创建线程的工厂。
2.4 JVM内存区域与类加载
JVM的题在这套卷子里占了不小比重。选择题问“下列哪个区域不属于线程共享”,选项有堆、方法区、虚拟机栈、元数据区。答案是虚拟机栈,它是线程私有的。
紧接着的简答题是:“描述一下类加载的双亲委派模型,为什么需要它?”双亲委派的意思是,一个类加载器收到类加载请求后,不会自己先去加载,而是把请求委派给父加载器,逐级向上,最后才由自己加载。
它的好处有三个:
- 避免类被重复加载,比如你自定义了一个
java.lang.String,因为双亲委派,最终会由启动类加载器加载JDK自带的String,避免核心类被覆盖。 - 保证Java类型体系的安全。
- 避免不同类加载器加载出同名类导致混乱。
这道题背后还藏着一个考点:如果你自己写了一个java.lang.String,然后放到classpath下,能加载成功吗?答案是不能,会抛SecurityException,因为双亲委派机制拦截了它。这也是为什么很多面试官喜欢顺着双亲委派问Tomcat的类加载器为什么要打破双亲委派。Tomcat需要为不同Web应用提供隔离的类加载环境,所以它自己实现了一套WebAppClassLoader,先加载自己WEB-INF/classes下的类,再委派给父加载器。
2.5 Spring与数据库:企业级应用绕不开的部分
作为企业级应用厂商,用友笔试里Spring和数据库肯定不会缺席。
Spring的题目有一道是:“Spring Bean的默认作用域是什么?”。答案是singleton,单例。然后问“prototype作用域适合什么场景”。适合有状态的对象,比如一个对象里面存了当前用户信息,如果用单例,多线程并发访问会互相污染数据。
还有一道事务传播行为的题:“ServiceA的methodA调用ServiceB的methodB,methodA的传播行为是REQUIRED,methodB的传播行为是REQUIRES_NEW,如果methodB抛了异常,methodA已经执行的部分会回滚吗?”答案是methodA已执行的部分不会因为methodB的回滚而回滚,因为REQUIRES_NEW会开启一个新事务,新事务的执行结果与外部事务无关。但如果methodB异常向上抛出且methodA捕获后不再抛异常,methodB新事务回滚,methodA自己的事务因为没有抛出RuntimeException,可能正常提交。
数据库的题,经典的是“为什么InnoDB的索引要用B+树而不是二叉搜索树或哈希表”。参考答案是:
- 二叉搜索树在极端情况下会退化成链表,树高度过高,磁盘IO次数增加。
- 哈希表虽然单点查询O(1),但不支持范围查询和排序。
- B+树是多路平衡树,高度低,一般三层左右就能存储千万级数据,一次查询只需要3到4次磁盘IO。
- B+树的数据都存储在叶子节点,并且叶子节点之间用指针连接,非常适合范围查询。
还有一道事务隔离级别的多选题,问你InnoDB默认的隔离级别是什么。答案是REPEATABLE READ,可重复读。但InnoDB通过MVCC和间隙锁,在可重复读下避免了幻读,所以实际隔离效果接近串行化。
3. 编程题与SQL题:真正拉分的是手写能力
3.1 手写编程题的评分逻辑
编程题不是只给代码就满分。阅卷老师会看你的解题思路、边界条件、时间复杂度分析。我当时就吃过亏,题目要求“找出整数数组中连续递增的最长子序列长度”,我直接写了一个双重循环,答案对了,但时间复杂度O(n^2),评论区都说应该一次遍历O(n)搞定。
后来我养成了一个习惯:任何算法题,先写注释,说明暴力解法,然后优化为最优解,最后再写代码。
比如求最长连续递增子序列长度:
public int findLengthOfLCIS(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int maxLen = 1; int curLen = 1; for (int i = 1; i < nums.length; i++) { if (nums[i] > nums[i - 1]) { curLen++; } else { maxLen = Math.max(maxLen, curLen); curLen = 1; } } return Math.max(maxLen, curLen); }这段代码的核心是“当连续递增中断时,重置curLen为1”。很多初学者会忘记在循环结束后再更新一次maxLen,导致整个数组都是递增的用例出错。这种边界条件,笔试里最容易扣分。
另外一道题是“反转单链表,要求迭代和递归两种方式”。迭代写法就是三个指针prev、cur、next依次挪动:
public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode cur = head; while (cur != null) { ListNode next = cur.next; cur.next = prev; prev = cur; cur = next; } return prev; }递归写法简洁但容易绕晕:
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; }我当时先用迭代法写,然后在注释里补充递归思路,保证至少一种写法完整。笔试题量大,能完整写出一种就不错了,别贪多。
3.2 SQL题:三表关联与索引优化
SQL题给的是员工表、部门表、薪资表三张表,要求查每个部门薪资最高的员工姓名和薪资。这类题有两种典型写法。
MySQL 8.0及以上可以用窗口函数:
SELECT department_name, employee_name, salary FROM ( SELECT d.name AS department_name, e.name AS employee_name, s.salary, ROW_NUMBER() OVER (PARTITION BY d.id ORDER BY s.salary DESC) AS rn FROM employee e JOIN department d ON e.dept_id = d.id JOIN salary s ON e.id = s.employee_id ) t WHERE rn = 1;如果用的是MySQL 5.7,不支持窗口函数,就要用关联子查询或临时表:
SELECT d.name AS department_name, e.name AS employee_name, s.salary FROM employee e JOIN department d ON e.dept_id = d.id JOIN salary s ON e.id = s.employee_id WHERE s.salary = ( SELECT MAX(s2.salary) FROM salary s2 JOIN employee e2 ON s2.employee_id = e2.id WHERE e2.dept_id = d.id );这道题的坑在于:如果两个员工薪资相同且都是部门最高,第一种子查询会返回多条记录,第二种窗口函数能保证只取一条。笔试时如果没特别说明“只取一人”,我建议用窗口函数,或者明确写出“如果有并列则取第一条”。
索引优化思路也是采分点。我当时这样写的:
- 员工表的dept_id建普通索引。
- 薪资表的employee_id建唯一索引或普通索引。
- 如果按照部门维度统计较频繁,考虑联合索引(dept_id, salary)。
阅卷人看到你能主动写出索引建议,基本这题就拿了大半的分。
3.3 业务设计题:从订单数据中提取关键信息
还有一道编程题,不是纯算法,而是描述一个场景:某电商系统每天产生大量订单,要求你设计一个方法,从订单列表中统计出每个商品的销售总金额,并按照金额从高到低输出。
这题本质是考Map的用法和排序,但很多人一上来就用HashMap<String, BigDecimal>存金额,忘了考虑BigDecimal相加要用add而不是加号。
我当时是这样写的:
public List<Map.Entry<String, BigDecimal>> calculateTotalAmount(List<Order> orders) { Map<String, BigDecimal> totalMap = new HashMap<>(); for (Order order : orders) { totalMap.put( order.getProductId(), totalMap.getOrDefault(order.getProductId(), BigDecimal.ZERO) .add(order.getAmount()) ); } List<Map.Entry<String, BigDecimal>> result = new ArrayList<>(totalMap.entrySet()); result.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); return result; }这里用了getOrDefault和BigDecimal的add,而不是totalMap.put(key, totalMap.get(key) + amount),因为double的精度在做金额计算时会有问题。这道题考察的就是“你会不会在业务代码里用正确的方式处理精度”。
4. 那些容易丢分的细节:阅卷视角下的采分点
4.1 手写代码的边界条件,最容易被扣
笔试阅卷人最喜欢看的是你写不写空指针判断、数组越界判断、循环结束后是否更新最大值。每次面试复盘我都发现,丢分点往往不在主体逻辑上,而在以下几个位置:
- 数组题:没有判空和长度0。
- 链表题:没有处理head为null。
- 字符串题:没有处理空串和null。
- 递归题:忘记停止条件。
- 金额计算:用了float/double而不是BigDecimal。
一个很实用的习惯是:写完代码后,自己脑内跑三个用例——空输入、单元素输入、正常输入。如果是链表,再加一个两个节点的用例。这一条能帮你少丢至少三分。
4.2 简答题的“废话过多”问题
很多人误以为简答题写得多就分高,其实阅卷是看关键词给分的。比如让你说“HashMap的put流程”,你得先写“计算hash -> 定位桶 -> 如果桶为空直接放 -> 如果桶不为空,判断是否链表或红黑树 -> 链表遍历查找,存在则覆盖,不存在则尾插 -> 超过8转红黑树 -> 扩容判断”。你把这些关键词写全,然后稍微展开说明,比长篇大论讲HashMap历史强得多。
我当时吃了个亏:有一道题问“JDK 8中HashMap为什么要引入红黑树”,我洋洋洒洒写了半页纸,从哈希函数讲到泊松分布,但漏了“解决链表过长导致的查询退化为O(n)问题”这个核心句。所以后来我总结出一个答题模板:先给结论,再写理由,最后加一个例子。结论先行,阅卷人扫一眼就能看到你的答案。
4.3 多选题的“宁可少选”策略
用友这套卷子多选题规则我记得是:少选得一半分,多选、错选不得分。所以遇到拿不准的选项,我的策略是只选最确定的那个。比如题目问“下列哪些是线程安全的集合”,我确定ConcurrentHashMap是,但拿不准Collections.synchronizedList包装后的List算不算,那我只选ConcurrentHashMap,至少保住一半分。
这种策略虽然激进,但比平均分高。如果你追求满分,那就要把每个集合类的底层结构都梳理一遍。
4.4 SQL题的隐性问题
SQL题还有一个很容易忽略的点:表连接时如果字段名有歧义,必须加上表别名。比如员工表和薪资表都有employee_id字段,你直接写WHERE employee_id = s.employee_id,数据库会报错。这种低级错误在笔试里非常常见。
另外,要求“写出优化思路”的题,不要只写“加索引”。你要能写出:
- 哪张表的哪个字段需要加索引。
- 避免使用select *,只查询需要的字段。
- 大数据量分页时,避免offset过大,用延迟关联。
这些才是阅卷人想看到的优化能力。
5. 从这套题反推用友的招聘偏好与后续面试衔接
5.1 笔试通过后,面试会怎么追问
做完这套题后我最大的感受是:用友的面试官问的问题,几乎都从笔试题往外扩。比如笔试考了HashMap,面试时就问“如果自定义对象作为HashMap的key,需要注意什么?”答案是要重写equals和hashCode,保证equals相等时hashCode也相等。
笔试考了事务传播行为,面试就让你现场说一个REQUIRES_NEW的实际使用场景。我当时举的例子是:在日志记录场景中,哪怕主业务事务回滚,日志也必须记录成功,所以日志记录方法要设置为REQUIRES_NEW。
笔试考了JVM内存区域,面试就让你分析String s = new String("abc")之后,堆、栈、常量池里各有什么。
所以你是为了面试准备的话,刷这套笔试题不能只对答案,你要把每个考点都延伸出至少三个追问,自己用A4纸把答案写一遍。这样笔试过了,面试环节也能顺势衔接上。
5.2 企业在招聘后端Java工程师时,其实是在挑什么人
用友属于企业级软件头部厂商,产品线包括ERP、财务软件、人力云等。这些系统的特点是业务复杂、数据量大、事务要求严格、系统不能轻易宕机。所以笔试里大量考察集合、并发、JVM、数据库,本质上是在筛选两类能力:
- 一是基础能力扎实,能处理日常开发中遇到的并发问题、内存问题、SQL性能问题。
- 二是业务建模能力,能把复杂的订单、报表、审批流转换成清晰的代码结构。
这和互联网大厂“上来就考算法、手撕红黑树”的风格差别很大。你准备用友这类公司的笔试题,重点应该放在Java基础、Spring、数据库、企业级应用设计上,而不是死磕DP和贪心。
5.3 简历上怎么呼应这套题的考察点
如果你投的是用友的Java开发岗,简历上的技术栈描述一定要覆盖这张卷子的核心考察点。我建议在简历里明确写出:
- 熟练掌握Java集合框架,研究过HashMap底层结构及扩容机制。
- 熟悉JVM内存模型、垃圾回收算法,有JVM调优经验。
- 熟悉MySQL InnoDB存储引擎,掌握索引优化、事务隔离级别。
- 熟悉Spring事务传播行为,能处理分布式事务场景。
这些句子看起来简单,但每一个都对应笔试的一道题。面试官看到你的简历描述和笔试题高度重合,对你后续面试的提问起点也会友好很多。
5.4 我后来复盘这套题时的几点体会
笔试结束当晚,我把所有错题和模糊题重新整理了一遍,发现真正丢分的不是知识盲区,而是这三件事:
- 多选题太贪心,总想着拿满分,结果扣分。
- 手写算法题没写注释,逻辑跳步,阅卷人读起来费劲。
- SQL关键字没大写,表连接字段没加别名,被扣了印象分。
这些都是可以在考前通过模拟训练规避的问题。所以我强烈建议你笔试前,完整地做两套题型类似的卷子,用手机计时,模拟真实考场环境。你会发现“会做”和“在两个小时内做完”完全是两回事。
最后分享一个小技巧:做笔试的时候,不要直接在题目旁边写答案,先在草稿纸上列关键词。比如看到HashMap,立刻写下“数组+链表+红黑树、加载因子0.75、扩容2倍、头插尾插区别”,看到事务,立刻写下“ACID、传播行为、隔离级别、MVCC”。这些关键词既是你的答题大纲,也是你检查答案时的对照清单。很多题只需要你看到关键词就能拼出正确答案,远比临时回忆靠谱。