明天你会感谢今天奋力拼搏的你。
ヾ(o◕∀◕)ノヾ
最近在找工作,虽然工作这么多年了,但依然免不了俗,要被面试官的绝技-Java八股文,教育一通。怎么办?只怪自己学艺不精,只好悬梁刺股,下次把场子找回来。。
下文题目的先后顺序不代表难易程度。
八股题1:MySQL的事务隔离级别?
扩展1:幻读与不可重复读的区别:
扩展2:大多数数据库支持上面四种标准隔离级别,但实现机制和默认级别不同。Spring的隔离级别除了上面四种,还有一种默认级别:使用底层数据库的默认隔离级别。Oracle和SQL Server的默认隔离级别是:读已提交。
八股题2:MySQL索引的数据结构?
八股题3:分析以下代码的输出,并说明 JVM 内存模型如何影响可见性
public class VisibilityTest {
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (running) { /* 空转 */ }
System.out.println("Thread exit");
}).start();
Thread.sleep(1000);
running = false; // 主线程修改标志
}
}
答案:running 未加 volatile,线程可能无法感知修改,可能永远不输出 "Thread exit"(线程缓存 running 为 true),解决方案:加 volatile 或 synchronized
解释:
即使不考虑 JVM 优化,硬件层面也可能导致问题:
解决方案对比:
| 方案 | 原理 | 性能损耗 |
| volatile running | 强制工作内存与主内存同步(内存屏障) | ~10ns |
| synchronized 块 | 释放锁时强制刷新主内存(隐式屏障) | ~100ns |
| AtomicBoolean | 利用 CAS 的 volatile 语义 | 同 volatile |
| 加入 println () | 输出操作自带隐式的内存屏障(IO 同步) | 高延迟 |
八股题4:自动装箱陷阱,如下代码结果是什么
Integer i1 = 127; Integer i2 = 127; // == ?
Integer i3 = 128; Integer i4 = 128; // == ?
int i5 = 128; Integer i6 = 128; // == ?
答案:true false true
当创建值在 -128 到 127 范围内的 Integer 对象时,Java 会使用 整数缓存机制(通过 Integer.valueOf(int) 实现)来复用对象,而不是每次都创建新的对象。因此第一个是true,第二个是false.
在比较 i5 == i6 时,Java 会自动对 i6 进行 拆箱,拆箱后i5 和 i6 都是基本数据类型 int,比较的是它们的值。所以第三个是true。
八股题5:类加载的执行顺序,如下代码执行结果
class Parent {
static { System.out.println("Parent static block"); }
{ System.out.println("Parent instance block"); }
public Parent() { System.out.println("Parent constructor"); }
}
class Child extends Parent {
static { System.out.println("Child static block"); }
{ System.out.println("Child instance block"); }
public Child() { System.out.println("Child constructor"); }
}
public class ClassLoadTest {
public static void main(String[] args) {
new Child(); // 输出顺序?
}
}
预期答案:Parent static -> Child static -> Parent instance -> Parent constructor -> Child instance -> Child constructor
解释:类加载顺序:父静态 -> 子静态 -> 父实例块 -> 父构造 -> 子实例块 -> 子构造
八股题6:Spring中对循环依赖的处理。
Spring 通过三级缓存和提前暴露 Bean 的早期引用来解决单例 Bean的循环依赖问题。
三级缓存结构:
下面以 A-B-A 的循环依赖为例详细说明流程:
Spring 的三级缓存虽然复杂,但这是为了支持强大的功能所付出的必要代价。通过三级缓存,Spring 能够在不牺牲灵活性的前提下,解决各种复杂的循环依赖问题,包括单例 Bean、原型 Bean(需配合其他机制)等。
八股题7:dubbo中的SPI机制对循环依赖的处理。
Dubbo SPI 通过 提前缓存实例 + Setter 注入 的组合策略,利用单例模式和递归注入机制,解决了大多数循环依赖场景。其核心思想是 允许半成品对象被引用,通过依赖注入的逐步完成最终初始化。开发者在实践中应避免构造器循环依赖,并关注可能的状态一致性风险。
以 A 依赖 B,B 依赖 A 为例:
八股题8:介绍下mybatis的一级缓存和二级缓存。
八股题9:下面代码是跑出异常还是正常返回
public static int test1() {
for (int i=0; i< 10; i++) {
try {
throw new Exception("test");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
continue;
}
}
return 0;
}
答案会正常返回0
因为:异常被抑制,在 Java 中,finally 块中的代码优先于异常传播,始终会被执行。如果 finally 块中有控制流语句(如 return 或 continue),它们会覆盖异常的传播行为。
因此,尽管 throw new RuntimeException(e) 抛出了异常,但由于 finally 块中的 continue,异常被抑制了。
八股题10:下面代码返回的值是什么
public static int test2() {
try {
return 0;
} finally {
return 1;
}
}
答案是返回1
如果 finally 块中有 return 语句,则会覆盖 try 或 catch 块中的返回值。
八股题11:异常捕获顺序陷阱,下面代码会被哪个异常捕获
public class ExceptionOrder {
public static void main(String[] args) {
try {
int[] arr = null;
System.out.println(arr[0]);
} catch (Exception e) { // 1
e.printStackTrace();
} catch (NullPointerException e) { // 2
e.printStackTrace();
}
}
}
答案:Exception
子类异常必须在父类异常之前捕获,因为NullPointerException是Exception的子类,所以catch 块 2 永远无法到达
八股题12:下面代码能否编译通过,不能的话解释原因,能的话写出输出的内容。
public class ExceptionFlow {
public static void main(String[] args) {
try {
System.out.println("Try block");
int i = 1 / 0;
System.out.println("After division"); // 1
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException");
throw new NullPointerException("Nested exception"); // 2
} finally {
System.out.println("Finally block"); // 3
}
System.out.println("After try-catch-finally"); // 4
}
}
答案:能
Try block
Caught ArithmeticException
Finally block
受检异常(Checked Exceptions)
非受检异常(Unchecked Exceptions)
八股13:如下代码执行是报错还是正常返回
import java.util.HashMap;
public class HashMapNullKey {
public static void main(String[] args) {
HashMap<Object, String> map = new HashMap<>();
map.put(null, "value1");
map.put(null, "value2");
System.out.println(map.size()); // 输出?
System.out.println(map.get(null)); // 输出?
}
}
答案:输出1和value2
HashMap 允许null键,但null键只能有一个(因为null的hashCode固定),后续插入会覆盖原值。
八股题14:泛型擦除陷阱,如下代码输出结果是什么。
import java.util.ArrayList;
import java.util.List;
public class GenericErasure {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // 输出?
}
}
答案:true
泛型信息在编译后会被擦除,运行时所有泛型 List 的 Class 对象相同。
八股题15:下面方法能否正常编译
class Parent {
public void method() throws RuntimeException { }
}
class Child extends Parent {
@Override
public void method() throws Exception { } // 能否编译?
}
答案,不能,子类重写方法时,声明的异常不能比父类更宽泛。
八股题16:下面代码执行的结果是什么?
public static void main(String[] args) {
int result = 0;
for(int i = 0; i < 100; i++) {
result = result++;
}
System.out.println(result);
}
执行结果为:0
这是一个常见的 Java 陷阱。result++ 是后置自增操作符。
类似示例:
int a = 0;
int b = a++; // b = 0, a = 1
int c = ++a; // c = 2, a = 2
八股题17:下面代码执行结果是什么?
public class OverloadDemo {
public static void method(long i) {
System.out.println("long");
}
public static void method(Integer i) {
System.out.println("Integer");
}
public static void main(String[] args) {
method(10); // 输出?
}
}
答:输出long
Java重载方法匹配的优先级是:精确匹配 > 基本类型提升 > 自动装箱/拆箱 > 可变参数
再给出一个示例:
public class OverloadDemo {
void process(int x) { System.out.println("int"); }
void process(Integer x) { System.out.println("Integer"); }
void process(Object x) { System.out.println("Object"); }
public static void main(String[] args) {
OverloadDemo demo = new OverloadDemo();
demo.process(10); // 输出 "int"
demo.process(10L); // 输出 "Object"(long → Long → Object)
demo.process(null); // 输出 "Integer"(Integer 比 Object 更具体)
}
}
八股题18:下列树形结构,用前序遍历、中序遍历和后续遍历,得到最后一位的结果是什么?
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ /
8 9 10
答案:10、7、1
解释:
| 遍历方式 | 遍历顺序 | 结果 |
|---|---|---|
| 前序遍历 | 根 → 左 → 右 | 1→2→4→8→9→5→3→6→7→10 |
| 中序遍历 | 左 → 根 → 右 | 8→4→9→2→5→1→6→3→10→7 |
| 后序遍历 | 左 → 右 → 根 | 8→9→4→5→2→6→10→7→3→1 |
八股题19:Integer的最大值加1,和最小值减一分别会等于多少?
Integer 的最大值:2,147,483,64
当对 Integer.MAX_VALUE(即 2,147,483,647)进行加一操作时:
二进制形式:0111 1111 1111 1111 1111 1111 1111 1111 + 0000 0000 0000 0000 0000 0000 0000 0001 = 1000 0000 0000 0000 0000 0000 0000 0000
根据补码规则,1000 0000 0000 0000 0000 0000 0000 0000 表示的是 -2,147,483,648(即 Integer.MIN_VALUE)
因此,Integer.MAX_VALUE + 1 的结果是 Integer.MIN_VALUE(即 -2,147,483,648)。
Integer 的最小值:-2,147,483,648
二进制表示:1000 0000 0000 0000 0000 0000 0000 0000(最高位为1表示负数,其余位为0)
当对 Integer.MIN_VALUE(即 -2,147,483,648)进行减一操作时:
二进制形式:1000 0000 0000 0000 0000 0000 0000 0000 - 0000 0000 0000 0000 0000 0000 0000 0001
在二进制补码表示中,这个操作等价于:
取反加一:0111 1111 1111 1111 1111 1111 1111 1111(这是 Integer.MAX_VALUE 的二进制表示) + 1 = 1000 0000 0000 0000 0000 0000 0000 0000(这仍然是 Integer.MIN_VALUE 的二进制表示)
因此,Integer.MIN_VALUE - 1 的结果仍然是 Integer.MIN_VALUE(即 -2,147,483,648)。
全部评论