
用友2018秋招Java笔试题七这套题我在整理的时候特意多看了几遍。原因很简单它把Java求职者最容易翻车的几个点都点名了数组和指针的区别、枚举类型的使用、运算符与表达式、冒泡排序和快速排序的手写、数组越界异常甚至还有JVM内存溢出。你说它难吧没有一道是偏题怪题你说它简单吧能把这套题答得滴水不漏的人我招过的应届生里真不多。用友是典型的企业级Java技术栈公司像NC、U9、U8 Cloud、YonBIP这些产品线全是Java系出身所以它出这套题的核心意图就一句话不是看你背了多少网上流传的Java八股文而是看你能不能把一个Java程序员的基本功讲明白、写清楚。这篇内容适合正在准备秋招春招Java岗的同学也适合做技术复盘的在职开发者我会把每类题背后的考察点、标准思路和真实的坑逐一拆开讲。1. 用友这套笔试题的出题逻辑为什么“基础”才是试金石1.1 用友技术栈的底子为什么是Java先搞清楚一个背景用友不是互联网公司那种“面试造火箭、入职拧螺丝”的路子它是做企业级软件和云服务的产品面向的是大型企业的财务、人力、供应链、制造这些核心业务环节。这类系统有什么特点并发量大、业务规则复杂、部署环境五花八门而且一套系统要跑好多年。这种业务形态决定了技术选型必须稳定Java在后端领域积累的生态、社区和人才储备正好是这类企业最需要的。所以它的笔试题目一般不会考你“最新最热”的框架比如某个刚出的微服务组件也不会考什么冷门的算法题而是把Java最核心的语法、集合、JVM、常见算法这些基本功翻来覆去地考。这套2018秋招Java笔试题七就是典型代表题目覆盖面广但基本控制在校招大纲范围内没有超纲题。这样出的好处是筛选效率高因为Java基础扎实的人上手企业级项目通常也不会差基础不牢的人即使简历写得再漂亮一考就露馅。1.2 这类笔试的四个共性特征我复盘过多家用友风格的笔试题之后总结出四个共性特征这套题基本占全了。第一范围宽但不超纲。语法、集合、并发、JVM、算法全考到但每一道题都是教科书级别的知识点不会拿一个特别偏门的API来刁难你。第二追着细节问。比如数组和指针的辨析不是问你“Java支持指针吗”这种送分题而是把二者放到具体场景里让你判断对错一个细节没想清楚就会选错。第三代码题不考奇技淫巧。冒泡排序、快速排序都是最经典的写法但它考察的是你能不能写出正确、完整、边界处理得当的代码而不是能不能写出花哨的优化。第四把“异常信息”当考点。这套题里很多题其实是从真实开发中的报错信息提炼出来的比如数组越界、内存溢出、JDK版本不匹配导致的编译失败考察你对这些报错有没有真正的排错经验。1.3 面对这套题的正确姿势针对这种出题风格我的备考建议是不要背题目要能讲清楚为什么。很多同学复习的时候喜欢背“标准答案”比如HashMap的原理背得滚瓜烂熟但被问到“为什么容量是2的幂”就卡壳。这套题恰恰喜欢在同一知识点上深挖一层把答案当成索引把背后的原理当成血肉才能真正应对这种考法。我在帮别人复盘时经常打一个比方八股文是导航地图上的路名原理是你脚下真实的路。光会背路名遇到修路、封路就不会走了把路踩实了题目怎么换都能到终点。下面几个章节我会按照这套题的典型考点逐个讲解核心知识点和答题思路。2. 语言基础题最容易拿分也最容易丢分的部分2.1 数组和指针的区别Java程序员经常被C/C“带偏”的题这套题里有一类题对学过C或者C的同学特别不友好就是数组和指针的辨析。题目通常会给你几个陈述句让你判断比如“Java中的数组本质上是一个指针”“数组名是一个指向首元素的指针”“Java中可以通过指针运算遍历数组”等等。如果你脑子里还带着C语言的模型很容易觉得“数组名不就是地址吗”然后选错。实际上Java里的数组是一个对象它有自己的类型、length属性以及内存中连续存储的元素空间。数组引用变量保存的是对象的引用这个引用和C语言里的指针有本质区别你不能对引用做加减运算也不能通过引用去访问任意内存地址一切访问都要通过JVM的数组边界检查去完成下标越界直接抛ArrayIndexOutOfBoundsException也就是Java中数组越界异常。这就是为什么要区分“引用”和“指针”引用是安全的、受控的指针是不受控的。答题的时候我建议这样组织答案先下结论Java中没有指针数组是对象而非指针再解释引用与指针的区别强调安全性和边界检查最后补充数组在内存中的模型包括引用变量存放在栈上、数组对象本身存放在堆上。这样层层递进既展示了你在校招笔试里应有的严谨度也顺便为可能的面试追问做了铺垫。2.2 运算符与表达式自增自减、短路求值、三目运算符Java运算符和表达式这块属于看着简单、考起来全是坑的部分。用友这套题里有一道非常经典的自增题我每次让候选人做错误率都很高int i 0; i i; System.out.println(i);输出是多少答案是0。很多人想当然地说“i先赋值再加所以是1”但实际上Java的运算过程是先把i的值0压入操作数栈然后i自增变成1最后再把操作数栈里的0赋值给i所以i又被覆盖回0了。这里考的就是JVM操作数栈和局部变量表的交互单靠大脑模拟很容易翻车。短路求值也是高频题比如boolean a false; int count 0; if (a (count 0)) { // 不执行 } System.out.println(count); // 输出0因为左边为false右边的count根本不会执行这就是短路效应。很多人能把规则背出来但放到具体表达式里还是会忘记右侧表达式的副作用。答题时要留意凡是出现、||、三目运算符的题先判断哪一边会被“短路”再考虑变量的最终值。还有三目运算符的类型转换陷阱比如Object obj true ? new Integer(1) : new Double(2.0);结果不是Integer而是Double因为三目运算符会把两个分支的类型做自动提升。这类题考察的是对Java类型系统的理解做题时尽量用“类型统一后再赋值”的思路去推导而不是直觉判断。2.3 枚举类型与Lambda表达式语法糖背后的设计意图Java枚举类型的使用在笔试题里通常不是考你“怎么定义一个枚举”而是考“为什么要有枚举”和“枚举能做到什么”这两个角度。最常见的题目是用枚举实现单例模式或者用枚举定义一个状态机。比如这道题为什么用枚举实现的单例被认为是最好的单例写法之一答案需要从四个层面说线程安全、序列化安全、反射安全、代码简洁。普通单例要处理volatile、synchronized、readResolve这些问题枚举在JVM层面就帮你把这些问题都挡掉了因为枚举实例的创建是JVM保证的而且枚举类型在序列化时有特殊处理反射也无法通过newInstance创建枚举实例。Lambda表达式这里笔试题喜欢考两个点一是函数式接口的判断二是变量捕获。比如int x 10; Runnable r () - System.out.println(x); x 20; // 编译报错为什么报错因为Lambda捕获的局部变量必须是effectively final也就是初始化之后不能再被修改。这个设计是为了保证线程安全因为Lambda可能会在另一个线程中执行如果它捕获的变量可以随意变就会产生数据竞争。答题时能点出“有效final”这个术语再解释一下设计意图这道题基本就稳了。3. 集合、并发与JVM笔试中的“拉开差距区”3.1 HashMap原理从“怎么存”问到“为什么这么存”如果说语言基础题是热身那HashMap就是笔试的分水岭。用友的题里不会简单地问“HashMap的底层数据结构是什么”而是会问你put一个键值对时整个流程是怎么走的。标准流程是这样的先对key计算hash值然后通过(n - 1) hash定位到数组下标如果该位置没有元素直接放入如果有元素就判断key是否相同相同则覆盖value不同则以链表或红黑树的方式追加当链表长度达到8且数组长度达到64时链表转红黑树。扩容时容量变为原来的两倍元素会重新计算位置。但光背流程还不够笔试真正拉开差距的是“为什么”。比如数组容量为什么必须是2的幂因为hash % n在n是2的幂时等价于(n - 1) hash而位运算比取模运算快得多。又比如为什么要用红黑树因为极端情况下大量key哈希碰撞链表会变得很长查询从O(1)退化到O(n)红黑树能保证最坏情况下的查询复杂度是O(log n)。这道题有一个非常容易踩的坑就是你光回答“数组加链表、JDK 8加了红黑树”面试官如果追问“JDK 7和JDK 8的扩容有什么区别”很多人就卡住了。所以笔试阶段我建议大家把“为什么”这个维度一次性准备到位回答时主动提一下头插法和尾插法的区别展示你对演进过程的了解。3.2 并发编程synchronized和Lock怎么选并发题也是这类笔试的常客。最常见的形式是给一段多线程代码问你运行结果是什么、有没有线程安全问题、怎么解决。这里有一个高频考点synchronized和ReentrantLock的区别以及各自的适用场景。对比维度synchronizedReentrantLock锁的获取与释放自动JVM管理手动需要lock/unlock配合是否可中断不可中断可以lockInterruptibly是否支持公平锁非公平支持公平和非公平条件变量只能配合wait/notify支持多个Condition性能JDK 6之后优化明显高并发场景下更灵活笔试时答这道题光列区别不够还得加一句个人理解synchronized解决的是“用起来简单”的问题ReentrantLock解决的是“灵活可控”的问题。大多数场景下先选synchronized只有需要超时、可中断、多条件队列这些高级能力时才考虑ReentrantLock。这样回答看起来就像一个真正写过并发代码的人而不是在背诵。另一个高频点是volatile特别是它的可见性和有序性。我之前看到一套类似的题会问volatile能保证原子性吗标准答案是“不能只能保证可见性和有序性”。比如volatile int count两个线程同时执行count仍然是线程不安全的因为count是读-改-写三步操作不是一个原子操作。这个点一定要用直白的例子说清楚避免被追问时含糊。3.3 JVM内存区域与OOMOutOfMemoryError怎么答JVM这块我要重点说一下“java: outofmemoryerror: insufficient memory”这个常见报错。很多人在本地跑程序时遇到这个错误第一反应是“内存不够了加大点”然后就去找IDE的启动参数。这个方向没错但笔试更希望你回答的是内存不够具体是哪个区域不够怎么定位先记住内存区域的划分堆、虚拟机栈、本地方法栈、方法区JDK 8之后是元空间、程序计数器。对应的溢出类型分别是堆溢出OutOfMemoryError: Java heap space、栈溢出StackOverflowError、元空间溢出OutOfMemoryError: Metaspace。不同区域的溢出原因和排查思路完全不同。举一道典型的场景题线上系统频繁抛出OutOfMemoryError: Java heap space请问怎么排查我的回答套路是四步走。第一步用jstat -gcutil pid观察GC情况确认是老年代持续增长还是年轻代频繁Full GC第二步用jmap -dump:formatb,fileheap.bin pid导出堆快照第三步用MAT或JProfiler加载堆快照查看大对象和对象引用链第四步根据对象类型判断是内存泄漏还是内存不足。如果对象是业务实体且数量巨大可能是存在未释放的引用如果是大数组或缓存无上限就要从代码层面做容量控制。这种题考察的是实战排查能力而不是单纯背概念。回答的时候尽量把工具名称和操作步骤带上会让面试官觉得你不是只会背书而是真处理过线上问题。我在平时带人时也反复强调JVM题最忌讳只答一句话“就是内存不够了”你要让考官看到你脑子里有一张完整的排查地图。4. 手写算法题冒泡排序与快速排序的标准答案4.1 冒泡排序优化点才是得分点用友这套题里的手写算法很少出动态规划这类高难题目冒泡排序和快速排序是出场率最高的。冒泡排序本身不难但很多人写出来能跑却拿不到满分因为少了优化意识。基础版很简单双层循环外层控制轮数内层做相邻比较和交换。但完整答案应该加上一个“是否发生交换”的标志public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 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; // 这一轮没有交换说明已经有序 } } }这个优化有一个容易忽略的考点如果数组本身已经有序优化后的时间复杂度是多少答案是O(n)因为第一轮扫描后swapped为false立刻退出循环。而基础版无论有序无序都要执行n-1轮比较耗时不稳定。笔试时把这个点主动说出来就是加分项。另一个进阶优化是记录本轮最后一次交换的位置下轮比较只用到这个位置。这样已经排好序的尾部区域就不用再参与比较。你可以把这个点作为“如果面试官追问还能怎么优化”的备选方案体现思考的完整性。4.2 快速排序边界条件和递归写法快速排序在笔试题里的地位比冒泡更高因为它涉及递归、分治、指针移动三个核心思想。手写快排最忌讳的是只记住模板一换基准值选取方式就写不对。一个稳定的写法是取中间位置的元素作为基准值public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[(left right) / 2]; int i left; int j right; while (i j) { while (arr[i] pivot) { i; } while (arr[j] pivot) { j--; } if (i j) { int temp arr[i]; arr[i] arr[j]; arr[j] temp; i; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }这段代码有几个关键点必须注意。第一个是while (i j)这里用的是小于等于不是小于否则左右指针可能不会交叉导致递归区间划分错误。第二个是内层while必须写 pivot和 pivot不能写成和否则遇到大量相等元素时会陷入无限交换。第三个是递归终止条件left right这个边界不对的话数组下标很容易越界运行时就会抛数组越界异常。笔试时如果你写错了常见的报错就是ArrayIndexOutOfBoundsException。这时候先检查基准值是不是取中间元素、递归区间是否正确、循环条件是不是写反了。我自己在帮人改代码时至少有一半的快排Bug都出在这三个地方。4.3 在线笔试的环境细节输入输出与JDK版本手写算法题还有一个隐形考点就是在线笔试环境本身。很多学校的同学平时用IDE写代码习惯了自动补全和格式化一上在线编程题就水土不服。这里有几个实操层面的经验值得提前知道。第一输入输出格式要看清。牛客网、赛码网这类平台的输入是标准输入流和LeetCode那种只写函数体的形式不一样。如果题目要求从标准输入读取数组你要用Scanner或BufferedReader逐行解析拿到数据后再调排序方法。输出时注意用空格分隔还是换行分隔多一个空格在某些裁判机上也算错虽然大多数平台会忽略尾部空白但稳妥起见还是严格按照题目要求输出。第二JDK版本差异。这套2018的题在当年可能用的还是JDK 8而你现在本地装的可能已经是JDK 17。这里有一个高频报错叫“源发行版17需要目标发行版17”本质上是你当前项目的编译级别和JDK版本不匹配。如果在本地练习时遇到命令行里试试javac -source 8 -target 8或者检查IDE的project structure里Language level设置。第三递归算法的栈深度。快速排序在最坏情况下的递归深度接近n如果数据量是百万级别可能出现栈溢出。笔试时如果遇到“手写快排并对超大数据量排序”可以考虑改用非递归实现不过校招笔试一般不会卡到这么大但你心里要有个数。5. 常见问题与排查技巧实录5.1 编译器和运行时报错的真实场景这套题里不少题目是从真实报错场景中提炼出来的和开发中遇到的坑高度重合。我把热词里高频出现的几类问题整理了一下个个都是实战中会遇到的。数组越界异常ArrayIndexOutOfBoundsException是最常见的一个。笔试里常考的方式是给你一段看起来正确的数组遍历代码问运行会怎样。比如循环条件写成for (int i 0; i arr.length; i)看似只差一个等号但最后一位下标arr.length已经越界。这是校招笔试题最喜欢埋的“小坑”考察你对数组下标的敏感度。解决思路很简单出问题先数一遍下标范围从0到length-1别凭直觉。还有一个热词是“java: you arent using a compiler supported by lombok”这是Lombok在编译器不匹配时的典型报错。Lombok在编译时通过注解处理器生成代码如果你的JDK版本太新或太旧Lombok版本跟不上就会报这个错误。解决方法是升级Lombok版本或者降级到Lombok支持的JDK版本。这个报错虽然不是用友笔试里的直接考点但笔试环境如果让你配置项目就很可能撞上。“java: internal error in the mapping processor: java.lang.nullpointerexception”和“java: 警告: 源发行版 17 需要目标发行版 17”也都是环境相关的常见坑。前者通常是MapStruct等注解处理器和Java版本冲突后者是说当前项目编译器级别与目标版本不匹配。遇到这些报错不要慌按“版本不匹配”这个方向去排查大部分都能解决。我建议平时练笔试题时多关注自己的编译环境不然辛辛苦苦写出的排序算法可能因为环境问题连编译都过不了。5.2 笔试作答策略与时间分配除了知识点本身答题策略也能拉开不少差距。用友这套题我估了一下题型大致是单选、多选、判断、简答和编程题题量适中但题干信息量不小。我的建议是先做会做的快速拿分。单选和判断这类客观题遇到犹豫的不要死磕先标记跳过把时间留给后面的编程题。因为编程题不仅考察正确性还考察代码风格和完整性一题往往能抵好几道选择题的分值。编程题拿到手先审题明确输入输出格式再动手写。写之前可以先在脑子里过一遍边界条件比如数组为空、只有一个元素、已经有序、全部相等这些情况代码里提前处理掉能避免很多隐形的功能扣分。另一个技巧是不要把答案只写成一个孤零零的方法。如果时间允许把输入的读取、数据的处理、结果的输出分步写好并加上关键注释。企业笔试的阅卷人通常能分辨出“会写代码”和“背了模板”的区别写上的每一步逻辑都清晰就算最终结果有瑕疵分数也不会太难看。5.3 避坑速查表这套题的高频错误在哪里考点常踩的坑正确的回应姿势数组与指针用C语言思维认为数组名就是指针强调数组是对象、引用不参与指针运算i i以为是1实际是0从操作数栈和局部变量表解释短路求值忽略右侧表达式副作用先判断能否短路再算变量的最终值枚举单例只回答“可以单例”说不出为什么从线程安全、序列化安全、反射安全、简洁四方面答Lambda变量捕获不理解effectively final强调捕获变量不能变原因在于线程安全HashMap容量只记“2的幂”说不出原因说明位运算代替取模的高效性volatile认为能保证原子性明确仅保证可见性和有序性原子性要用锁或原子类快排边界递归区间出错导致数组越界检查循环运算符、递归终止条件、基准值位置OOM只笼统说“内存不够”定位是堆还是元空间用jmap、jstat、MAT排查这张表是我在复盘这套题的过程中总结出来的基本覆盖了大多数候选人丢分的重灾区。每一条背后都有至少一道题或一个真实的线上故障支撑不是凭空总结的。建议你按这个维度给自己做一张个性化表格把每道错题填进去考前集中过一遍比盲目刷十套新题更有针对性。另外想单独提一下HashMap容量为什么是2的幂这个问题因为它在笔试题里问到的次数实在太多了。数组下标定位是(n - 1) hash当n是2的幂时n-1的二进制是全1这样和hash做与运算结果相当于hash的低位能均匀分布。如果不是2的幂比如n是10n-1的二进制是1001中间有一位永远是0那某些位置永远不会有元素加大了哈希碰撞的概率。这个细节答出来面试官对你的印象会完全不同。我在实际整理这套题的时候最大的感受是企业真实需要的Java能力和你在刷题软件里练的八股文其实是两码事。八股文帮你拿面试但能不能把“数组为什么越界”“HashMap为什么要2的幂”“Lambda为什么不能捕获非final变量”这些基础讲透才是能不能真正上手企业级项目的关键。这套用友2018秋招Java笔试题七虽然已经过去几年但它的出题思路反而更清晰地说明了一件事基础不牢地动山摇。建议你在备考时每做一道题都多问自己一层为什么把错题按“考点是什么、原理是什么、如果面试追问会问什么”的方式记下来这样练十分钟的效果可能比闷头刷一小时题都好。