用友Java秋招笔试题解析:集合、JVM与多线程核心考点
2026/9/1 20:13:51 网站建设 项目流程

又到了一年秋招季,不少同学都在刷Java笔试题,我也顺手翻到一份存了很久的用友2018秋招Java笔试题(一)。题目本身已经过去几年,但考察的方向放在今天依然很有代表性——集合、JVM、多线程、面向对象,这些基础题始终是笔试筛人的主力。这篇文章就借这套题,把Java基础面试里最容易踩坑的几类考点梳理一遍,既分析题目背后的出题意图,也给出可以直接用的复习思路和答题套路。适合正在准备校招、想快速过一遍Java核心知识点的同学,也适合工作一两年的开发者查漏补缺。

1. 这套笔试题的出题思路与考察重点

1.1 为什么大厂秋招笔试普遍爱考基础

我当年参加用友秋招的时候,整套笔试大概是一个半小时,前面是选择题,后面是两三道编程题。后来跟做校招的HR朋友聊过,才知道这种安排很有讲究:笔试的定位并不是筛出最强的人,而是快速过滤掉基础不牢的人。Java的语法、集合、并发、JVM这些内容如果掌握得扎实,说明候选人至少认真学过、做过东西;相反,如果连HashMap的底层原理都说不出,后面机试和面试的沟通成本会非常高。

所以你会发现,很多笔试题目看起来“不难”,但非常细节。比如给一段代码问输出是什么,考察静态代码块和实例代码块的执行顺序;给你一个HashMap,问往里面放多少元素时会触发扩容;让你用多线程实现一个计数器,要求线程安全。这些题单看都不算超纲,但组合在一起,能很清楚地判断你到底是“背过八股文”还是“真正写过代码”。

1.2 核心考点分布与复习优先级

根据我对历年Java笔试题的观察,考点基本集中在七个大方向。我做了一张表,可以当作复习清单用:

考察方向出现频率复习优先级典型题目示例
面向对象与基础语法必须掌握重载与重写的区别、String为什么不可变
集合框架非常高必须掌握HashMap的put流程、ArrayList扩容机制
JVM内存与GC重点突破运行时数据区域划分、GC Roots有哪些
多线程与并发重点突破synchronized与Lock区别、volatile的作用
异常处理掌握Error与Exception区别、try-with-resources
IO/NIO与网络掌握BIO/NIO/AIO区别、TCP三次握手为什么是三次
设计模式与Spring了解单例模式写法、Spring Bean生命周期

如果你时间紧,我建议优先复习前四类,它们加起来占了笔试六七成的分数。编程题也往往从这些方向出,比如手写单例、手写生产者消费者、手写快排等。

1.3 笔试时间分配与答题策略

很多同学笔试翻车不是不会做,而是时间分配失败。我的经验是:先做选择题,遇到不确定的标记一下,不要死磕;再做编程题,留够至少四十分钟;最后返回去检查标记的选择题。选择题里那些纯记忆性的概念题,往往想多了反而错。

编程题一定要先理清思路再写代码。如果题目要求从控制台读入多组数据,我习惯先把数据的读取和输出框架写好,再补核心算法。还有一点很关键——笔试环境的编译器和本地IDE可能不一样,千万不要用过于依赖IDE自动补全的写法,尽量写朴素、稳定的代码。类名如果是Main就用Main,不要习惯性写成Test。

2. 面向对象与基础语法必考点

2.1 封装、继承、多态的底层逻辑

这套笔试题里有一道概念题让我印象很深:说说你对面向对象三大特性的理解,并用代码举例。这道题看似简单,但能难倒不少人,因为很多同学只会背“封装隐藏细节,继承实现复用,多态提高扩展性”,深入一问就答不出来了。

先说封装。封装不只是private修饰字段、提供getter/setter,它的核心是“把变化隔离在内部”。比如你写一个订单类,金额字段如果直接public,外部可以随便改成负数;封装起来后,可以在setter里做校验,保证金额永远大于等于0。这才是封装的意义——不是不让访问,而是让访问受控。

