☰
Java基础笔试题高分攻略:从HashMap到JVM内存的采分点解析
2026/10/15 10:14:51 网站建设 项目流程

简介:这是一份面向 Java 初学者与求职者的基础知识笔试题集,覆盖基本概念、数据类型、运算符、控制结构、函数、数组、集合框架、泛型与封装等核心考点,适合备考笔试、课程复习或自测查漏。压缩包内有 1 个 docx 文档,整体仅 25KB,文件体量轻,内容以选择题、读程序写结果、简答题和编程题为主,便于按题号翻阅或打印练习。已有 1304 人学习下载。通过配套参考答案与解析示例,读者可快速核对数据结构与集合 API 的使用细节、常见语法陷阱,并借助泛型 List、Calendar 日期计算、自定义 Stack 等编程题动手巩固 Java 基础编程能力。对需要集中梳理 Java 入门知识点的读者而言,是一份实用的小型复习资料。

1. Java基础知识笔试题:为什么背了答案的候选人反而挂得最快

某公司一次校招笔试里,A同学提前背熟了五十道常考题,结果挂在了一道最基础的题上:「请说明为什么重写 equals 必须同时重写 hashCode」。他的答案方向是对的,却把两个方法的契约关系说反了——判卷人一眼就看穿这是死记硬背。这类场景我见过太多次。Java 基础知识笔试题看起来考的是记忆,实际上考的是理解深度:答案人人都能背,区分度全在「为什么」和「边界在哪」。这份笔记要解决的正是这个问题:笔试到底在考哪些知识点、每一类题怎么答才能拿满采分点、哪些地方最容易翻车,以及怎么把背过的答案讲出真会的质感。适合正在准备校招或社招笔试的候选人,也适合需要快速自测 Java 基础是否扎实的开发者。目标只有一个:让你手里的题库从「背过」变成「扛得住追问」。

2. 考点分布与复习优先级:先看清题目在考哪三层能力

2.1 典型笔试题的知识域占比:先算清楚时间投在哪

Java 基础笔试题的出题范围其实非常固定。我统计过近几年多个来源的题目构成,大致可以分成六个知识域,占比从高到低排列如下。

知识域典型题型预估占比
Java SE 语法基础String 与 StringBuilder、包装类、异常机制、== 与 equals30%
集合框架HashMap、ArrayList、HashSet、迭代器与 fail-fast20%
JVM 内存与 GC内存区域划分、对象存活判定、GC Roots、类加载15%
并发编程volatile、synchronized、线程状态、锁升级、线程池参数15%
类加载与反射双亲委派、ClassLoader、反射调用性能10%
输入输出与网络IO 模型、序列化、TCP 与 HTTP 基础10%

注意这个比例背后有一个规律:占比最高的前四项恰好是日常编码中最容易踩坑的知识域。也就是说,出题人并不是在刁难你,而是在模拟「一个 Java 开发者写代码时最常犯错的那些点」。复习优先级应该直接照这个表来:先集合,再 JVM,再并发,IO 和网络放到最后。很多候选人把大量时间花在背 TCP 三次握手和 HTTP 状态码上,结果 HashMap 的扩容机制答得支离破碎,这是典型的投入产出倒挂。

2.2 笔试判卷的「三层能力」模型:为什么答案正确还会被扣分

我参与过几次笔试阅卷,发现判卷人看答案时心里有一个三层标尺。第一层是记忆层:结论是否正确,比如 HashMap 底层是不是「数组 + 链表 + 红黑树」。第二层是理解层:能否解释结论的来龙去脉,比如为什么冲突链表长度超过 8 才转红黑树。第三层是应用层:能否指出边界条件和典型误用,比如 HashMap 为什么线程不安全、在并发场景该换什么。

大多数候选人死在第一层到第二层之间。一个典型例子是「String 和 StringBuilder 的区别」,初级答案是「String 不可变,StringBuilder 可变」——这句话只能拿到一半分。高分会继续补充:String 不可变带来的字符串常量池复用、拼接触发频繁创建对象、StringBuilder 在单线程循环拼接中的性能优势,以及 StringBuffer 加了同步锁反而在单线程下更慢的对比。每一层补的都是采分点,而这些采分点恰恰是工作中真实用得到的东西。所以背题的正确姿势不是背答案,而是背答案的推导链。

2.3 一道完整题目对比:低分答案与高分答案差在哪

