用友2017秋招笔试题解析:Java基础、算法与SQL优化全攻略
1. 先看清这份题的底牌老规矩先说结论用友2017秋招笔试题四是一套典型的国内大厂校招笔试题考察方向集中在Java基础、数据结构与算法、数据库设计和基础逻辑能力难度中等偏上但远没有到ACM竞赛那种劝退级别。对当时投递用友的应届生来说这套题更像是一块试金石——不是看你会不会写多炫的代码而是看你有没有扎实的工程基础能不能理解一个企业级ERP厂商日常开发中真正用得上的技术点。为什么特意说“用友”这个背景因为用友做的是To B的软件服务的是企业的财务、人力、供应链、制造这些核心业务环节。这类公司招人的时候笔试题目通常带有明显的“务实”倾向不考那些花里胡哨的奇技淫巧而是紧紧围绕Java生态、数据库、业务建模这些企业开发最常见的技术栈来出题。2017年又处于互联网和传统软件行业竞争最激烈、企业数字化转型刚刚全面提速的时间节点所以这套题里能看到不少针对并发、集合、SQL优化这些实际开发痛点的考察。我记得当时有不少同学在牛客网上分享这套题大部分人的感受是选择题不难但陷阱多编程题看着眼熟但想在限定时间内写出健壮、考虑完善的代码其实并不容易最后的数据库设计题则是直接检验你有没有做过后端业务系统的经验。说白了这套题刷的不是手速而是内功。如果你能把这套题的每一个考点都吃透那对你的校招备战来说收益远远超出用友这一家公司的范围。下面我就把这份笔试题四按题型拆开结合当年的题目内容和我自己后续工作中的复盘把核心考点、解题思路、容易丢分的细节一点点捋清楚。2. 选择题里的高频考点基础不牢地动山摇2.1 String、StringBuilder、字符串池的相爱相杀这套题的选择题部分几乎每年都会有一道跟String相关的题。2017这套四里出现的是这样的考点组合给你一段代码让你判断创建了几个对象、内存中发生了什么、最终输出结果是什么。这类题考察的核心有两个字符串常量池和字符串不可变性。举个例子题目里很可能出现这种代码String a hello; String b new String(hello); String c b.intern(); System.out.println(a b); // false System.out.println(a c); // true System.out.println(b c); // false这段代码的考点非常密集。第一行String a helloJVM会先去字符串常量池里找有没有字面量hello如果没有就创建一个并把引用交给a。第二行new String(hello)对象本身是在堆上创建的但注意这个构造方法的入参hello依然会先查常量池所以此时常量池里已经有了hello这个对象。于是b指向堆上那个新对象而a指向常量池里的对象a和b的引用当然不相等。intern()方法的语义是从常量池中返回当前字符串的引用如果常量池里已经有内容相同的字符串就直接返回那个引用否则先加入常量池再返回。所以c和a指向同一个常量池对象c和b自然不相等。这类题丢分的原因往往不是不懂原理而是被题目里那些迷惑性的输出项带偏。我建议刷这类题的时候脑子里一定要带着一张内存分布图栈、堆、方法区常量池挂在方法区JDK 7以后挪到了堆里但逻辑上依然是常量池各放什么东西。把引用和对象的边界理清了String的题基本就是送分题。但这里有一个进阶考点2017这套题选了一个变种final修饰的String拼接。比如String a hello; final String b he; final String c llo; String d hello; String e b c; System.out.println(d e); // true当两个字符串都是final常量且值是编译期常量时b c在编译阶段就会直接优化成hello所以e和d指向常量池中的同一个对象。但如果把final去掉那b c就会在运行期通过StringBuilder拼接生成新的String对象结果就是false。这种细节如果没实测过很容易在考场上想当然。2.2 HashMapJDK 7和JDK 8的内存模型差异用友这种做企业软件的公司对HashMap的考察频率非常高因为ERP系统里做缓存、做批量数据聚合、做权限映射到处都在用Map。2017这套题里我记得印象比较深的是一个关于HashMap扩容的判断题问HashMap在什么情况下会出现死循环以及JDK 8解决了这个问题没有。HashMap的死循环问题根因在于JDK 7及以前扩容时采用的是“头插法”迁移链表节点。假设线程A和线程B同时对HashMap做put操作都触发了扩容旧数组里的链表元素在迁移过程中可能形成一个环。这样下一次查询的时候如果恰好命中这个环上的桶就会在链表上无限循环最终导致CPU 100%。JDK 8优化了这个过程改用“尾插法”也就是迁移时保持链表的原始顺序并且引入了红黑树去优化长链表的查询效率所以JDK 8虽然不会因为扩容产生环但依然不是线程安全的并发写时依然存在丢数据、覆盖值的问题。这道题给校招生的警示是不要把HashMap背得滚瓜烂熟却不知道它在多线程环境下该用谁替代。正确的替代方案是ConcurrentHashMap但2017年的考纲里对ConcurrentHashMap的段锁机制问得不多更多是停留在“是否线程安全”这个层面。如果你在笔试备注里写上“JDK 8 HashMap在并发场景依然不能使用应该用ConcurrentHashMap或线程安全的集合工具类”那这题就稳了而且会显得你有实战意识。2.3 SQL优化与索引失效的经典场景选择题还有一道让我印象很深的SQL题考的是索引失效的条件。2017年的题干大致是有一张员工表字段包括emp_id、emp_name、hire_date、department_id其中hire_date建有普通索引问下面几个查询里哪个用到了索引。四个选项大概是A. SELECT * FROM emp WHERE hire_date 1 2024-01-01; B. SELECT * FROM emp WHERE hire_date 2024-01-01 AND department_id 10; C. SELECT * FROM emp WHERE hire_date 2024-01-01 OR department_id 10; D. SELECT * FROM emp WHERE hire_date LIKE %2024%;这个题的核心就是索引失效场景的识别。A选项对索引列做了运算索引失效C选项因为OR条件中有一个列没有索引会导致整个查询退化为全表扫描D选项的前导模糊匹配也走不了索引。只有B选项能正常用到hire_date的索引。别小看这道题我在实际开发中遇到不少人写SQL时会在索引列上做函数运算比如WHERE DATE(create_time) CURDATE()这等于让数据库放弃索引。正确的写法应该是范围条件WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00。企业级报表系统里一张千万级的订单表如果因这种SQL语句走全表扫描慢查询日志分分钟刷屏。这套题的出题人很懂实际业务痛点所以在这一题上设置了两个“看起来对”的干扰项专门坑那些只知道背“索引能加速”却不知道底层原理的人。3. 编程题实操手写代码和评委眼中的得分点3.1 合并两个有序数组不要只满足于能跑编程题第一题很多版本的2017用友秋招题里都有这套四也不例外给定两个升序排列的int数组要求合并成一个升序数组并返回。看起来就是归并排序的合并步骤几乎是送分题但当年这道题卡住了不少人原因是题目里有一句话——尽量不使用额外空间。如果不限制空间最常见的写法是新建一个数组用双指针逐个比较填充时间复杂度O(mn)空间复杂度O(mn)。但题目既然这么问考察意图就很明显你能不能原地合并。原地合并的经典做法是把两个数组合并到第一个足够长的数组里从后往前填充。比如public void merge(int[] nums1, int m, int[] nums2, int n) { int i m - 1; int j n - 1; int k m n - 1; while (j 0) { if (i 0 nums1[i] nums2[j]) { nums1[k--] nums1[i--]; } else { nums1[k--] nums2[j--]; } } }为什么要从后往前因为nums1开辟的空间在尾部从前往后覆盖的话会覆盖掉nums1尚未比较的元素而nums2的元素必须在比较完之后才能落位所以从后往前可以安全地利用数组尾部的空闲区。这个思路和直插排序的移位逻辑有点类似核心是“把空间留到最后用”。我当时刷这道题的时候第一个版本也写成了创建新数组后来检查题目要求才发现空间限制。这个教训我一直记着笔试里读题比写代码更重要。题目里每一个附加条件都不是白写的都是为了引导你往特定方向思考。这道题还有一个容易被忽略的边界两个数组都为空时怎么办其中一方为空时怎么办。我建议在校招冲刺阶段每做完一道算法题都养成“边界条件自测”的习惯。像这道题你至少要在心里跑一遍nums1为空、nums2为空、所有nums1元素都比nums2大、所有nums2元素都比nums1大这四种情况代码是否都能正确处理。企业里的代码Review非常关注这些边界因为生产环境的数据永远不会像LeetCode示例那么友好。3.2 括号匹配栈结构的教科书应用第二道编程题是判断一个字符串中的括号是否正确匹配。括号有三种小括号、中括号、大括号。这是一道典型的栈应用问题算法思路不复杂遇到左括号就入栈遇到右括号就判断它是否与栈顶的左括号匹配匹配则弹栈不匹配直接返回false遍历结束后如果栈为空说明括号全部匹配否则说明有多余的左括号。面试官真正期待的高质量代码不仅是算法正确还包括代码结构清晰。下面是我自己比较满意的一版public boolean isValid(String s) { if (s null || s.length() 0) { return true; } if (s.length() % 2 1) { return false; } MapCharacter, Character map new HashMap(); map.put(), (); map.put(], [); map.put(}, {); StackCharacter stack new Stack(); for (char c : s.toCharArray()) { if (map.containsKey(c)) { if (stack.isEmpty() || stack.peek() ! map.get(c)) { return false; } stack.pop(); } else { stack.push(c); } } return stack.isEmpty(); }注意里面两个细节一是长度不为偶数直接返回false这叫“提前短路”提交的时候能省不少执行时间二是用Map来映射左右括号代码可读性比一堆switch-case强得多。如果写得顺手还可以进一步把Stack换成ArrayDeque因为JavaStack类的性能并不是最优的ArrayDeque在单线程环境下更轻量。这道题表面上考的是算法实际上考的是“工具选型”意识。用友的笔试虽然没有指定只能用JCF自带的栈但你在代码中选用什么容器、怎么组织逻辑都会被阅卷的人看在眼里。企业里面写代码从来不是“能跑就行”而是“好读、好改、好测”。3.3 单例模式多线程下的双重检查锁2017这套题里还有一道编程/简答题实现一个单例模式并且要考虑多线程情况。这题在学校里几乎人手都会背但一旦限定“多线程安全”并且“性能不能太差”很多人就露馅了。最基础的方式是synchronized修饰整个getInstance方法代码短、线程安全但每次获取实例都要经历锁竞争高并发下性能堪忧。更常见也容易被面试官追问的是双重检查锁也就是常说的DCLDouble Checked Lockpublic 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; } }DCL的核心在于两个判断第一次判断是为了避免无谓地进入同步代码块第二次判断是为了保证多线程同时通过第一层检查后只有一个线程能创建实例。这里必须加上volatile否则第二个线程可能在使用instance时看到的是一个“构造尚未完成”的半初始化对象。原因是instance new Singleton()这行代码在底层分为三步分配内存、调用构造器、把引用指向内存。如果不加volatileJVM的指令重排序可能导致第3步先于第2步执行另一个线程恰好判断instance ! null拿到的就是一个还没构造完成的对象。这个考点被无数面试官津津乐道不是因为它难而是因为它能完美检验一个人对并发模型、内存可见性、指令重排序的理解深度。对校招生来说能写出DCL并讲清volatile的作用已经能拿到很高的技术印象分。再补充一个选择题里容易出现的“单例相关”坑利用反射可以强行调用私有构造器或者用序列化反序列化破坏单例。用友这套题的原题没往下延伸但面试环节经常会问我建议你在准备笔试时就把这些扩展点一起看了因为笔试中你写在备注里的思考往往就是面试官追问的线索。4. 数据库设计题用友最看重的业务建模能力4.1 订单表设计从需求到建表的完整推导这套题四的大题部分给出的是一个简化版的“企业订单系统”场景要求设计订单表和订单明细表并回答几个业务查询相关的SQL问题。之前说过用友的核心赛道是ERP而订单/采购/库存这些都是ERP里最经典的数据模型这道题属于典型的业务建模考察。我看到的题目描述大概是这样的一个客户可以下多个订单一个订单包含多个商品每个商品在订单中有购买数量和成交单价。要求设计表结构并在此基础上完成几个查询。这类题没有标准答案但优秀答案都有一些共通的建模原则。首先订单主表和订单明细表必须分离。为什么从范式角度看如果一张表里同时放订单信息和商品信息第一条订单如果有5个商品那订单信息就要重复存储5遍不仅冗余大而且容易产生数据不一致。更重要的是企业订单的“订单头”和“订单行”天然就不是一对一的关系——订单头记录的是客户、下单时间、总金额、订单状态这类一次性信息订单行记录的是每一个商品的具体金额、折扣、发货状态。两者生命周期虽然相关但变更频率和查询维度完全不同。我的表结构设计参考如下CREATE TABLE t_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, customer_id BIGINT NOT NULL COMMENT 客户ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待支付1已支付2已发货3已完成4已取消, total_amount DECIMAL(12,2) NOT NULL COMMENT 订单总金额, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, UNIQUE KEY uk_order_no (order_no), KEY idx_customer_time (customer_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order_item ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 明细ID, order_id BIGINT NOT NULL COMMENT 订单ID, product_id BIGINT NOT NULL COMMENT 商品ID, product_name VARCHAR(128) NOT NULL COMMENT 商品快照名称, price DECIMAL(12,2) NOT NULL COMMENT 成交单价, quantity INT NOT NULL COMMENT 购买数量, subtotal DECIMAL(12,2) NOT NULL COMMENT 小计金额, KEY idx_order_id (order_id), CONSTRAINT fk_order_item_order FOREIGN KEY (order_id) REFERENCES t_order(order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个细节是阅卷人特别容易给分的点金额字段一律用DECIMAL绝对不要用FLOAT或DOUBLE。这个知识点在选择题里可能也出现了但放到设计题里更能体现工程经验。因为浮点数在计算机里是近似存储计算金额会产生不可控的精度误差财务系统对一分钱都不能错。订单号加了唯一索引。订单ID是自增主键但对外暴露的订单编号通常有业务含义比如时间戳序列号必须唯一约束。product_name字段做了快照冗余。为什么不直接关联商品表因为商品名称、价格可能会变而订单里记录的应该是下单那一刻的商品信息。企业级订单系统里这叫“数据快照”既能保证历史订单可追溯又避免每次查询都去关联商品表性能更优。在(customer_id, create_time)上建了联合索引。因为“查某个客户的历史订单”是最常见的查询场景联合索引能一次性覆盖两个过滤字段避免回表和文件排序。4.2 事务与并发写SQL时脑子里要有锁设计题的最后一部分通常是写几条SQL其中有一条是查出“下单金额最高的前3位客户”。这个需求看起来简单写出来的SQL却大有讲究SELECT customer_id, SUM(total_amount) AS total FROM t_order WHERE order_status IN (1, 2, 3) GROUP BY customer_id ORDER BY total DESC LIMIT 3;这道题考察的是聚合函数、分组、排序、取前N条的综合运用。但如果你停在“能写出来”这个层面还不够。我当时在解题过程里额外标注了一条SUM(total_amount)时只统计有效状态的订单因为待支付或已取消的订单不应该计入真实成交金额。这个细节想清楚之后整道题的分数档次就拉开了。再到更深一层事务隔离级别和锁的问题也值得展开。一个ERP系统里订单创建往往和库存扣减强相关。在并发情况下两个用户同时买最后一个库存商品数据库层面怎么保证不超卖这就要用到行锁或乐观锁。2017年的笔试虽然没直接问锁但我在面试环节被追问过“Inventory库存-8这个操作在并发场景下怎么保证安全”。用乐观锁的典型方案是在库存表上加一个version字段UPDATE t_inventory SET quantity quantity - 8, version version 1 WHERE product_id 1001 AND version 2;如果更新影响的行数为0说明版本已变化需要重新读取库存再试。这种解法现在成了后端基础题标配但在2017年的校招里能主动往这个方向回答的人不多说出来真的很加分。5. 这套题踩过的坑真实考生的血泪教训5.1 时间分配选择不要太恋战用友2017这套笔试题的总时长大约90分钟选择题编程题设计题。很多考生栽在时间分配上尤其是在前面几个偏理论的选择题上反复纠结。比如String那题明明已经把“引用相等”和“内容相等”区分开了却因为题目里多了一个concat()方法的结果而陷入自我怀疑一纠结就是七八分钟。我个人的建议是选择题每道题控制在2分钟以内超过直接先蒙一个并标记回头有时间再细算。笔试题量大编程题和设计题的采分点更密集前面恋战的时间成本太高。你得清楚自己的目标是总分最大化而不是每一题都完美。5.2 代码题最怕“能跑但不完整”我身边当年一起刷题的人里有不止一个在合并有序数组那道题上写出了标准新建数组解法但完全没考虑题目“原地合并”的要求。这种情况哪怕功能完全正确拿到的分也要打折扣。笔试阅卷不是机器判题那么简单重点看你的思路和代码习惯。所以每次写完代码务必自查三件事边界条件处理没有、空间复杂度是否满足题目限制、异常输入是否会抛错。把这三件事养成肌肉记忆你会发现在LeetCode上刷题的正确率都能提升不少。5.3 数据库题别只写表结构要写设计说明很多同学在做数据库设计题的时候只把建表语句一贴就完事了。这是非常大的失分点。用友这种企业的技术官看重的不是你能不能敲出SQL而是你懂不懂为什么这么设计。我当时在每张表的关键字段后面都加了COMMENT并且单独列了一段文字说明解释为什么用DECIMAL、为什么加冗余快照字段、为什么建联合索引。加起来也就一百来字但给阅卷人的信号是这个人不仅会写代码还知道每行代码背后的成本和收益。笔试不是写论文但适度的“解释性注释”绝对是加分项。企业开发里代码Review同样推崇“注释解释为什么而不是解释是什么”。5.4 会用API但要懂底层的“为什么”2017年的Java生态Stream还不像现在这样普及但Lambda表达式和Java 8新特性已经开始大规模进入校园招聘考题。这套题的选择题里虽然没有直接考Stream但我在用Stream重写合并有序数组这类题目时自己探索了一版int[] merged IntStream.concat(Arrays.stream(nums1), Arrays.stream(nums2)) .sorted() .toArray();这个写法很简洁但笔试慎用。原因很简单出题人想考察的是双指针算法的思维过程你用内置排序虽然结果对却把你最想展示的思维能力藏起来了。这也算是一个微观体现——在笔试场景里尽量用“能暴露你思考过程”的方式答题而不是用“最简洁的API”答题。6. 刷题之后的延伸这套题如何帮你面对后续面试把一套笔试题吃透的价值绝不只在于笔试本身。用友这套题覆盖的考点——String内存模型、HashMap并发问题、SQL索引优化、单例模式、业务表设计——几乎就是后端校招面试中Java基础数据库项目经验三条主线的最小公约数。按照这套题去梳理自己的知识体系比漫无目的地刷一百道LeetCode更高效。我建议你把每道错题都整理成一张“知识点-具体问题-我的理解-面试追问方向”的表格比如题目考点核心问题理解深度可能追问方向String不可变性new String(a)创建几个对象必须能画内存图intern()机制细节HashMap并发JDK 7死循环根因理解头插法/尾插法差异ConcurrentHashMap分段锁原理SQL索引失效函数运算导致索引失效能举出三个具体场景联合索引最左前缀原则双重检查锁为什么必须volatile能说出指令重排序静态内部类单例优缺点订单表设计主表和明细表分离能解释范式与快照冗余分库分表后如何保持一致性每次面试前只看这张表过一遍自己的掌握程度哪里卡壳就回到完整笔记里补。这套方法我从校招一直用到了工作后的跳槽准备稳定好用。用友2017秋招笔试题四这份卷子放到今天来看很多考点依然不过时。数据结构与算法是基本功SQL优化和业务建模是企业开发的日常并发和内存模型则决定了你能不能在真实的分布式场景里写出安全可靠的代码。如果你现在正处于校招期我强烈建议你认真对待这套题里的每一处细节不要只满足于“做对了”而是要求自己“讲得清”。今天先聊到这里这套题的完整代码和扩展题解我都整理在本地笔记里了后面找个时间再和大家说说如何把这些知识点串联成一套自己的面试话术。如果你也在刷这套题欢迎在评论区聊聊你的思路和困惑。

相关新闻