继承的坑最多。很多人一上来就喜欢用继承复用代码,但继承其实是面向对象里耦合度最高的一种关系,父类一改,子类全得跟着变。笔试喜欢考继承下的代码执行顺序,比如静态代码块、实例代码块、构造函数的执行顺序。记住结论:父类静态代码块 -> 子类静态代码块 -> 父类实例代码块 -> 父类构造函数 -> 子类实例代码块 -> 子类构造函数。这种题考的就是你对类加载和对象创建过程的理解。

多态就更常考了。多态存在的三个条件:继承、重写、父类引用指向子类对象。但真正要理解的是它的底层实现——当JVM执行一个虚方法时,会通过对象的实际类型在方法表中查找对应的方法地址,而不是根据引用类型决定调用哪个方法。这也是为什么“编译看左边,运行看右边”。

2.2 String、StringBuilder、StringBuffer 三者该怎么答

这三者的对比题,在Java笔试里出现频率快赶上HashMap了。它们本质区别就三点:可变性、线程安全性、性能。

String是不可变的,每次对String的修改都会创建新对象。你写String s = "a"; s = s + "b";,实际上是创建了一个新的String对象,然后把引用指向它。如果在一个循环里做字符串拼接,会产生大量中间对象,非常浪费内存。JVM也不是傻子,编译器会优化,但优化后的结果往往是变成了StringBuilder,还不如你直接写。

StringBuilder是可变的字符串序列,线程不安全但性能好;StringBuffer是StringBuilder的线程安全版本,方法加了synchronized,但代价是性能有所下降。实际开发中,单线程环境下闭眼用StringBuilder,拼接SQL、拼接日志用StringBuilder都很顺手。多线程环境下需要共享同一个可变字符串对象时,可以用StringBuffer,但说实话这种情况很少,因为多个线程同时改一个字符串的场景本身就少见。

笔试的坑点在于:有人会以为String的+拼接是线程安全或性能优秀的,其实编译器优化之后本质还是new StringBuilder(),并且每次循环都会new一个新的。所以最佳实践是:循环外或多次拼接时,显式使用StringBuilder。

2.3 重载与重写的区别,高频但容易答偏

重载和重写是纯概念题,但很多同学答不到点上。重载发生在同一个类中,方法名相同,参数列表不同(个数、类型、顺序),与返回值无关;重写发生在父子类之间,方法名和参数列表必须相同,返回值可以协变(子类返回类型是父类返回类型的子类型),访问修饰符不能比父类更严格,抛出的异常不能比父类更广。

笔试有一道经典陷阱:public int doSomething(String a)public String doSomething(String a)能不能构成重载?答案是不能,因为只看参数列表,不看返回值。同理,doSomething(int a)doSomething(Integer a)是重载,因为基本类型和包装类型不同。

实现重写时还要注意一个细节:如果父类方法用final修饰,子类不能重写;如果父类方法是private的,子类根本看不到它,不叫重写。这些细节点经常被出成判断题,需要特别留意。

3. 集合框架:几乎年年出现的HashMap

3.1 ArrayList 和 LinkedList 怎么选才不丢分

集合框架的选择题,常常从ArrayList和LinkedList切入。核心区别就两个:底层结构不同,导致操作的复杂度不同。

ArrayList底层是Object数组,所以随机访问特别快,get(i)是O(1);但插入和删除,尤其是在数组中间位置操作,需要移动元素,最坏是O(n)。LinkedList底层是双向链表,理论上的插入和删除是O(1),但注意这里有个大坑——如果是在指定下标插入,你仍然需要先遍历到那个位置,复杂度还是O(n)。只有当你持有某个节点的引用,直接在该节点前后增删时,才是O(1)。

