用友Java校招笔试题拆解:基础语法、集合与多线程考点精讲

发布时间:2026/9/1 20:13:55
用友Java校招笔试题拆解:基础语法、集合与多线程考点精讲 用友的校招笔试题一直以“广而不深、细而不偏”著称2018年这套Java方向的第一套题放在今天回看依然很有代表性。它不像大厂那样盯着算法题使劲卷而是把重心放在Java基础语法、集合框架、面向对象设计、异常处理、多线程这些实际开发天天用的知识点上题型覆盖选择、判断、简答和编程整体难度中等偏上对基本功的考查非常细致。这篇文章我打算把这个系列第一套题完整拆一遍不是单纯对答案而是把每道题背后考的知识点、易错点、以及当时出题人想听到的答题方向都梳理出来。适合正在准备校招的同学、计划跳槽的初级Java工程师以及带新人的技术组长参考——很多题目其实可以直接拿来当面试时的追问素材。1. 用友这套笔试题的考察思路拆解1.1 从出题人角度看校招笔试想筛什么校招和社招的笔试逻辑有本质区别。社招看重的是“你能直接干活吗”所以会面框架、面项目、面线上事故处理经验;校招则相反没有项目可问只能退回到考察候选人的学习能力和基本功扎实程度。用友这类老牌企业软件公司业务线以ERP、云平台、大数据产品为主核心系统对稳定性要求极高所以他们的校招笔试不会太“花哨”但一定会在基础细节上抠得很细。我当年做完这套题的第一个感受是选择题里几乎没有一道是“看一眼就能选出来”的送分题每一道都暗藏一两个坑。比如会考String在常量池中的引用指向、ArrayList的subList视图与结构性修改的联动影响、try-catch-finally里return的执行顺序这些知识点在项目里你要是没踩过坑很难答得又快又准。这说明出题人默认你已经系统学过Java而不是临时背了几天八股文。1.2 知识点覆盖范围与分值分布虽然不能100%还原当年的卷面分值但根据我对这套题的记忆和同类笔试题型的规律考察内容大致分为五块知识点模块典型考查形式预估分值占比Java基础语法与运算符选择题、判断题20%面向对象三大特性选择题、简答题20%集合框架与泛型选择题、编程题20%异常处理与常用类库选择题、程序结果分析15%多线程与并发基础简答题、程序结果分析15%手写代码与逻辑题编程题10%这个分布和很多传统软件企业的校招笔试题非常接近基础占大头、编程题占小头、不考偏门的算法。如果你现在正冲着大厂刷LeetCode我建议也回头把Java基础过一遍因为越是根基型的公司越不吃“题海战术”那一套。1.3 为什么说这套题对现在的面试仍有参考价值有人可能觉得2018年的题太老了Java都升级到17、21了看老题还有什么意义。这里我要多说一句恰恰因为是基础题反而不受版本升级影响。用友这套题里面考的核心——面向对象设计原则、集合的底层结构、异常控制流、多线程同步——这些内容从Java 1.4到Java 21几乎没有变化变化的只是API细节和语法糖。举个具体例子题目里考的HashMap的resize逻辑在JDK 8引入红黑树之后已经定型后面的JDK版本并没有推翻它只是做了性能优化。你把这个机制吃透了放到今天面试依然能打。同理String的不可变性、常量池的intern机制也一直是Java面试的高频考点跟版本没什么关系。1.4 题量分布与答题节奏预判这套卷子题量不少我记得大概有20道左右的选择题、5道左右判断题、3道简答题、1到2道编程题总时长120分钟。这个体量意味着平均每题不能超过5分钟选择判断要控制在每题2分钟左右。很多同学栽就栽在前面选择题纠结太久后面编程题没时间写。我的建议是拿到卷子先花3分钟把全部题目快速扫一遍判断题和简答题在脑子里先过一过答题思路然后按“先易后难、先客观后主观”的顺序答。尤其是编程题哪怕不能完整跑通也要把核心逻辑和注释写清楚阅卷人看到思路是对的也会给步骤分。2. 基础选择题与判断题看到就要秒答的知识点2.1 面向对象封装、继承、多态里的经典陷阱面向对象这部分用友的题有几个非常典型的考法基本每年都会变着花样出现。第一个是继承中构造方法的调用顺序给你一段代码父类和子类各有一个带参构造器和无参构造器问实例化子类时按什么顺序输出。答案永远是先父类构造器再子类构造器。很多同学会在父类没有无参构造、子类构造器又没有显式写super(参数)的时候迷惑这种题其实考的就是你是否记得“子类构造器第一条语句必须是super()或this()如果都没有则隐式调用父类无参构造器”。第二个是重载与重写的混淆。判断题里经常出“重载可以改变返回值类型”这种说法单独看是对的但如果说“仅改变返回值类型就算是重载”则不对因为重载的判定依据是参数列表不同返回值类型不同并不足以构成重载。我整理过一个口诀重载看参数重写看方法签名方法名参数列表异常和返回值必须兼容。第三个是多态的动态分派。有一次的题目是给一个父类引用指向子类对象然后调用一个在子类中被重写的方法问输出什么。很多人忘了Java动态分派只看运行时对象类型结果答错。这个知识点和下面要讲的instanceof、getClass()区别经常一起考都属于面向对象里“看着简单、实际容易错”的经典题。2.2 String、常量池与equals/hashCode的坑用友这套选择题里String相关的题目几乎必考。最常见的是给你几个字符串赋值语句问创建了几个对象、输出结果是什么。比如String s1 abc; String s2 new String(abc); System.out.println(s1 s2); System.out.println(s1.equals(s2));第一问输出false第二问输出true涉及的核心点是双引号直接声明的字符串存放在常量池中new出来的对象在堆中比较的是引用地址。但如果你把s2用intern()方法处理一下结果就又不同了。记住intern()会尝试把字符串加入常量池如果已经存在则返回常量池中的引用。还有一个考法是运算符拼接字符串时的优化行为。JDK 5以上编译器会对字面量拼接做编译期优化比如String s3 a b会直接在编译期变成ab所以s3 ab是true。但如果是变量拼接编译器在JDK 8之后会转为StringBuilder.append()这时就是new出来的对象引用肯定不同。这个点在选择题里出现的频率非常高属于典型秒杀题。另外equals与hashCode的契约也是常客——重写equals时必须重写hashCode否则HashSet、HashMap会出现逻辑混乱。我记得有一道题就是问“两个对象equals为true但如果hashCode不同放进HashMap会怎样”答案是它们会被放到不同的桶里同一个HashMap中出现两个逻辑上相等的key导致get时无法正确取回。2.3 集合框架ArrayList、LinkedList、HashMap核心考点集合这块是整套选择题的大头。用友至少出了三四道题ArrayList和LinkedList的区别、HashMap底层结构、HashSet如何保证元素唯一性、以及集合遍历时修改元素会抛什么异常。先说ArrayList和LinkedList的选择题。平时大家都背过“ArrayList适合随机访问LinkedList适合插入删除”但题里会往深一层考都是线程不安全的ArrayList扩容默认是原来的1.5倍。LinkedList虽然在头尾插入是O(1)但如果你要按下标插入得先从头或尾遍历过去复杂度还是O(n)所以“LinkedList一定比ArrayList插入快”这个说法是错的要看插入位置在哪。ArrayList的subList返回的是内部视图不是新List你在这个视图上做结构性修改会影响原List同时如果你在视图创建之后又修改了原List再访问视图会抛出ConcurrentModificationException。HashMap是重头戏。选择题里常考的是默认加载因子0.75、默认初始容量16、JDK 8之后链表转红黑树的阈值为8、容量是2的幂次方。后面这些点经常会被浓缩成一道题“HashMap为什么容量必须是2的幂次方”答案是为了在计算数组下标时用(n - 1) hash代替取模运算位运算更快减一之后低位全是1能使hash值充分分散减少碰撞。类似的考法还有“HashMap在并发put时可能丢数据”、“JDK 7中并发put可能造成环形链表、JDK 8修复了这个问题但依然不安全”这些已经属于进阶题了但2018年用友的卷子里确实出现过链条成环的判断题。2.4 异常体系与try-catch-finally/return的执行顺序异常这块用友考了几种题型一是区分受检异常checked与非受检异常unchecked比如IOException、SQLException属于受检异常必须在方法签名里声明或捕获;而NullPointerException、IllegalArgumentException、ArithmeticException属于运行时异常编译器不强制处理。二是问catch块的顺序问题——子类异常必须放在父类异常之前catch否则编译报错因为父类会先“接住”所有子类异常后续的catch块永远不被执行。这道题在判断题里几乎百发百中。更值得关注的是try-catch-finally中的return问题。经典的代码是这样的public static int test() { try { return 1; } finally { return 2; } }执行结果不是1而是2。因为JVM在执行try的return之前会先去执行finally块如果finally块里也有return会覆盖try里的return值。但如果你只在finally里修改某个局部变量而不直接return结果就会有很大迷惑性。比如public static int test() { int result 1; try { return result; } finally { result 2; } }这里返回的依然是1因为return已经将result的值1压入了操作数栈finally里怎么改都影响不到栈里的值。这种题特别能考出一个人的JVM字节码理解程度用友之所以爱出是因为这种细节在日常调试中要么不碰、一碰就是疑难杂症。3. 简答题核心考点原理类怎么答、设计类怎么展开3.1 经典问题 和 equals 的区别这道题几乎是所有Java笔试的送分题但送分归送分想拿高分还是有技巧的。大部分人只会答“比较的是引用地址equals比较的是内容”。我从当年阅卷的角度告诉你这种答案只能拿一半分。出题人真正想看到的答案是分层的第一层在比较基本类型时比较的是值在比较引用类型时比较的是引用地址两者语义不同。第二层Object类中默认的equals()实现就是没有重写时两者等价。第三层像String、Integer这些类重写了equals()才实现了按内容比较所以你要看具体类有没有重写。第四层重写equals()必须同时重写hashCode()否则在散列集合中使用会出现逻辑错误。把四层都答出来并且能配上类似“String s1 new String(abc); String s2 new String(abc); s1.equals(s2)为true但s1 s2为false”的例子这道题基本就是满分。简答和笔试不同面试官真的会顺着你的回答继续追问所以不要只背结论把底层机制弄明白。3.2 经典问题HashMap的put流程与扩容机制这道题近年来已经从大厂面试流毒到校招笔试2018年用友简答题里也赫然在列。完整的put流程可以这样组织先计算key.hashCode()然后将hash值的高16位与低16位做异或扰动函数减少哈希碰撞。然后用(n - 1) hash计算数组下标。如果该位置为空直接放入新Node。如果已有元素先比较hash值和equals相同则覆盖value。如果是树节点走红黑树的插入逻辑。否则走链表逻辑遍历查找是否有相同key找到就覆盖找不到就尾插法新增节点并检查链表长度是否超过TREEIFY_THRESHOLD 8超过就转为红黑树。最后检查整个HashMap的size是否超过threshold超过就执行resize。扩容机制要从这三个点讲扩容发生在put之后、是懒加载、新容量为旧容量的两倍。JDK 7中扩容后需要重新计算每个元素的hash再定位JDK 8做了一点优化——不需要重新计算hash只需要看原来hash值新增的那一位是0还是1是0则位置不变是1则位置变成“原位置旧容量”。这个优化在回答的时候提一下会显得你对源码研究得比较深。数据迁移后要重新分配链表或树结构这也是HashMap为什么不适合并发的原因多个线程同时resize时可能丢失数据或形成环。虽然JDK 8通过尾插法避免了环形链表但数据丢失问题仍存在所以并发场景必须用ConcurrentHashMap。3.3 设计题单例模式的几种写法与线程安全性单例模式在早年笔试里几乎是必考题用友也不例外。简答题通常是“请写出几种单例模式的实现方式并说明它们的线程安全性。”这道题真正想看到的是你对不同实现方案的取舍理解。最基础的写法是懒汉式和饿汉式。饿汉式在类加载时就初始化实例写法简单、线程安全由类加载机制天然保证但会造成资源浪费。懒汉式是在第一次使用时才创建实例如果用非同步的方式实现线程不安全。加了synchronized修饰的懒汉式虽然保证了线程安全但每次获取实例都要加锁性能较差所以就有了双重检查锁double-checked locking但双重检查锁在JDK 5之前由于内存模型的缺陷可能会出现“半初始化”问题在JDK 5之后因为volatile语义的完整化而得到了修复。更推荐的方式是静态内部类public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }静态内部类既能延迟加载又线程安全而且性能好因为JVM在加载内部类时才会创建实例由类加载机制保证只创建一次。最后还可以提一句枚举方式enum实现的单例不仅能防反射攻击还能防序列化破坏是《Effective Java》作者Joshua Bloch强烈推荐的方式。把这些都答上这道简答题基本就稳了。3.4 多线程为什么synchronized和volatile常被放在一起考简答题里还有一类是问synchronized和volatile的区别。用友给的题比较典型“volatile能保证原子性吗synchronized呢两个关键字各自解决什么问题”这里一定要打破考生的一个误区volatile并不能保证原子性。它解决的只是可见性和有序性问题——当一个线程修改了volatile变量其他线程能立刻看到这个修改同时禁止指令重排序。但如果是count这种操作即使变量被volatile修饰依然不是原子的因为count实际上是“读-改-写”三步操作volatile保证不了三步的原子性。synchronized则同时保证了原子性、可见性和有序性。它通过锁机制让同一时刻只有一个线程能执行临界区代码通过内存屏障和锁的释放规则来保证其他线程能看到修改。所以当你面试或笔试的时候如果遇到“为什么有了synchronized还要用volatile”这种问题可以从性能角度解释绝大多数场景下volatile的开销低于synchronized因为它不需要上下文切换和锁的竞争所以在只需要保证可见性而不需要原子性的时候比如标志位、状态开关用volatile更合适。4. 编程题实战从读题到写码的完整流程4.1 题目一手写冒泡排序与优化冒泡排序是校招笔试里最常出现的排序题2018年用友的卷子也安排了一道。题目通常长这样“请实现冒泡排序并对其做优化使数组在有序时尽快结束排序。”很多同学直接写出了两层for循环然后就不管了只拿了基础分。下面我把加优化的写法拆一下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标志位如果某一轮遍历中一次交换都没有发生说明数组已经完全有序直接跳出循环。这样在最好情况下时间复杂度降到O(n)而不是始终是O(n²)。再进一步优化可以记录最后一次交换的位置该位置之后的元素已经排好下一轮只需遍历到该位置即可。这个优化能显著减少接近有序数组的遍历次数。这道题除了实现本身阅卷还会关注代码的健壮性比如是否判断了arr null、是否考虑了空数组。我当年阅卷时见过很多只写主体不带边界判断的代码这类写法在只追求功能时没问题但在笔试评分标准里一定会被扣分。4.2 题目二找出数组中重复出现的元素这道题用友的题干是“给定一个整数数组找出其中所有重复出现的数字时间复杂度尽量低。”它是典型的“时间换空间还是空间换时间”取舍题。基础解法是暴力双重循环每个元素和它后面的所有元素比较时间O(n²)、空间O(1)只适合数组特别小的情况。笔试中要想拿高分就应该用HashSet或HashMap的方式public static ListInteger findDuplicates(int[] arr) { ListInteger result new ArrayList(); if (arr null) { return result; } SetInteger seen new HashSet(); SetInteger duplicates new HashSet(); for (int num : arr) { if (!seen.add(num)) { duplicates.add(num); } } return new ArrayList(duplicates); }这里我用了一个小技巧seen.add(num)的返回值可以判断该元素是否已经在集合中如果返回false说明已经存在那当前元素就是重复值。再配合一个duplicates集合确保同一个重复数字只被加入一次。如果需要进一步降低空间复杂度可以先把数组排序O(n log n)再遍历比较相邻元素是否相等空间复杂度降到O(1)。具体用哪种方法取决于题目的约束条件。如果题目说“数组元素值在1到n之间”那还可以用原地标记法把元素值对应的位置标记为负数来识别重复这种做法在不允许使用额外空间的场景下最优。提前把这些思路都整理好笔试遇到类似题就能快速定位最合适的解法。4.3 题目三十进制转二进制的递归与位运算写法这道题也很有代表性当年用友考的是“输入一个正整数输出它的二进制表示方式”。初学者通常想到的是不断除以2取余数然后倒序拼装。基本功扎实的考生会写出递归版本更进阶的会使用位运算直接处理。我推荐一个简洁的递归版本public static String toBinary(int n) { if (n 0) { return 0; } return toBinaryInternal(n); } private static String toBinaryInternal(int n) { if (n 0) { return ; } return toBinaryInternal(n / 2) (n % 2); }思路是递归出口是n为0时返回空字符串每层递归都把当前数除以2后的二进制结果和当前最低位拼接。这种写法的可读性很强阅卷人一眼就能看懂你的递归思路。如果你更熟悉位运算也可以用Integer.toBinaryString()这个API直接得到结果但笔试通常要求手写实现直接用API会显得你没有掌握原理。位运算版本的核心是不断右移并取最低位public static String toBinaryBitwise(int n) { StringBuilder sb new StringBuilder(); for (int i 31; i 0; i--) { sb.append((n i) 1); } String result sb.toString().replaceFirst(^0, ); return result.isEmpty() ? 0 : result; }这个版本从最高位一路取到最低位每一轮通过(n i) 1取出第i位的值。好处是循环次数固定、没有递归栈开销但要注意前导零的去除。这版更贴近计算机底层的处理方式属于加分项。4.4 手写代码的细节与得分点我改过不少笔试代码负责任地讲写得对不等于写得高分阅卷标准里通常有一个隐性要求——代码风格。用友这套编程题的满分答案通常具备以下几个特征方法命名符合小驼峰规范、变量名有意义而不是a、b、x;有边界条件判断null判断、空数组判断;关键逻辑有简短的注释;代码结构清晰能拆出独立方法时就拆。另外还有一个常被忽视的点——异常处理。编程题里如果可能出现除零、数组越界、空指针最好主动加上判断或抛出自定义的IllegalArgumentException。这不仅是给阅卷人看的也是你实际工作水平的一种体现。Java程序员如果连基本输入校验都不做放到工程项目里是要出事故的。笔试时可能会有编译环境写完代码一定要手动敲一遍检查有没有漏分号、引号不匹配这些低级错误。我当时见过很多考生思路完全正确但代码在IDE里跑不起来只能拿一半分非常可惜。5. 校招Java笔试的避坑指南5.1 时间分配最容易翻车的一环先从时间分配说起。用友这套卷子120分钟题量按前文估算大概20道选择、5道判断、3道简答、1到2道编程。很多人的翻车点完全一致选择题第二题卡住了心里纠结“到底选B还是C”一纠结就是10分钟后面节奏全乱。我自己的做法是先做编程题。因为编程题分值最高而且只要思路通写起来相对快。我会先花5分钟把题干和要求读两遍确认输入输出格式然后直接开写;写完之后调试到能跑通为止再去回头做选择和简答。这种方法确保最值钱的题目不因为时间不够而空着。快做完客观题之后留出至少15分钟检查一遍选择题有没有看错“正确的是”“错误的是”、判断题是不是被绕进去了、编程题有没有漏掉边界条件。校招笔试的淘汰率高很多时候不是你不会而是粗心丢分。5.2 简答题怎么答才不被扣分简答题最忌讳只写结论不写理由。比如“和equals的区别”你只写“前者比地址后者比内容”那只能得一半分。阅卷人想看的是你对知识点有体系化的理解所以哪怕时间再紧也要在关键概念后加一句“为什么”或“例子”。另外一个很重要的技巧是分点作答。简答题用“第一、第二、第三”或“1. 2. 3.”分点阅卷人扫一眼就能看到你的要点是否齐全也方便给分。用友的阅卷当时是按点给分的你没分点他很可能就漏看了你写的内容。笔试不是写作文不需要过度铺垫开头一句话总结后面直接列要点和例子这种答题习惯在校招、社招笔试里都吃香。5.3 常被忽略的高频丢分点结合我带校招生和批改笔试题的经验我再总结几个高频丢分点HashMap初始化容量没概念如果你知道要放很多数据却直接new HashMap()会频繁触发resize。更优做法是设初始容量比如预估100条数据设成128以上。用比较Integer超出127的数值Integer默认缓存了-128到127之间的对象超出这个范围用比较会返回false所以包装类型比较一律用equals。这个知识点在选择题里专门考过。忘记LinkedList也是Deque的实现类可以当栈或队列用push、pop、poll这些方法都是现成的。如果笔试考双端队列操作可以直接用LinkedList没必要自己写。finally里不要写return不仅会覆盖try里的返回值还容易让排查问题的人发疯。笔试如果问你这样做好不好答案一定是不好。遍历集合时不要用list.remove()用Iterator的remove()方法否则会抛并发修改异常。如果用了增强for循环想删元素会直接抛异常。这个点经常出现在纠错题中。5.4 我从这套题里总结出的备考路线最后说说我对这套题的总体评价和备考建议。用友2018年的这套Java笔试题风格上不是“广而深”而是“广而稳”每个知识点都不超纲但每个知识点都被抠到了容易出错的地方。它不要求你是算法竞赛选手但要求你把Java核心类库和语法真正吃透。我当时带校招生时会推荐这样一条复习路线先过一遍《Java核心技术 卷I》的基础章节重点看对象、继承、接口、异常、泛型、集合、并发这几块;然后用IDEA写10个左右的小练习比如手写synchronized和volatile的对比示例、实现一个线程安全的单例、用TreeMap和HashMap分别做一次词频统计;最后把常见的笔试编程题刷一遍冒泡、快排、二分查找、链表反转、括号匹配这几个经典题目必须手写熟练。这套组合基本覆盖了80%以上校招Java笔试的考点不止是用友对其他企业的笔试同样适用。就我自己改卷的体验来说一道简答题能不能拿满分往往不在于你背诵了多少面试题而在于你**有没有从这个知识点出发去延伸出“为什么”**的能力。这种能力不是临时压题能压出来的得靠平时写代码时多想一步、多查一次源码。好在Java的源码质量极高看JDK源码本身就是很好的学习过程。