本文目录导读:

- 基础与对象:值传递还是引用传递?
- 创建了几个对象?">字符串常量池:
new String("xxx")创建了几个对象? - 多线程:可见性与指令重排序(
volatile实战) - 异常处理:
finally与return的复杂关系 - 类加载机制:静态块与继承的执行顺序
- 泛型:类型擦除与桥接方法
- 总结与进阶探索方向
这是一份为你准备的 Java 探索案例合集,这些案例涵盖了从基础语法到并发编程、JVM 原理以及新特性的典型场景,每个案例都配有核心知识点和代码示例,旨在通过实践帮助你深入理解 Java 的运行机制。
基础与对象:值传递还是引用传递?
核心探索: 验证 Java 只有值传递,没有引用传递。
public class ValuePassingExploration {
public static void main(String[] args) {
int a = 10;
changePrimitive(a);
System.out.println("基本类型 a 被修改后: " + a); // 输出 10
StringBuilder sb = new StringBuilder("Hello");
changeReference(sb);
System.out.println("引用类型 sb 被修改后: " + sb); // 输出 Hello World
StringBuilder sb2 = new StringBuilder("Hello");
changeReferenceNew(sb2);
System.out.println("尝试重新分配引用后: " + sb2); // 输出 Hello
}
private static void changePrimitive(int x) {
x = 20; // 修改的是副本
}
private static void changeReference(StringBuilder builder) {
builder.append(" World"); // 通过副本修改了指向的对象
}
private static void changeReferenceNew(StringBuilder builder) {
builder = new StringBuilder("World"); // 仅改变了局部变量的指向,对原对象无影响
}
}
方法无法修改传入的引用本身,但可以通过引用修改堆上的对象。
字符串常量池:new String("xxx") 创建了几个对象?
核心探索: 理解字符串常量池、intern() 方法以及堆与元空间的关系。
public class StringPoolExploration {
public static void main(String[] args) {
String s1 = "abc"; // 字面量,放入常量池
String s2 = new String("abc"); // 堆中创建新对象
String s3 = s2.intern(); // 尝试放入常量池,发现已有,返回常量池引用
System.out.println(s1 == s2); // false (堆 vs 常量池)
System.out.println(s1 == s3); // true (都是常量池引用)
// 探索动态拼接
String s4 = "a" + "b" + "c"; // 编译器优化,等同于 "abc"
System.out.println(s1 == s4); // true
String a = "a";
String b = "b";
String c = "c";
String s5 = a + b + c; // 运行时拼接,底层使用 StringBuilder,在堆中创建新对象
System.out.println(s1 == s5); // false
}
}
new String 会创建 1 个堆对象(如果常量池没有,还会在常量池创建 1 个),运行时字符串拼接会在堆中生成新对象。
多线程:可见性与指令重排序(volatile 实战)
核心探索: 不加 volatile 导致死循环,加上后保证可见性。
public class VolatileExploration {
// 重点:去掉 volatile,线程 B 可能永远看不到 A 的修改
private static volatile boolean flag = false;
// private static boolean flag = false; // 测试用
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("线程 A 启动,等待 flag 变为 true...");
while (!flag) {
// 如果不加 volatile,这里可能会无限循环(寄存器缓存)
// 如果不加 volatile,但这里加上 System.out.println(),由于 println 内部有锁也会刷新缓存
}
System.out.println("线程 A 检测到 flag 变化,退出!");
}).start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println("线程 B 启动,修改 flag...");
flag = true;
System.out.println("线程 B 已修改 flag");
}).start();
}
}
更深探索: 加 volatile 可以禁止指令重排序,这是实现 DCL(Double-Checked Locking)单例模式的关键。
异常处理:finally 与 return 的复杂关系
核心探索: 理解 finally 的执行时机以及如何影响返回值。
public class FinallyExploration {
public static void main(String[] args) {
System.out.println("result 1: " + getValue1()); // 输出 20
System.out.println("result 2: " + getValue2()); // 输出 10
System.out.println("result 3: " + getValue3()); // 输出 2
}
// case 1: finally 修改返回值
private static int getValue1() {
int a = 10;
try {
return a;
} finally {
a = 20; // 修改的是局部变量,但返回值已经拷贝到栈帧的快照中
// 对基本类型,修改 a 不影响返回值
}
}
// case 2: finally 覆盖 try 的 return
@SuppressWarnings("finally")
private static int getValue2() {
try {
return 10;
} finally {
return 20; // 覆盖了 try 中的 return
}
}
// case 3: 复杂对象引用
private static StringBuilder getValue3() {
StringBuilder sb = new StringBuilder("1");
try {
return sb;
} finally {
sb.append("0"); // 修改了对象内部状态,返回值指向的对象被改变
// 但如果 sb = new StringBuilder("2"),则对原返回值无影响
}
}
}
finally 总是在 return 之前执行,除非执行 System.exit() 或发生致命错误。
类加载机制:静态块与继承的执行顺序
核心探索: 类初始化的顺序:父类静态块 -> 子类静态块 -> 父类实例代码块/构造器 -> 子类实例代码块/构造器。
class Parent {
static { System.out.println("Parent 静态块"); }
{ System.out.println("Parent 实例代码块"); }
public Parent() { System.out.println("Parent 构造器"); }
}
class Child extends Parent {
static { System.out.println("Child 静态块"); }
{ System.out.println("Child 实例代码块"); }
public Child() { System.out.println("Child 构造器"); }
}
public class ClassInitExploration {
public static void main(String[] args) {
System.out.println("--- 首次创建 Child 对象 ---");
Child child1 = new Child();
System.out.println("--- 再次创建 Child 对象 ---");
Child child2 = new Child();
}
}
输出结果:
--- 首次创建 Child 对象 ---
Parent 静态块
Child 静态块
Parent 实例代码块
Parent 构造器
Child 实例代码块
Child 构造器
--- 再次创建 Child 对象 ---
Parent 实例代码块
Parent 构造器
Child 实例代码块
Child 构造器
静态块仅执行一次,实例代码块在每次构造对象时执行,并在构造器之前执行。
泛型:类型擦除与桥接方法
核心探索: 验证编译后泛型信息被擦除,并观察桥接方法的存在。
import java.lang.reflect.Method;
class StringBox implements Box<String> {
private String item;
@Override
public void put(String item) {
this.item = item;
}
public static void main(String[] args) {
// 通过反射查看字节码中的方法
for (Method method : StringBox.class.getDeclaredMethods()) {
System.out.println("方法名: " + method.getName() +
", 参数类型: " +
java.util.Arrays.toString(method.getParameterTypes()));
}
}
}
interface Box<T> {
void put(T item);
}
输出结果(存在桥接方法):
方法名: put, 参数类型: [class java.lang.String]
方法名: put, 参数类型: [class java.lang.Object] // 这就是编译器生成的桥接方法
泛型在运行时被擦除,桥接方法保证了多态的正确性。
总结与进阶探索方向
| 案例主题 | 核心知识点 | 难度 |
|---|---|---|
| 值传递 | 栈帧、局部变量表 | ⭐ |
| 字符串常量池 | 元空间、intern、编译优化 |
⭐⭐ |
volatile 可见性 |
CPU 缓存一致性、MESI 协议 | ⭐⭐⭐ |
finally 返回值 |
字节码、异常表、atsn 指令 |
⭐⭐ |
| 类加载顺序 | 加载、验证、准备、解析、初始化 | ⭐⭐ |
| 泛型擦除 | 桥接方法、Signature 属性 |
⭐⭐⭐ |
进一步探索建议:
- JVM 监控: 使用
VisualVM或jconsole监控上述代码的内存和线程状态。 - 反编译: 使用
javap -c -v查看finally或泛型案例的字节码,你会看到更底层的结构。 - AQS 锁: 尝试实现一个简单的
ReentrantLock案例,探索LockSupport.park()和unpark()。
这些案例不仅帮助你通过面试,更能让你真正理解 Java 的底层设计哲学。