所以实际选择不是无脑选LinkedList。日常开发里,90%的场景用ArrayList就够了,因为遍历和随机访问是常态;LinkedList的优势场景非常局限,比如实现队列,或者频繁在头部插入删除。另外,LinkedList还实现了Deque接口,可以当栈和队列用。

ArrayList的扩容机制也常考:初始容量是10,每次扩容为原来的1.5倍(新容量 = 旧容量 + 旧容量右移一位),并把原数组复制到新数组。如果你知道大概要放很多元素,可以预先指定初始容量,避免多次扩容带来的复制开销。

3.2 HashMap 的底层原理与 put 流程

HashMap是笔试重灾区,几乎每次都有题目涉及。会画底层结构、能描述put流程,这是基本功。

JDK1.8之后,HashMap底层是“数组 + 链表 + 红黑树”。数组的每个位置是一个桶,每个桶存放一个链表或红黑树。put一个键值对时,流程是:

  1. 对key的hashCode做扰动计算:(h = key.hashCode()) ^ (h >>> 16),让高位也参与低位的运算,降低哈希冲突概率。
  2. 通过(n - 1) & hash计算桶下标,n是数组长度,所以数组长度必须是2的幂。
  3. 如果桶为空,直接新建节点放入。
  4. 如果桶非空,说明发生哈希冲突,遍历链表比较key,如果key相同则覆盖value。
  5. 如果链表长度超过8,并且数组容量大于等于64,链表转为红黑树;如果数组容量小于64,先扩容,而不是转树。
  6. 插入完成后,如果size超过阈值(容量 * 0.75),触发扩容。

这里常考的知识点有:为什么容量是2的幂?因为用位运算(n - 1) & hash代替取模更快,并且能保证均匀分布。为什么加载因子是0.75?这是空间和时间的一个折衷,过大容易冲突过多,过小则浪费空间。为什么链表转红黑树的阈值是8?根据泊松分布,当加载因子0.75时,链表长度达到8的概率非常低,所以阈值设为8是比较稳妥的。

HashMap线程不安全,多线程并发put时,JDK1.7还可能因为头插法导致死循环,JDK1.8改为尾部插入后解决了死循环问题,但数据覆盖问题依然存在。所以并发场景要使用ConcurrentHashMap。

3.3 ConcurrentHashMap 的线程安全机制

笔试有时候会紧接着问ConcurrentHashMap。JDK1.8之后的实现其实比1.7简单了不少,1.7用Segment分段锁,1.8直接用CAS + synchronized锁住桶的头节点。

put流程大致是:先计算hash,定位到桶;如果桶为空,用CAS直接放入节点;如果桶非空且不是扩容状态,synchronized锁住头节点,然后插入链表或红黑树。这样锁的粒度比整个表小得多,并发度更高。

还要知道ConcurrentHashMap不允许key或value为null,而HashMap允许。原因是ConcurrentHashMap的作者Doug Lea在Java官方并发邮件列表里回答过:如果key或value为null,在并发环境下会出现歧义——无法区分是“没有这个key”还是“这个key对应的value就是null”。虽然这个解释一直没有官方的源代码注释,但作为面试题答案记下来就够了。

4. JVM与内存管理高频题

4.1 运行时数据区域划分,一张表搞定

JVM内存这块,笔试特别爱考区域划分和各自的异常类型。JDK8之后,方法区被元空间取代,永久代没了。我把主要区域整理如下:

区域线程共享存放内容可能抛出的异常
对象实例、数组OutOfMemoryError: Java heap space
虚拟机栈局部变量表、操作数栈、方法返回地址StackOverflowError / OutOfMemoryError
本地方法栈native方法调用信息StackOverflowError
程序计数器当前线程执行的字节码行号
方法区/元空间类信息、常量、静态变量OutOfMemoryError: Metaspace

