十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用友校招Java笔试题复盘:基础原理与工程实践全解析

用友校招Java笔试题复盘:基础原理与工程实践全解析 1. 这套卷子的命题逻辑用友校招Java笔试到底在筛什么样的人先说一个很多人容易误解的地方用友这类做ERP、财务软件、云服务起家的ToB公司校招笔试和互联网大厂笔试的筛选逻辑其实不太一样。互联网大厂喜欢用算法题海选动辄Hard级别的动态规划用友的Java笔试题却明显偏向“基础是否扎实、原理是否理解得透、动手写代码是否规范”而不是看你会不会背冷门API或者刷了多少道LeetCode。我复盘的这套2018秋招Java笔试题整体风格可以用四个字概括返璞归真。整张卷子几乎没有偏题怪题但想拿高分并不容易。原因在于很多题目表面上是选择题实际却暗含了“你以为你懂但写出来就错”的陷阱。比如基础语法部分的运算符优先级、自增自减混合运算还有集合框架里的HashMap扩容细节都是典型例子。1.1 从企业业务形态反推考点分布如果你了解用友的业务结构就能理解它为什么这样出题。用友的核心产品线包括U8、NC、U9 Cloud等都是面向企业客户的重量级管理系统。这类系统的特点是并发用户多、业务逻辑复杂、数据一致性要求高、系统需要长期稳定运行。所以笔试命题时会格外关注几个层面的能力Java基础是否扎实能否驾驭大规模业务代码集合、并发、JVM等底层原理是否清晰因为企业级应用躲不开这些问题对象设计能力因为ERP系统动辄成百上千个实体类关系建模是基本功手写代码的规范性因为交付给客户的是可持续维护的代码不是竞赛题解。这套卷子基本就是按这个思路排布的基础语法题占30%左右面向对象题占20%左右集合与JVM占20%左右算法手写占15%左右其余是数据库和综合题。比例上可能和现在很多公司略有不同但核心逻辑没变考察的不是“你记住了多少”而是“你能不能上手干项目”。1.2 题型结构与时间分配策略那年的用友笔试题型大致分为四类单选题、多选题、简答题、手写编程题。总时长90分钟题量在40道上下。我当时最明显的感受是单选和判断部分花的时间比预想的多因为不少题都在“看似正确”和“实际正确”之间摇摆。这里有一个实际的应试策略现在回过头看依然有效先做手写编程题因为算法题最怕时间不够导致逻辑断裂再做简答题这类题踩点给分能写多少写多少最后做选择题尤其是多选题宁少选不漏选不确定的选项不碰。很多同学习惯从第一题按顺序做结果最后编程题只剩十分钟这是笔试的大忌。编程题分值高、区分度大必须优先保。后面我会专门讲冒泡排序和快速排序的那两道手写题可以对照自己现在的水平评估一下。2. 基础语法题运算符、等于判断和数组越界丢分重灾区基础部分的题目看起来人畜无害实际上是最容易丢分的地方。这套卷子里的几个典型陷阱这么多年过去依然在各类Java面试题里反复出现很值得展开讲透。2.1 自增自减与短路运算一道题的多种变形卷子里有一道让我印象很深的题目大致是这样的int i 0; int j i i; System.out.println(j); System.out.println(i);这道题在面试八股文里已经快被讲烂了但每次笔试都还有人错。它的核心在于操作数求值顺序和自增自减的副作用时机。拆解一下i先取当前值0参与运算然后i变为1i先让i从1变为2再取2参与运算所以j 0 2 2最终i 2。为什么强调这个因为实际开发中几乎不会有人写这种代码但笔试考它本质是在考察你是否真正理解“表达式求值”的底层规则而不只是背结论。类似的变形还有int a 5; boolean b (a 3) (a 5);这里如果改成而不是结果会完全不同。是短路运算左边为false时右边根本不会执行则两边都会执行。我在复盘时发现很多同学能分清和的区别但一旦和自增混合在一起就容易把优先级搞混。再看一段来自这套卷子风格的经典填空题int x 3; int y x x x--; System.out.println(y); // ?一步步算第一个x取3参与运算x变4接着xx先变5再参与运算最后x--取5参与运算x变4。所以y 3 5 5 13最终x 4。这种题没有技巧只能靠平时多写多拆。我建议备考时不要只看答案要自己画一张表格把每一步的“取值”“自增/自减后的值”列出来动手推两遍。2.2 与 equalsInteger 缓存池是高频陷阱用友这卷子的选择题里有一道经典Integer比较题几乎可以算是Java笔试题的“必修课”Integer a 100; Integer b 100; Integer c 200; Integer d 200; System.out.println(a b); // ? System.out.println(c d); // ?答案是true和false。根本原因在于Integer内部实现了IntegerCache缓存池范围是-128到127。在这个范围内Integer对象是缓存的比较的是引用相等超出范围后new出了新对象引用自然不等。其实这道题考的不是equals和的区别而是自动装箱时缓存机制的边界意识。如果只看a b是true可能还绕得过去但c d是false恰恰说明底层是走了不同的对象实例。我在复盘时把相关知识点串了一遍发现这几个方向都是高频考点基本类型直接用比数值包装类型用equals比数值包装类型和基本类型混合比较时包装类型会自动拆箱注意Long也有类似的缓存Character缓存范围是0到127。2.3 数组越界异常的底层逻辑与答题写法热词里“数组和指针笔试题”“java数组越界异常”这类词热度一直居高不下说明这是很多人的痛点。用友这套题里出了这样一道简答题请说明ArrayIndexOutOfBoundsException的触发场景并给出至少三种避免方式。很多人读到这题就开始写“数组下标超过长度”然后就没有然后了。这题想拿高分必须分层次回答触发场景访问数组时下标为负数或者下标大于等于array.length。JVM在每次数组访问时都会自动做边界检查不满足条件就会抛出这个运行时异常。避免方式循环遍历时使用for-each从机制上避开下标管理必须用下标时以array.length作为边界条件并且留意“长度”和“最大下标”之间的1差涉及动态索引比如用户输入、算法计算出的下标时先判断范围再访问。回答加分点可以补充说生产环境里遇到这个异常往往不是真正的“越界”而是业务逻辑里的索引算法有问题。比如二分查找中low和high算错或集合转数组后的下标映射错位。我自己当年在复盘这类题时有个体会面试官问你异常不是想听你背异常名而是想看你有没有系统的防御性编程意识。能主动说出“先判断、再访问”“用增强for循环避免手动管理下标”这类思路比只写对定义得分高一个档次。3. 面向对象与对象关系聚合、继承、多态背后的设计观面向对象几乎是所有Java笔试题的必考板块用友这套题也不例外。不同的是它考察的粒度比较细除了封装继承多态这些基础概念还专门考了对象之间的关系尤其是“聚合”和“组合”的区别。3.1 聚合与组合别只会背定义这道简答题是这样的一辆汽车包含发动机和车轮一个公司包含多个员工。请分析“包含”关系在面向对象设计中的两种类型并说明它们的生命周期差异。不少背过概念的人能写出“聚合是整体与部分的关系部分可以独立存在组合是整体与部分的关系部分不能独立存在”。但下一问就露馅了请举例说明如何用代码体现这种区别。正确的打开方式是聚合Aggregation整体和部分生命周期不同步。比如公司与员工公司倒闭了员工依然存在可以跳槽去别的公司。代码上通过构造方法或setter传入外部对象class Company { private ListEmployee employees; public Company(ListEmployee employees) { this.employees employees; } }组合Composition整体和部分生命周期同步。比如人与心脏人没了心脏也不存在。代码上通常在构造方法内部创建部分对象class Person { private Heart heart; public Person() { this.heart new Heart(); } }这道题真正的考点是**“对象生命周期管理”**。在企业级系统里像ERP中的订单与订单明细通常就是组合关系而客户与联系人往往是聚合关系。笔试时如果能答到“组合关系中外部销毁整体时部分也应被回收或同步处理”就能和只会背概念的人拉开差距。3.2 多态的动态绑定简答题的得分点“什么是多态”这种题太基础了用友这套题问得更深一层请解释Java中多态的实现原理并说明成员变量、静态方法、实例方法在编译期和运行期的绑定差异。这道题直接决定了你在面向对象部分能不能拿高分。多数人能答出“父类引用指向子类对象”但绑定规则才是核心成员变量编译期绑定看引用类型。Parent p new Child();访问p.age时取的是Parent的age即使Child里也有同名字段。静态方法编译期绑定看引用类型。静态方法是类级别的不能用多态来解释。实例方法运行期绑定看实际对象类型。这正是多态的实现基础。JVM运行期绑定靠的是**方法表vtable**机制。类加载时JVM会为每个类在方法区生成方法表存储实际方法的入口地址调用invokevirtual指令时根据实际对象的类型找到对应的方法表再定位到方法实现。子类重写方法后方法表中的入口地址指向子类实现调用时就会动态分派到子类方法上。这个知识点放到后端的日常开发中意义非常明显框架里到处都是接口引用 具体实现的模式Spring的依赖注入本质就是多态的极致运用。笔试把多态原理吃透后面学框架会轻松很多。3.3 抽象类还是接口判断标准是什么还有一道选择题是判断抽象类和接口的使用场景选项里有几个干扰项比如“抽象类不能有构造方法”这种错选项。这里容易出现一个误区很多人以为抽象类不能实例化就等于没有构造方法其实抽象类是有构造方法的只不过不能直接new它用于子类实例化时的父类初始化调用。判断该用抽象类还是接口我给一个最简单实用的标准关注“是什么”的层级关系用抽象类。比如猫和狗都是动物共同行为可以下沉到抽象类Animal。关注“能做什么”的能力契约用接口。比如会飞、会游泳这是能力可以任意组合叠加。用友这类企业系统的代码库里接口无处不在业务服务接口、仓储接口、远程调用接口。因为ERP业务复杂模块之间必须靠接口解耦否则改动一个模块就对整个系统产生连锁影响。笔试考这个问题本质上还是在考你有没有接口意识。4. 集合与JVM内存HashMap、并发容器和OutOfMemoryError集合框架是Java笔试的常青树用友这套题对集合的考察不算偏但有一个明显倾向喜欢把集合和JVM内存问题结合起来考。这可能跟ToB系统的运行环境有关——客户端现场部署、长时间运行、数据量大内存问题一旦爆发就是大事故。4.1 HashMap在JDK 7与JDK 8中的差异这套卷子里有一道高频简答题HashMap的底层结构是什么JDK 8中做了哪些优化为什么标准回答拆成三部分JDK 7数组 链表采用头插法。扩容时链表顺序会反转并发扩容下可能出现循环链表导致get操作死循环CPU飙高。JDK 8数组 链表 红黑树。链表长度超过8且数组长度超过64时链表转为红黑树把查询时间复杂度从O(n)降到O(logn)。采用尾插法避免扩容时链表反转。为什么阈值是8因为负载因子0.75下链表长度达到8的概率极低服从泊松分布概率约为千万分之六设8既避免频繁树化又能在极端哈希冲突时兜底。关于HashMap的题我建议再延伸一下“为什么HashMap线程不安全”。常见答案有两个角度一是多线程同时put可能导致数据覆盖二是扩容时多个线程同时rehashJDK 7环境下可能形成环形链表。此外modCount机制导致fail-fast迭代时其他线程修改集合会抛ConcurrentModificationException。这些补充点都是加分细节。4.2 内存溢出vs内存泄漏从一道诊断题说起热词里有“java: outofmemoryerror: insufficient memory”说明很多人实际工作中都撞到过内存问题。用友这套卷子里有一道案例分析题大意是一个长期运行的服务在某个时间段后频繁抛出OutOfMemoryError: Java heap space日志里能看到大量无法释放的ArrayList对象。请分析可能的原因并提出排查思路。这道题不考死概念考的是排障思路。回答时最好分步走判断是内存溢出还是内存泄漏。内存溢出是“确实需要的内存超过堆上限”内存泄漏是“本该被回收的对象被无意识持有导致可用内存越来越少”。两者的外在表现都是OOM但前者是内存不够用后者是内存被“偷走”了。用工具定位。先通过jstat、jmap查看堆使用和GC情况再用jmap -dump导出堆转储文件然后用MAT或VisualVM分析重点看大对象、类加载器、集合类持有引用。大量ArrayList无法释放往往就是静态集合在不断添加元素或者对象被长期存放到某个缓存里却永远没有清理。给出对策。如果是大量查询结果集堆积考虑分页、限流、批量释放如果是缓存设计问题考虑限制缓存大小用WeakHashMap或LinkedHashMap实现LRU或引入真正成熟的缓存组件如果是SQL查询一次性加载过多数据那问题和方向就不在JVM而在数据访问层。这套答题思路实际上比背“什么是内存泄漏”要实用得多。用友这类面向企业客户的公司服务端经常要部署在客户的环境里现场出问题就得靠这种系统化的排查能力。4.3 数组与集合混用时的边界意识这套卷子还在选择题里考了一道和Arrays与List转换相关的题这也是热词里频繁出现的内容比如asList的坑int[] arr {1, 2, 3}; List list Arrays.asList(arr);如果直接这么写list的长度是1因为Arrays.asList()接收的是可变参数int[]整体被当成一个对象放进了List。要真正把数组转换成List应该使用包装类型数组Integer[] arr {1, 2, 3}; ListInteger list Arrays.asList(arr);Arrays.asList()返回的还不是普通的ArrayList而是Arrays内部类它没有实现add和remove方法调用会抛UnsupportedOperationException。笔试里如果考到集合修改操作的合法性问题这个点是核心。进一步延伸还有经典的List.toArray()陷阱如果传入的数组长度小于集合长度toArray会无视你传入的数组重新分配一个新数组。所以规范写法是list.toArray(new String[0])让它自己决定分配大小效率上反而更好。数组和集合这块看似基础却是很多人在项目里写出隐藏Bug的地方。笔试考的都是面试官自己在开发中踩过的坑这个思路一定要意识到。5. 两道手写算法冒泡排序的优化和快速排序的工程化实现手写算法题是这套卷子的重头戏。用友考的不是竞赛级算法而是排序这种基础中的基础但要求你写出来的代码干净、正确、考虑边界。两道题分别是冒泡排序和快速排序看起来毫无难度但真正能拿满分的没几个。5.1 冒泡排序从标准写法到flag优化第一道是实现冒泡排序并说明如何优化。如果只是写两重for循环能拿基础分但想满分必须写出优化版本。最简单的优化是引入swapped标志位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 tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }优化点在于如果某一轮遍历中没有发生任何交换说明数组已经有序直接结束排序。这在处理“几乎有序”的数据时效果很明显最好情况下时间复杂度从O(n²)降到O(n)。更进阶的优化是记录“最后一次交换的位置”下一轮只需要遍历到这个位置即可因为该位置之后的所有元素已经有序。写出这两层优化这道题基本就是满分。5.2 快速排序partition的正确姿势与退化问题第二道是实现快速排序并分析最坏时间复杂度。快速排序的核心是partition也就是把数组分成“小于基准值”和“大于等于基准值”两部分。我记得当年卷子上给的评估标准里特别关注两点是否原地排序、是否处理了递归边界。推荐用双指针挖坑法实现public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } arr[i] arr[j]; while (i j arr[i] pivot) { i; } arr[j] arr[i]; } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }这里的细节很多外层循环条件是i j不是i j否则可能越界或死循环循环内部两个while都必须带上i j防止指针交错基准值选择固定取最左边在完全有序的极端情况下会退化成O(n²)所以很多工程实现会采用三数取中法或者随机选基准值空间复杂度递归调用栈深度平均为O(logn)最坏退化为O(n)。能在笔试白纸上写出递归边界if (left right) return;的人其实已经赢了一半。很多人都知道快排的平均复杂度是O(nlogn)但一写就忘递归边界这是很可惜的丢分点。5.3 笔试手写代码的评分潜规则结合我自己当年参加笔试和后来帮部门筛简历的经验手写题评分除了对错还有一套潜规则边界处理优先于功能完成。先判断null和空数组再写主逻辑这代表你有防御性编程意识。变量命名要有意义。i、j、k用可以但arr、pivot、left、right这种可读性极强的命名更受认可。写之前先梳理思路。笔试纸上不用写注释但建议在代码块前用一两句话写清思路让面试官知道你每一步在干什么。别删除式重写。如果写错了划线划掉比涂黑好看得多也方便面试官在后续面试中了解你的思考方向。如果今天让我给备考者一个建议那就是排序算法不要只看不写一定要限时在白纸或者纯文本编辑器里各写两遍以上。第一遍通常会丢三落四第二遍才会形成肌肉记忆。6. Java 8特性与枚举Lambda、Stream和枚举的实战考察从2018年往回看Java 8已经发布四年了用友这套题里也考了几个Java 8新特性的点主要集中在Lambda表达式和枚举的高级用法上。放到今天这些已经是面试必考但在当年的笔试题里算是比较用心了。6.1 Lambda表达式从匿名内部类到函数式写法卷子里有一道题让把匿名内部类的写法改写成Lambda表达式题目大致是Runnable task new Runnable() { Override public void run() { System.out.println(run); } };改写结果是Runnable task () - System.out.println(run);这道题简单但后面跟着一个衍生问题Lambda表达式对应Java里的什么类型答案是函数式接口也就是有且只有一个抽象方法的接口。常见的如Runnable、Comparator、Consumer、Function等。Lambda是这些接口实例的一种简洁表示方式本质上还是对象不是普通函数。类似的Stream API也是这套卷子里考察热词中反复出现的点。比如对一个ListInteger过滤出偶数并求和ListInteger list Arrays.asList(1, 2, 3, 4, 5); int sum list.stream() .filter(n - n % 2 0) .mapToInt(Integer::intValue) .sum();笔试里如果考到Stream大概率会问“中间操作和终止操作的区别”重点回答“中间操作是惰性的不会立即执行只有遇到终止操作才会触发流水线执行”。这也是和日常开发最相关的知识点。6.2 枚举类型的高级用法状态机与单例“java枚举类型的使用”在热词里出现了说明这也是很多人搜索的痛点。用友这套题里有一道简答题Java中的枚举可以定义字段和方法吗它可以实现接口吗请举例说明枚举在实际项目中的应用场景。答案都是肯定的。Java的枚举本质上是java.lang.Enum的子类可以定义字段、构造方法、抽象方法也可以实现接口。举个例子ERP系统中常见的订单状态用枚举管理就比用int常量清晰得多public enum OrderStatus { CREATED(已创建, 1), PAID(已支付, 2), SHIPPED(已发货, 3), COMPLETED(已完成, 4); private final String desc; private final int code; OrderStatus(String desc, int code) { this.desc desc; this.code code; } public String getDesc() { return desc; } public int getCode() { return code; } }这样的设计把状态和描述封装在一个枚举里业务代码中直接OrderStatus.PAID.getDesc()就能拿到对应的中文描述避免到处写魔法值和魔法字符串。还有一个进阶考点是枚举单例。用枚举实现单例既能天然抵御反射攻击又能避免序列化破坏单例是《Effective Java》里推荐的写法。虽然工作中不一定用得上但笔试里提到这个点能体现你的阅读面。6.3 新特性相关题目的“安全答题模板”如果你对Java 8新特性掌握不深面对这类型题目时我建议遵循一个“安全答题模板”先说解决了什么问题再说核心语法长什么样最后给出一个具体场景。比如问题冗长的匿名内部类让代码可读性差语法(参数) - 表达式场景List.sort((a, b) - a.compareTo(b))。这个模板能保证你在不熟悉细节时依然逻辑完整不至于一个点都答不上来。但老实说新特性这部分还是平时多写最有效。我见过很多同学看教程都会一上笔试就卡在Lambda的变量捕获规则上比如“Lambda引用的局部变量必须实际上是final的”。这不是死记硬背的问题而是练习量不够。用友整套题复盘到这里我想再说一句实在话这套卷子之所以值得反复咀嚼不是因为它难而是因为它干净。它既没有绞尽脑汁的脑筋急转弯也没有脱离实际面试场景的偏门问题所有考点都能在企业级Java开发里找到落点。备考时与其痴迷逛网各种“大厂压轴题”不如把这套卷子里每一个考点都过一遍把基础打扎实效果往往比刷十套难题还明显。
返回列表