
又到秋招季后台好几个学弟学妹问我“用友的Java笔试题到底怎么刷网上能找到的真题太零散了。”这话我深有体会。我当年参加用友2018秋招笔试二的时候也吃过信息零散的亏。那套卷子整体风格其实很有代表性题量不大但知识点覆盖面很广很多题看着像是考基础实际每一道都在挖坑。这篇文章我就把自己做这套题的过程、踩过的坑、复盘后的方法论全部整理出来给正在准备Java校招的同学一个可以直接参考的刷题方向。这套笔试适合谁看主要面向准备Java后端开发岗校招的同学尤其是目标是用友或者其他传统软件厂商研发岗的。它考的不是刁钻的算法而是你Java基础扎不扎实、能不能看懂生产环境下的代码。如果你正在刷Java面试题又不想死记八股文那这篇复盘应该能帮你少走不少弯路。1. 一套典型Java笔试题的命题思路拆解1.1 用友校招笔试的题型结构与出题风格先说当时拿到的卷面整体印象。用友2018秋招Java笔试题二的题型大概分四块单选题、多选题、程序阅读题和简答/编程题。单多选占大头程序阅读题次之最后有一两道编程题整体时间给得不算特别宽裕。这个结构其实很能说明问题。传统软件厂商的研发岗尤其是做ERP、云产品这类To B业务的日常工作中大量时间是在读别人的代码、改历史遗留代码、排查线上问题。所以他们笔试不跟你玩“纯算法竞赛”那一套而是更看重三样东西基础语法熟不熟练、能否读得懂带业务含义的代码、遇到异常和并发问题有没有成体系的排查思路。单选和多选覆盖的知识点相当常规基本都是Java核心基础基本数据类型、运算符优先级、String与包装类、集合框架、异常机制、多线程基础、JVM内存区域、类加载顺序、SQL基础。单看这些名词很多同学会觉得“就这”。但如果真这么想就太小看这套题了。我当时最快的感受是题目本身很少直接问“什么是HashMap”而是给你一段代码问输出是什么、或者哪一行会报错。这种考法比“默写概念”狠得多它考察的是你是不是真的理解底层的执行机制而不是背住了术语定义。1.2 为什么这些考点会被选中从岗位要求反推考察意图站在出题人角度想一想为什么用友会选这些考点核心原因在于岗位日常任务。用友的产品线很长从U8、T3到U9 Cloud、NC Cloud背后都是大型Java服务端系统。这类系统的特点是什么一是生命周期长代码可能要维护十年以上二是并发和事务场景多财务数据、库存数据都不能出错三是涉及大量数据库交互SQL功底必须过关。所以笔试里反复出现的几个主题全部对应实际工作场景集合类和HashMap相关题目对应的是你日常写业务代码时最常打交道的容器如果连扩容机制和线程安全性都搞不清楚生产环境一出问题根本无从下手。多线程和锁相关题目对应的是ERP系统里批量任务、定时调度、库存扣减这类高并发场景。JVM和类加载相关题目对应的是线上OOM内存溢出、类冲突、HotSwap热部署这些实战问题。搜索引擎里能看到大量“用友NC uploadservlet命令执行”、“用友U8接口”相关的内容说明这类企业系统对运行时稳定性的要求非常高。SQL和事务相关题目对应的是财务模块、进销存模块里最核心的数据一致性需求。把这个逻辑想明白之后你复习的目标就不是“把Java题海刷完”而是“搞清楚生产环境下Java最常出问题的几个点”。这样的复习效率会高很多。2. 高频考点深度解析Java基础部分的“暗坑”2.1 面向对象重载、重写、动态绑定最容易丢分用友这套笔试卷子里面向对象部分的题量不小但考的并不直接最常见的形式是给出父子类各一个方法让你判断调用哪一个。这里面最核心的概念是重载Overload和重写Override的区别以及Java运行时动态绑定的机制。很多背过八股文的同学能脱口而出“重载是编译期多态重写是运行期多态”但一到具体题目就懵。原因很简单他们没有把“编译看左边运行看右边”这九个字内化成代码直觉。举个例子当时有一道程序阅读题大概是这样的class Parent { public void print(String s) { System.out.println(Parent String); } } class Child extends Parent { public void print(Object o) { System.out.println(Child Object); } } public class Test { public static void main(String[] args) { Parent p new Child(); p.print(hello); } }输出是什么答案是“Parent String”。原因在于Child里的print(Object o)并不是对print(String s)的重写而是重载。两个方法的参数类型不同所以和方法重写没关系。变量p的静态类型是Parent编译期就已经确定了要调用的是Parent中定义的print(String)方法。虽然运行时对象实际是Child但Child并没有重写print(String)所以只能调用父类的实现。这道题错的人非常多。很多同学一看到new Child()就想当然选择“动态绑定”忽略了动态绑定只对重写方法生效。这个坑如果不亲自踩一遍光靠背概念很难真正避开。另一个常考的点是构造方法的调用顺序。子类构造时第一行必然是super()哪怕你没写编译器也会自动加。所以创建子类对象的时候一定是父类静态代码块 - 子类静态代码块 - 父类普通代码块 - 父类构造器 - 子类普通代码块 - 子类构造器。这个顺序题用友笔试基本年年有只是换层皮。2.2 集合框架HashMap与Hashtable的底层差异集合框架几乎是所有Java笔试题的重头戏用友也不例外。当时卷子里有一道多选问的是HashMap和Hashtable的区别。选项里混了好几个容易混淆的说法比如“HashMap线程安全”、“Hashtable允许null键”、“HashMap在Java 8之后引入红黑树”等等。正确的版本是HashMap线程不安全Hashtable线程安全Hashtable的方法大多用synchronized修饰。HashMap允许key和value为nullHashtable不允许。HashMap继承自AbstractMapHashtable继承自Dictionary。Java 8之后HashMap在链表长度超过8且数组长度大于等于64时会把链表转成红黑树优化冲突严重时的查找性能。Hashtable线程安全是有代价的它直接在方法上加重量级锁并发度低所以现在生产环境基本都用ConcurrentHashMap。但笔试题不会管你生产环境用什么它就是要考你这些基础差别别答混。HashMap还有一个高频考点是扩容机制。默认初始容量16负载因子0.75。意思是当元素个数超过16 × 0.75 12时就会触发扩容容量翻倍到32。这里很多同学会记错把“达到12个元素”记成“数组长度达到12”其实是元素数量不是数组位置。Java 8之后HashMap的put过程也是一个经典的程序阅读题素材计算key的hashCode再通过(h key.hashCode()) ^ (h 16)做扰动降低哈希碰撞概率。(n - 1) hash计算出数组下标其中n是数组长度。因为n是2的幂所以位运算等价于取模但效率更高。如果该位置没有元素直接放入如果有则遍历链表/红黑树。如果key已存在覆盖旧值如果不存在尾插法插入新节点。插入后检查size是否超过threshold超过则扩容。这个流程不一定直接以代码题出现但非常可能以“某段代码反复put问最终size是多大”的形式出题。我印象中当时就有一道和put过程相关的选择题涉及key相同覆盖value的问题答案很容易错算成里面有多个元素其实是同一个key被覆盖了。2.3 异常与多线程从基础概念到并发实战考点用友这套卷子的异常题不算难但有一个点考得特别细checked exception受检异常和unchecked exception非受检异常的区分标准。规则很简单RuntimeException及其子类是非受检异常除此以外的Exception子类是受检异常。受检异常要求方法必须声明throws或者用try-catch处理否则编译不通过非受检异常不需要强制处理。当时有道题问下面哪个属于受检异常选项有NullPointerException、ArrayIndexOutOfBoundsException、IOException、ArithmeticException。正确答案是IOException。但很多人会错选NullPointerException因为实际开发中空指针出现得太频繁误以为它是受检的。这就是概念和执行经验的区别。多线程部分用友出题偏向基础但注重应用。比如考了Thread和Runnable的区别、sleep()和wait()的区别、synchronized的作用范围、volatile的可见性和有序性。其中有一道程序阅读题问的是多线程环境下某个共享变量i最终的值。这题考的就是i不是原子操作即使加了volatile也不能保证线程安全。因为volatile只保证可见性和有序性不保证原子性。要保证i线程安全得用AtomicInteger、synchronized、Lock等机制。这块考点我建议复习的时候不要只看概念最好自己动手写几个并发程序观察一下输出结果。比如写一个10个线程各执行1000次i的程序你会发现最终结果经常不是10000。亲眼看到这个结果之后你对“非原子操作”的理解会深很多。3. 实操复盘几道典型题目的完整作答过程3.1 经典题1基础语法与运算符优先级先来一道我印象很深的题。原文大概意思是以下代码输出什么public class Test { public static void main(String[] args) { int i 1; i i; int j i; int k i i * i; System.out.println(i); System.out.println(j); System.out.println(k); } }这道题考两个点i和i的区别以及运算符优先级。先看int i 1; i i;。很多人会想i不就是先取i再自增吗那i先取出1再自增为2再把1赋给i所以i还是1对但理解还不够本质。在Java里i做的事情是先把i的当前值压入操作数栈然后对局部变量表中的i加1。赋值的时候是把操作数栈里的值取出来赋给i。所以i i的结果是i先变成2然后被覆盖回1。最终i还是1。接下来int j i此时i是1先取1给j然后i自增为2。所以j 1i 2。再看int k i i * i;。这行要非常小心涉及优先级和求值顺序。优先级规则后缀自增i的优先级最高然后是一元运算符i然后是乘除法最后是加减法。所以这行代码先计算i和i。计算过程是这样的初始时i 2。开始计算i i * i。先算左边的i此时i的值是2取出放到一边。再算ii变成3得到3。再算i当前i是3先取出3然后i自增为4。乘法3 × 3 9。加法2 9 11。所以k 11i最终是4j 1。最终输出4 1 11这道题我在考场上做的时候左边那个i到底什么时候取值纠结了很久。Java的求值顺序是“先左后右”但这里要注意变量取值和自增发生的时间点。我当时在草稿纸上画了操作数栈和局部变量表的推演过程才敢下笔。建议你做这类题的时候也养成画栈的习惯不要凭感觉尤其涉及到多个自增混在一起的时候。3.2 经典题2String、包装类与equals判等用友这套题里还有一类必考题String相关。考察点在字符串常量池、和equals区别、包装类缓存机制。有一道程序阅读题大致的形态如下public class Test { public static void main(String[] args) { String s1 abc; String s2 new String(abc); String s3 a bc; String s4 new String(abc).intern(); System.out.println(s1 s2); System.out.println(s1 s3); System.out.println(s1 s4); } }输出是什么s1 s2false。s1指向常量池里的abcs2指向堆里的一个String对象地址不同。s1 s3true。s3是编译期常量拼接编译器会直接优化成abc所以s3和s1都指向常量池的同一个对象。s1 s4true。intern()方法会检查常量池里是否已有abc有则返回常量池引用。这道题的核心是理解Java的字符串常量池机制。其中a bc因为两个都是编译期常量编译器会直接合成abc但如果其中有一个是变量比如String x a; String s x bc;那就是运行时拼接会生成新的String对象s1 s就是false。包装类也有类似的考点。当时有道题考的是Integer缓存Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); System.out.println(c d);第一行输出true第二行输出false。原因在于Integer默认缓存了-128到127的值在这个范围内的自动装箱会直接返回缓存对象。所以a和b指向同一个对象而128超出了缓存范围每次装箱都是新建对象c和d地址不同。这类题是典型的“背过就会没背就懵”。但如果只记住结论不记原理还是容易翻车。底层原理其实很简单Integer.valueOf(int)方法里有一个缓存数组范围默认是-128到127可以通过JVM参数调整上限。用友这套题考到这个层面说明出题人希望候选人不仅知道结果还理解自动装箱背后的机制。3.3 经典题3多线程交替打印最后说说编程题。我记得当时有一道手写代码题要求用两个线程交替打印数字比如线程1打印奇数、线程2打印偶数输出1到100。这道题思路不难核心是线程间协作。我当时的解法是用Object的wait()和notify()public class PrintTask { private static final Object lock new Object(); private static int num 1; public static void main(String[] args) { Thread t1 new Thread(() - { while (num 100) { synchronized (lock) { if (num % 2 1) { System.out.println(Thread.currentThread().getName() : num); num; lock.notify(); } else { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } }, 奇数线程); Thread t2 new Thread(() - { while (num 100) { synchronized (lock) { if (num % 2 0) { System.out.println(Thread.currentThread().getName() : num); num; lock.notify(); } else { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } }, 偶数线程); t1.start(); t2.start(); } }思路是两个线程竞争同一把锁拿到锁后判断当前数是否是自己该打印的。如果是打印后num并notify()唤醒对方线程如果不是就wait()释放锁并进入等待状态。这样两个线程交替执行。这题有两个容易扣分的地方。第一wait()方法必须放在synchronized代码块里面否则会抛IllegalMonitorStateException。很多同学笔试写代码时容易忽略这一点。第二循环判断条件是while (num 100)而不是if (num 100)。虽然在这个场景下用if也能跑但wait()存在“虚假唤醒”的可能用while才是更规范、更健壮的写法。面试官通常看到while会加分。当然这题还有别的解法比如用ReentrantLock加Condition或者用Semaphore。我也见过有同学用AtomicInteger自旋实现的也能跑但效率上不如阻塞唤醒的方式优雅。如果笔试时间充裕可以适当补充一两种解法向面试官展示你对并发工具类的掌握广度。3.4 经典题4SQL查询与简单算法手写除了Java基础这套题还有一类必考内容SQL。用友是做企业软件起家的SQL功底对研发岗来说是硬指标。当时有一道SQL题大概是给定员工表和部门表查询每个部门薪资最高的员工。这类题几乎是所有数据库笔试题的经典款。用窗口函数的写法是SELECT d.dept_name, e.emp_name, e.salary FROM ( SELECT emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) e JOIN department d ON e.dept_id d.dept_id WHERE e.rn 1;如果没有窗口函数用子查询也能写逻辑是先找出每个部门的最高薪资再关联员工表。思路不复杂但要注意如果最高薪资有并列需求是“查一个人”还是“查所有并列的人”写法不一样。算法那块用友笔试的难度放在互联网大厂面前算温柔的了。我记得当时考了一道冒泡排序的变体要求统计排序过程中交换的次数。这题本质上就是在考你冒泡排序的实现有没有真正吃透。如果只会对着模板默写一旦题目稍微变一变就容易卡壳。我建议复习排序算法时不要只背代码要把每一轮排序之后数组的状态画出来知道为什么这样比较、为什么这样交换、什么时候可以提前终止。用友这类企业笔试题不追求你掌握了多少冷门算法但要求你会的东西是真会。4. 笔试现场的常见问题与排查技巧实录4.1 时间分配先拿稳哪些分笔试现场最容易出的问题就是时间不够用。用友这套题单选多选大概三十多道程序阅读五六道简答加编程两三道。如果你在某一道偏题上纠结太久后面编程题很可能会写不完。我的建议是先做有把握的题再做需要思考的题最后啃实在不会的题。单选题和多选题是第一优先级它们分值高、耗时短程序阅读题第二优先级这类题通常需要画栈推演比较费时间编程题放在最后至少留出30到40分钟。编程题如果实在写不完也要把思路写上去比如伪代码、步骤注释、核心方法名让阅卷人看到你有解决问题的思路。笔试阅卷不是只看最终结果解题过程的逻辑是否清晰也很重要。还有一个细节如果笔试是线上进行的注意保存代码。我当年就见过有同学写了一大半结果页面刷新代码全没了。如果是本地IDE写记得随手CtrlS如果是在线编辑器尽量每隔几分钟把代码复制到本地草稿箱。4.2 陷阱题识别清单刷完这套题我总结出几个反复出现的“陷阱信号”你可以做一份自查清单遇到这些信号时多留个心眼看到i和i同时在一条表达式里先不要急着算把操作数栈画出来再说。看到new String(xxx)和字符串常量比较先想一想常量池和堆的区别。看到Integer比较大小下意识想一下范围是否在-128到127之间。看到多线程操作共享int变量第一反应不是“该用volatile”而是“i不是原子的”。看到父子类方法调用第一反应不是“动态绑定”而是先判断这是重写还是重载。看到try-catch-finally要特别小心finally里如果有return它会吞掉try或catch里的返回值。我当年就栽在最后一条上。finally里加return是极其糟糕的写法但笔试偏偏喜欢考。当时的题大概是说try块里return 1finally块里return 2问最终返回什么。答案是2因为finally的return会覆盖之前的返回值。这种题考的不是你写代码多优雅就是考你对JVM执行流程的掌握程度。4.3 踩坑实录与复习建议最后分享一点真实的踩坑经历。我第一次做这套题之前也刷过不少“Java面试八股文”感觉自我良好。结果一做题发现很多题并不直接问你“HashMap的底层数据结构是什么”而是直接扔一段代码让你判断输出。这种方式让我意识到概念记住不等于真的懂你真的得在代码层面理解它。后来我调整了复习方式不再背题而是用“代码验证法”。每学一个知识点就写一个小Demo去验证。比如学了字符串常量池就写代码用比较各种String对象再通过javap -c看字节码指令学了JVM内存区域就写一个不断创建对象的程序然后通过JVM参数调小堆内存亲手触发一次OOM。这个过程比背十遍八股文都有用。对于准备用友这类传统软件厂商的校招同学我还有一个额外建议多去了解产品和技术栈相关的常见问题。用友的NC、U8、U9 Cloud这些产品网上有大量实操手册和问题记录比如“U8未检测到合法许可”、“T3启动时报错2147024770”、“NC的uploadservlet相关安全隐患”等。这些内容不一定直接考但如果能在面试环节聊出几句会让面试官觉得你对公司产品真的有了解不是临时抱佛脚。用友2018秋招Java笔试题二整体难度不算高但它很有代表性考察的都是Java程序员日常开发中真正用得上的底子。如果你能把这份卷子里的每一道题都弄明白不光是背答案而是理解背后的原理再去做其他公司的Java笔试题也不会太慌。我一直觉得笔试的最终目的不是拿满分而是通过一次测试逼自己把基础补扎实。基础这关过了后面谈项目、谈设计才站得住脚。