以「谈谈 JVM 内存区域」这道真题为例,低分答案是这么写的:堆存对象,栈存局部变量,方法区存类信息,程序计数器存指令地址。不能说错,但判卷人只会给基础分,因为全部是标签式结论,没有任何展开。高分答案的结构是:先划分五个区域,再补两类属性——哪些是线程私有、哪些是线程共享,然后简要说对象分配在堆上、栈上只放引用,最后点一句「JDK 8 之后方法区被元空间取代,用的是本地内存」。这四句话顺序一摆,记忆层、理解层、应用层全都有覆盖。

复习的时候可以用一个笨办法:每背一道题,就在题目旁边写三行批注。第一行写结论,第二行写一个关键机制的展开,第三行写一个典型误用或边界条件。写不出来就说明这题还没背透。这个习惯比多刷一百道题更有用,因为笔试现场你写下的每一个字都是采分点,而批注能逼你把每个结论的支撑逻辑提前想清楚。

3. JVM 内存与 HashMap 高频题:答题模板与判卷采分点

3.1 JVM 运行时内存区域:一类必考题的「五区域四属性」答法

JVM 内存区域划分是 Java 笔试的钉子户,几乎每套题都有它的身影。这道题想拿满分,需要把答案组织成一个稳定的结构。我常用的模板是「五区域四属性」:先说出五个区域的名字,再补充四个关键属性——是否线程私有、是否可能抛出 OOM、存储内容的典型类型、JDK 版本带来的变化。以方法区为例,它是线程共享的,存储类元信息、常量、静态变量;JDK 8 之前叫永久代,之后被元空间取代,使用的是本地内存而非 JVM 堆内存。

线程私有区域里最容易出追问的是虚拟机栈。面试官大概率会追加一句「栈帧里有什么」,这时候你要能答出局部变量表、操作数栈、动态链接、方法出口四个组成。另一个高发追问点是「对象创建后一定在堆上吗」,答案是「不一定」,逃逸分析如果证明对象不会逃逸出方法,JIT 可能把它分配到栈上并自动消除同步。这层答出来,你就在同类候选人中拉开了距离。

// 一段用于验证内存区域的基础代码,笔试时用来辅助记忆 public class MemoryAreaDemo { // 静态变量存储在方法区(元空间),属于线程共享 private static int staticVar = 1; // 实例变量随对象存储在堆中 private int instanceVar = 2; public void demo() { // 局部变量存储在虚拟机栈的局部变量表中 int localVar = 3; // 新建对象分配在堆上,引用存储在栈上 Object obj = new Object(); System.out.println(staticVar + instanceVar + localVar); System.out.println(obj.getClass().getName()); } }

代码里的注释直接把三层对应关系标出来了:静态变量、实例变量、局部变量分别落在哪个区域,这就是这道题最常考的展开点。笔试手写答案时,建议把「栈管运行、堆管存储」这句短话写进去,它是判卷人快速定位采分点的锚点。如果题目还要求画图,优先画线程共享与私有的分界线,这条线比区域内部的细节更值钱。

3.2 HashMap 底层实现:从扰动函数讲到阈值 8 的来源

HashMap 是集合框架里被问得最狠的题,而且追问深度没有上限。最基础的版本必须能说出:底层是数组加链表,JDK 8 之后链表长度超过 8 且数组长度达到 64 时转成红黑树。紧接着要能讲清三个机制——hash 扰动函数、扩容流程、转树阈值。扰动函数指的是(h = key.hashCode()) ^ (h >>> 16),它把高 16 位和低 16 位做异或,目的是让哈希值的高位也参与数组下标计算,降低冲突概率。

扩容流程是另一个高频采分点。默认加载因子 0.75,当元素个数超过容量 * 0.75时,容量翻倍。这里有两个细节容易被忽略:一个是扩容后旧数组元素要重新计算下标,不是简单复制;另一个是 JDK 8 之后链表迁移利用了「高位为 0 或 1」来拆分链表,而不是逐节点 rehash,这一步优化把扩容耗时降了一个量级。至于为什么链表长度到 8 才转红黑树,标准答案是泊松分布下链表长度达到 8 的概率极低,不到千万分之一,所以 8 是一个时间和空间的折中。

// HashMap 的 put 流程,简化为伪代码方便笔试时复述 public V put(K key, V value) { // 步骤 1:对 key 做扰动计算,让高位参与寻址 int hash = (key == null) ? 0 : (key.hashCode() ^ (key.hashCode() >>> 16)); // 步骤 2:根据 hash 与数组长度减一进行与运算,得到桶下标 int index = hash & (table.length - 1); // 步骤 3:如果桶为空直接放入,如果桶非空则遍历链表或红黑树 // 步骤 4:找到相同 key 则覆盖,找不到则新增节点 // 步骤 5:插入后检查 size 是否超过 threshold,超过则扩容 return oldValue; }