经常考的题目包括:对象实例分配在哪里(堆)、类变量分配在哪里(JDK8后静态变量存在堆中,类元信息在元空间)、栈和堆的区别。还有一道经典题:递归调用过深会抛出什么异常?答案通常是StackOverflowError,因为每次方法调用都会在虚拟机栈中压入一个栈帧,栈深度超过默认值时就会报这个错误。

4.2 判断对象可回收:可达性分析与 GC Roots

JVM垃圾回收的前提是判断哪些对象是“垃圾”。主流JVM用的是可达性分析算法,不是引用计数。为什么不用引用计数?因为循环引用无法解决。比如A引用B、B引用A,两者都没有外部引用,但引用计数不为0,永远不会被回收。

可达性分析的核心是从GC Roots出发,向下遍历引用链,凡是不可达的对象都被标记为垃圾。GC Roots包括:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。

笔试经常会问finalize()方法是否可靠。答案是不要依赖它,它的执行时机不确定,而且在JDK9后已经被标记为废弃。正确的做法是什么都不做,让对象自然被回收;如果需要释放外部资源,用try-with-resources。

4.3 类加载过程与双亲委派,一套连招

类加载过程也是高频题,顺序是:加载 -> 验证 -> 准备 -> 解析 -> 初始化。加载是找到class文件字节流并在内存中生成Class对象;验证是检查字节流是否符合JVM规范;准备是给静态变量分配内存并设置初始值(注意是初始值,不是代码里的赋值);解析是把常量池中的符号引用替换为直接引用;初始化才是真正执行静态变量赋值和静态代码块。

双亲委派模型是另一个常考点。除了启动类加载器(Bootstrap),其他加载器都有一个父加载器。当一个类需要加载时,先交给父加载器尝试加载,父加载器加载不了再由子加载器自己加载。这样做的核心目的是保证类加载的安全性和唯一性——比如java.lang.String,无论如何都由启动类加载器加载,防止核心API被篡改。笔试如果问“能不能自己写一个java.lang.String”,答案是不能,因为双亲委派会先用父加载器加载真正的String类。

5. 多线程与并发:考察工程能力的分水岭

5.1 线程创建的几种方式,别只答四种

笔试常见题:创建线程有几种方式?很多人答:继承Thread、实现Runnable、实现Callable、用线程池。这个答案能拿一半分,因为追问一句“这几种方式本质上是什么”就露馅了。

本质上,所有线程创建的最终入口都只有一个:new Thread(),然后调用start()。继承Thread是直接创建Thread的子类;实现Runnable是给Thread传入一个Runnable目标方法;实现Callable比Runnable多一个返回值,配合FutureTask使用;线程池内部也是创建Thread对象来执行任务。

所以更准确的说法是:“只有一种创建线程的方式,就是new Thread();不同的只是任务的定义方式。”这句话答出来,面试官会高看你一眼。

5.2 synchronized 与 Lock,选谁都有理由

synchronized是JVM内置的关键字,Lock是JDK提供的接口。笔试从以下几个维度对比:锁的获取和释放、是否可中断、是否公平锁、绑定多个条件、性能。

早期synchronized性能差,但JDK6之后引入了偏向锁、轻量级锁、重量级锁的升级过程,经过锁消除、锁粗化等优化后,性能已经和Lock差距不大。所以现在面试别再回答“Lock比synchronized性能好”,而是说“在低竞争场景下两者相近,synchronized更简洁;在高竞争复杂场景下,Lock更灵活”。

Lock的优势在于:可以用lock.lockInterruptibly()响应中断,可以用tryLock()非阻塞尝试获取锁,可以指定公平锁,可以通过newCondition()创建多个等待队列。如果你需要以上能力,用Lock;否则synchronized足够。

5.3 volatile的两大语义:可见性和禁止重排序

volatile是并发面试必考。它保证两条:一是线程对变量的修改对其他线程立即可见,二是禁止指令重排序。但很多同学着急背,没理解什么叫“可见性”。

