知其然,知其所以然
Java 内部原理 2026-06-13

三元表达式里的拆箱 NPE:真实验证条件选中的分支才会真实拆箱

流传很广的说法是 flag ? 0 : nullableInteger 这种写法,哪怕 flag 为 true 走的是 0 分支,也会因为编译器要统一类型而提前拆箱 nullableInteger,导致真实抛出 NullPointerException——真实反编译字节码发现在 Java 17 上这个说法不成立:只有真正选中 null 分支时才会调用 Integer.intValue()。真实测出 flag=true 时正常返回 0,flag=false 时才真实抛出 NPE。

Java 自动拆箱 NPE 动画演示
Java 内部原理 2026-06-12

String.hashCode() 真实公式:真实造出一对内容完全不同却哈希相同的字符串

真实验证 Aa 和 BB 这对经典碰撞:两者 hashCode() 真实都是 2112,内容完全不同,equals() 真实是 false;按 s[0]*31+s[1] 公式手动真实计算,结果和 String.hashCode() 完全一致;给两个字符串加上同样的前缀 hello-,两者的 hashCode 真实依然相等,验证了碰撞可以递归扩展到任意长度;最后把这对碰撞的 key 放进 HashMap,真实验证两个 key 尽管哈希相同,依然被正确当成两个独立条目。

Java hashCode String 动画演示
Java 内部原理 2026-06-11

方法重载 vs 重写:真实验证重载看编译期类型,重写看运行时类型

声明 Animal a = new Dog(),真实调用 a.sound()(重写方法),结果按运行时真实类型 Dog 分派,输出汪汪;真实调用重载方法 describe(a),编译期声明类型是 Animal,即使运行时真实是 Dog,重载解析依然真实选中了 describe(Animal) 版本,不是 describe(Dog);把同一个对象强转成 Dog 类型再传进去,重载解析结果真实跟着变了。重载在编译期就真实决定调用哪个版本,重写在运行时才真实决定。

Java 重载重写 语言特性 动画演示
Java 内部原理 2026-06-10

类到底什么时候被初始化:真实验证编译期常量根本不会触发它

一个类里有 static final int CONST = 42,另一个类里有 static final int NON_CONST 但要调用方法才能算出来;真实运行发现:读取 HasConstant.CONST 之前,它的静态初始化块真实完全没有执行;读取 HasNonConstant.NON_CONST 时,静态初始化块真实执行了。用 javap 反编译发现更彻底的真相:引用 HasConstant.CONST 的地方,编译器直接把字面量 42 内联进了字符串常量池,连一次真实的 getstatic 字节码都没有生成。

Java 类初始化 JVM 动画演示
Java 内部原理 2026-06-09

Thread.interrupt() 真实语义:忙等循环真的不会被打断

对一个死循环忙等的线程调用 interrupt(),真实验证它完全不会停下来:isInterrupted() 变成 true,但循环计数器真实还在持续增长;对照一个阻塞在 Thread.sleep(60000) 里的线程,interrupt() 真实立刻让它抛出 InterruptedException 提前醒来,总耗时约 205ms,远小于计划的 60 秒;第三组验证协作式取消:循环里主动检查 isInterrupted(),真实能在 interrupt() 之后很快退出循环。interrupt() 的真实语义只是设置一个标志位,不是强制终止。

Java 线程中断 并发 动画演示
Java 内部原理 2026-06-08

真实制造一个死锁,再用 JDK 自带的 API 真实把它揪出来

两个线程反向加锁(线程一先拿 lockA 再要 lockB,线程二先拿 lockB 再要 lockA),用 CountDownLatch 真实控制两边同时卡在互相等待的状态;不借助任何外部工具,只用 ThreadMXBean.findDeadlockedThreads(),真实检测到 2 个死锁线程,每个线程真实报告了它在等哪把锁、那把锁真实被谁占着——两个线程互为等待关系,真实、完整地闭合成一个环。