这段伪代码把 put 的关键步骤固定在五步内,笔试写答案时按这个顺序展开即可。背后的三个参数要特别记牢:初始容量 16、加载因子 0.75、转树阈值 8。追问「为什么容量都是 2 的 n 次幂」时,回答要点是hash & (length - 1)这个与运算只有在 length 是 2 的 n 次幂时才能等价于取模,而且位运算比取模快得多。这个问题几乎必追,提前想好答案能省下大量现场组织时间。

3.3 重写 equals 为什么必须重写 hashCode:用一个反例讲清契约

这道题是基础中的基础,却是每年笔试的重灾区。核心契约只有一句话:两个对象如果 equals 相等,那么 hashCode 必须相等;反过来不成立。笔试踩坑最典型的是把方向记反了,写成「hashCode 相等则 equals 一定相等」。正确的记忆锚点是:hashCode 是用来缩小查找范围的,equals 才是最终判断依据。HashMap 的 get 流程是先用 hashCode 定位桶,再在桶内用 equals 找目标对象,如果两个 equals 相等的对象 hashCode 不一样,它们会被放进不同的桶,导致明明相等的对象查不到。

import java.util.HashMap; import java.util.Map; import java.util.Objects; public class EqualsDemo { static class Person { String id; Person(String id) { this.id = id; } // 只重写 equals,不重写 hashCode @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person person = (Person) obj; return Objects.equals(id, person.id); } } public static void main(String[] args) { // 两个内容相同的对象,equals 返回 true Person p1 = new Person("1001"); Person p2 = new Person("1001"); System.out.println(p1.equals(p2)); // true // 但 hashCode 默认来自 Object,两个对象的值不同 Map<Person, String> map = new HashMap<>(); map.put(p1, "张三"); // 这里会得到 null,而不是预期的 "张三" System.out.println(map.get(p2)); // null } }

运行这段代码你会得到一个让新手惊讶的结果:p1.equals(p2)是 true,但map.get(p2)返回 null。原因是 p1 和 p2 的默认 hashCode 不一样,put 和 get 定位到了不同的桶。修复方式是在 Person 类里补上Objects.hash(id)作为 hashCode 实现。笔试答题时,直接把这个反例过程写出来,比干巴巴背「为了符合哈希表契约」要有说服力得多。判卷人看到你能用一个能运行的例子证明契约的必要性,基本就会给满分。

4. 并发、异常与类加载:靠「背了吗」区分候选人的高频题

4.1 volatile 的三层语义:可见性、有序性,以及不能保证的原子性

volatile 是并发题里的入门必考,但它是最容易被误解的关键字之一。笔试答案至少要覆盖三层语义:第一层是可见性,一个线程修改 volatile 变量后,其他线程能立即看到修改,因为底层会插入内存屏障并强制刷新主内存。第二层是有序性,volatile 会禁止指令重排序,典型例子是双重检查锁单例模式中声明 instance 为 volatile,防止「分配内存、初始化、赋值」三步被重排。第三层是局限性,volatile 不保证原子性,这一点最容易出错。

import java.util.concurrent.CountDownLatch; public class VolatileDemo { // 用 volatile 修饰计数器,但结果依然可能小于 10000 private static volatile int count = 0; public static void main(String[] args) throws InterruptedException { int threadNum = 10; CountDownLatch latch = new CountDownLatch(threadNum); for (int i = 0; i < threadNum; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { count++; // 非原子操作:读-改-写三步 } latch.countDown(); }).start(); } latch.await(); System.out.println("count = " + count); // 通常小于 10000 } }

这段代码演示了 volatile 的典型误用:count++不是原子操作,它包含读取、加一、写回三个步骤。两个线程可能同时读到同一个旧值,各自加一后写回,导致少加一次。笔试回答「volatile 能替代 synchronized 吗」时,标准答案是「不能,因为 volatile 只能保证可见性和有序性,不能保证原子性;替代 synchronized 的场景只能是无状态赋值」。想拿高分还可以补一句:JDK 5 之后 volatile 的语义被增强,采用了 happens-before 规则,写入 volatile 变量之前的操作对其他线程读取该变量之后的操作可见。

4.2 synchronized 与 ReentrantLock 对比:锁的表格式答法

锁机制的题在笔试里通常以「说说 synchronized 和 ReentrantLock 的区别」出现。这道题适合用对比表来组织答案,因为判卷人阅卷时看到的关键词一目了然。我一般会从六个维度展开:锁的实现、是否可中断、是否公平、是否支持条件队列、锁的释放方式、性能差异。

对比维度synchronizedReentrantLock
锁实现JVM 层内置锁,通过 monitor 对象实现JDK 层基于 AQS 实现
获取锁是否可中断不可中断,线程只能阻塞等待可中断,支持 lockInterruptibly
是否公平非公平默认非公平,可选公平模式
条件队列只能有一个,通过 wait/notify可创建多个 Condition 条件队列
释放方式自动释放,退出同步块即释放必须在 finally 中手动 unlock
JDK 6 之后性能引入锁升级机制,性能接近在锁竞争不激烈时无明显优势

回答这道题有一个加分细节:JDK 6 对 synchronized 做了锁升级优化,从偏向锁到轻量级锁再到重量级锁,所以不要再答「synchronized 性能比 ReentrantLock 差」。这层更新过的信息一出来,立刻和只会背旧结论的候选人拉开差距。另外追问「什么是锁升级」时,要点是:偏向锁解决单线程反复进入同步块的场景,轻量级锁通过 CAS 自旋解决短时竞争,自旋失败才膨胀为重量级锁,阻塞挂起线程。

4.3 异常机制与 finally 的执拗:一段代码引出最阴的坑

异常处理题里杀伤力最大的是 final、finally、finalize 三兄弟辨析,以及 finally 与 return 的执行顺序。后者几乎年年有题,且年年有人答错。下面这段代码就是笔试常客。

public class FinallyDemo { public static String test() { try { return "try"; } finally { System.out.println("finally 执行了"); } } public static int number() { int result = 1; try { return result; // 先保存返回值 1 } finally { result = 2; // 修改的是局部变量,不影响返回值 } } public static void main(String[] args) { System.out.println(test()); // 输出 "try" System.out.println(number()); // 输出 1,不是 2 } }

这里有三个采分点。第一点:finally 块一定会在方法返回前执行,即使 try 块里有 return 语句。第二点:如果 finally 块里也有 return,它会覆盖 try 里的 return,因为 finally 的 return 会被编译进方法返回路径。第三点:如果 try 块 return 的是一个基本类型或引用,finally 里修改该变量不会改变返回值,因为返回值在 return 指令执行前已经被保存到操作数栈。笔试答到这里只是标准分,想冲高分可以补一句:不要在 finally 里写 return,这是业界公认的坏味道,会导致异常被吞掉。这个补充直接体现工程经验。

4.4 类加载与双亲委派:一个不容易背错但容易讲浅的题

类加载机制在笔试里通常以两个形态出现:一是「类加载过程分哪几步」,二是「什么是双亲委派模型」。第一个问题的标准答案是加载、验证、准备、解析、初始化五步,注意准备阶段会为静态变量分配内存并设置零值,初始化阶段才执行静态代码块和静态变量赋值。第二个问题要讲清:一个类加载请求会先委托给父类加载器,逐级向上,只有父加载器无法完成时才由子加载器自己加载。

很多人背到这里就停了,其实这道题的采分点在后面:双亲委派的作用是什么,以及一个经典反例。作用有三条——避免核心类被篡改,比如 java.lang.String 只能由启动类加载器加载,防止用户自定义的 String 混入;保证核心类加载结果一致,同一个类不会因为加载器不同而出现多个版本;第三点是安全考虑,防止恶意代码冒充核心库类。经典反例是 SPI 机制,比如 JDBC 驱动,核心类库需要调用第三方实现的接口,但双亲委派被子加载器反向委托不到,所以才有线程上下文类加载器来打破双亲委派。能讲清这个反例,说明你是真懂而不是背的。

5. 基础知识笔试的避坑指南:五个经典误答与判卷采分点

5.1 把「不可变」背成绝对结论:String 的边界条件

现象:回答 String 不可变时,被追问「那 String 的 replace 方法返回的是什么」,候选人答「原对象被修改了」,直接失分。原因是把「不可变」理解成了「不能变化」,实际上不可变指的是对象本身状态不变,所有修改操作都返回新对象。解决:背这个知识点时,要连带记住一个演示代码——调用 replace 或 substring 后,原字符串引用指向的对象内容不变,但返回了一个新对象。笔试答案里点出「String 类的 value 数组被 final 修饰并且私有,外部无法修改」就已经高分,再补一句「StringBuilder 的 value 数组没有被 final 修饰,所以可修改」就更完整。

5.2 HashMap 的容量与加载因子答混:「0.75 是容量上限」这种说法直接暴露没看源码

现象:答 HashMap 扩容机制时,把加载因子 0.75 说成「数组最多装 75% 就不让装了」,或者把 threshold 说成容量上限。原因:混淆了 threshold(扩容阈值)和 capacity(容量)两个概念。threshold = capacity * loadFactor,是触发扩容的警戒线,不是存储上限,超过 threshold 后容器会扩容而不是拒绝写入。解决:用具体数字推演——初始容量 16,加载因子 0.75,threshold 是 12,当第 13 个元素插入时触发扩容到 32。笔试时写出这个推算过程,比任何文字描述都有说服力。

5.3 手写线程交替打印翻车:wait 和 notify 的时序没推演

现象:要求手写两个线程交替打印 1 到 100,候选人写出了 wait 和 notify 的代码,但没有用 while 包裹条件判断,导致线程唤醒后不重新检查条件,直接出现重复打印或死等。原因:wait 存在虚假唤醒的可能,官方文档明确要求必须在循环中调用 wait。解决:记忆模板是while (条件不满足) { wait(); }之后再执行逻辑,notify 之后立即退出同步块释放锁。笔试代码里这个 while 是显眼的采分点,写了就比其他候选人高一档。

5.4 == 与 equals 的常量池边界:把「字符串比较用 equals」当成万能结论

现象:题目让判断String a = "aa" + "b"与String b = "aab"是否相等,候选人一律答「字符串比较要用 equals,所以不相等」,结果正确答案是 a 和 b 指向同一个常量池对象。原因:忽略了编译期常量折叠——两个字符串字面量拼接在编译期就被优化成了同一个字面量。解决:记忆规则分三档:字面量赋值和编译期可确定的拼接指向常量池同一个对象;new String 创建的对象在堆上;通过变量拼接或调用方法的字符串在运行期生成,不在常量池。笔试时先看是否编译期常量,再决定用 == 还是 equals 判断。

5.5 类初始化时机答错:静态代码块不是类加载时执行

现象:问「静态代码块什么时候执行」,候选人答「类加载的时候」,被扣分。原因是类加载过程的五个阶段里,加载、验证、准备、解析在前,初始化在后,静态代码块和静态变量赋值是在初始化阶段执行的。类加载不等于类初始化,一个类被加载后可以不被初始化。解决:用一个实际场景辅助记忆——通过子类名访问父类的静态字段,只会触发父类的初始化,不会触发子类的初始化,因为初始化只发生在主动使用时。笔试答出「主动使用六种场景」里的两三种,这道题基本就拿满了。

6. 把基础题答出区分度:三层答题法与追问链演练

笔试和面试的界限越来越模糊,很多公司直接在笔试后面加一轮口头追问。所以最后这点时间,我想说一个把题库价值最大化的技巧:三层答题法。第一层,先给结论,一句话说清楚答案核心;第二层,拆机制,讲一个底层原理或源码细节;第三层,给佐证,补一个工程场景或反例。用「HashMap 线程安全吗」这道题来演练——第一层答案非常干脆:不安全。第二层展开:JDK 7 中扩容时头插法可能形成环形链表导致死循环,JDK 8 改成尾插法后死循环问题缓解,但 put 和 get 并发时仍有数据覆盖和 size 计数不准的问题。第三层补工程佐证:并发场景应该用 ConcurrentHashMap,它在 JDK 8 里抛弃了分段锁,改用 CAS 加 synchronized 锁住桶首节点。

追问链也是可以提前演练的。出题人从这道题往下追的路径几乎是固定的:HashMap 不安全,那 ConcurrentHashMap 怎么保证安全?这里的标准答法是 JDK 7 的分段锁和 JDK 8 的 CAS 加锁机制,以及扩容时的协助迁移逻辑。再往下追:ConcurrentHashMap 的 size 方法怎么保证准确?答案是通过 baseCount 加 CounterCell 数组分段计数。这条追问链你提前走一遍,真正被问到的时候就像在播放录音,而不是现场现想。

我自己带新人时最怕的也是这种「纸面熟练、一追问就露馅」的状态。有一种笨办法我用了很久:每背完一道题,合上答案,假装对面坐着一个较真的面试官,把这道题讲一遍,讲到卡壳的地方就是你没理解透的地方。笔试不是考察记忆力,是考察你在压力下还能不能把原理讲圆。基础题之所以叫基础题,正因为它是所有人都以为会、但只有少数人能讲出层次的东西。希望这篇笔记能帮你把每一道基础题都变成你的加分项,而不是失分项。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询