可见性不是说volatile变量不存在缓存,而是说每次读取volatile变量时,JMM都会要求线程从主内存重新读取;每次写入时,也会立即刷新到主内存。这本质上是通过内存屏障实现的。

但volatile不保证原子性。经典例子是volatile int count,多个线程同时执行count++,结果仍然可能小于预期。因为count++分三步:读count、加1、写回。volatile保证了读和写各自是可见的,但三步之间存在时间窗,其他线程可能已经修改了count。

还有一个常考点:volatile可以禁止重排序,所以常用来实现单例的双重检查锁,单例的instance需要用volatile修饰,防止指令重排序导致拿到未初始化完成的对象。这个例子一定要会背,它是volatile的经典应用场景。

6. 笔试实操中的常见问题与排查技巧

6.1 编程题为什么总是编译不过?环境与细节

笔试编程题很尴尬的一点是,本地IDE跑得好好的,一提交就编译失败。大多数情况不是代码逻辑问题,而是环境差异。我总结几个常见雷区:

第一,类名和文件名不匹配。很多在线笔试题要求你提交整个类,默认类名是Main,你如果写了public class Test,直接编译报错。第二,不要写package语句。提交到笔试平台时,多余的package声明会导致编译失败。第三,注意输入输出。如果是多组数据的输入,要用循环读取;输出格式严格区分大小写和空格,多一个空格可能判错。第四,不要使用System.exit(0),它会终止整个JVM,影响评测环境其他用例的执行。

还有一个小技巧:有些在线环境的JDK版本比较老,不支持太新的语法。比如你用了var(JDK10+)或List.of(JDK9+),可能直接编译失败。写代码时尽量保持朴实,用最基础的循环和数组。

6.2 易混淆概念速查表,考前再看一眼

我把笔试里最容易混淆的一组概念整理出来,考前过一遍很有用:

对比项结论
==vsequals==比较引用地址,equals默认也是比较地址,重写后比较内容
Stringvsnew String("a")前者可能来自常量池,后者一定在堆中创建新对象
ErrorvsExceptionError是JVM层面的严重问题,Exception是程序可处理的问题,都是Throwable子类
sleep()vswait()sleep不释放锁,wait释放锁;sleep是Thread静态方法,wait是Object方法
finalvsfinallyvsfinalize()final修饰变量/方法/类,finally是异常处理块,finalize是垃圾回收回调,已废弃
ArraylistvsVectorVector线程安全但性能差,ArrayList线程不安全,现在一般用CopyOnWriteArrayList
StackvsArrayDequeJava官方推荐用ArrayDeque实现栈,性能更好,Stack类是遗留类

6.3 面试官追问的隐藏考点

笔试只是第一关,面试时考官经常拿笔试题当引子,不断追问。比如你写了HashMap的put流程,他会问:为什么链表长度为8时转红黑树?为什么是树化阈值不是数组大小?ConcurrentHashMap读的时候需要加锁吗?这些问题看似延伸,其实都是笔试考点的深化。

我的建议是,复习时把每个高频知识点当成一棵树,比如HashMap这一节点,要顺带复习HashSet的实现方式(底层就是HashMap)、HashTable与ConcurrentHashMap的区别、红黑树的特点和复杂度、以及为什么不用二叉搜索树。这样笔试和面试就形成一套完整的知识网络。

另外,多看看JDK源码的经典实现,比如Integer的缓存范围(-128到127)、String的intern方法、ThreadLocal的内存泄漏问题。这些虽然不一定在笔试题里直接出现,但经常作为加分项出现在面试追问中。

根据我这些年改简历、看笔试、参加技术面试的经验,Java笔试真正考的不是你记了多少题,而是你有没有形成体系化的知识结构。用友这套2018年的题目,放在今天依然值得反复琢磨,因为出题人想看到的是一个基础扎实、思路清晰、能从底层原理看问题的候选人。复习的时候别只背答案,多问自己“为什么”,把这几个核心模块串起来,笔试自然就稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询