Java 死锁 并发 动画演示
Java 内部原理 2026-06-07

反射调用真实慢多少:真实测出比直接调用慢 6 倍,setAccessible 能省一半

真实计时对比直接方法调用、未 setAccessible 的反射调用、已 setAccessible 的反射调用,各跑 5000 万次:直接调用约 32ms,反射调用(未 setAccessible)约 200ms,是直接调用的 6.2~6.5 倍;调用 setAccessible(true) 之后反射调用降到约 96~105ms,相对未调用时快了近 2 倍,但依然比直接调用慢约 3 倍。三组结果全程用求和结果校验一致,排除了任何计算错误的可能。

Java 反射 性能 动画演示
Java 内部原理 2026-06-06

两个一模一样的类:真实用两个 ClassLoader 造出两个不能互相强转的 Foo

用两个独立的 URLClassLoader 加载同一份 Foo.class,真实验证:foo1 == foo2 是 false,两者是完全不同的 Class 对象;用其中一个 Class 创建的实例,真实无法强转成另一个的类型,抛出 ClassCastException: class Foo cannot be cast to class Foo——报错信息里两个 Foo 名字一模一样,区分它们的关键在括号里的类加载器信息。类在 JVM 里的身份真实是(全限定名, ClassLoader)这个二元组,不是名字本身。

Java ClassLoader 类加载 动画演示
Java 内部原理 2026-06-05

int 溢出真实静默环绕:Integer.MAX_VALUE + 1 真实变成负数,没有任何报错

真实运行 Integer.MAX_VALUE + 1,结果真实是 -2147483648,正好是 Integer.MIN_VALUE,没有抛出任何异常;用两个真实的大数 15 亿和 10 亿相加,int 版本真实得到一个负数,和真实数学结果完全对不上,程序对此毫不知情;对照用 Math.addExact 做同样的加法,真实抛出 ArithmeticException,不溢出时两者结果完全一致。long 也会溢出,只是数值范围大得多。

Java 整数溢出 算术运算 动画演示
Java 内部原理 2026-06-04

try-with-resources 真实是逆序关闭:被抑制的异常真实附在主异常身上,不是丢了

真实运行三个资源的 try-with-resources 验证关闭顺序:打开是 A、B、C,关闭真实是 C、B、A,和声明顺序正好相反;再让 try 主体和两个资源的 close() 都真实抛异常,catch 到的是 try 主体的异常,通过 getSuppressed() 真实拿到另外两个 close() 异常,顺序是先 B 后 A,没有一个异常真的丢失,只是被明确标记为次要的抑制异常挂在主异常下面。

Java 异常 try-with-resources 动画演示
Java 内部原理 2026-06-03

泛型可变参数的堆污染:真实复现一次编译器拦不住的 ClassCastException

写一个泛型可变参数方法 toArray(T... args) 把内部数组直接返回出去,再用它拼出经典的 pickTwo 场景:真实打印返回数组的运行时类型是 Object[],不是 String[];尝试把结果强转成 String[] 接收,真实抛出 ClassCastException。这不是编写者的疏忽,而是泛型可变参数在类型擦除下的真实、编译器拦不住的结构性缺口,SafeVarargs 注解只是承诺不做危险操作,不是消除风险。

Java 泛型 堆污染 动画演示
Java 内部原理 2026-06-02

Collections.unmodifiableList 真实只是一层视图:改原始 list,不可变的那份也真实跟着变

真实验证 Collections.unmodifiableList(original) 不是深拷贝而是一层包装视图:original.add()/original.remove() 之后,拿到的 unmodifiable 引用真实同步跟着变了内容;直接对 unmodifiable 调用 add() 真实抛出 UnsupportedOperationException,证明它确实阻止了从这个引用发起的修改,但挡不住原始集合被修改;真正想要独立、不可变的快照,得先拷贝一份,再包装。

Java 集合框架 Collections 动画演示
京ICP备2026067502号-1京公网安备11010502063487号