流传很广的说法是 flag ? 0 : nullableInteger 这种写法,哪怕 flag 为 true 走的是 0 分支,也会因为编译器要统一类型而提前拆箱 nullableInteger,导致真实抛出 NullPointerException——真实反编译字节码发现在 Java 17 上这个说法不成立:只有真正选中 null 分支时才会调用 Integer.intValue()。真实测出 flag=true 时正常返回 0,flag=false 时才真实抛出 NPE。
真实验证 Aa 和 BB 这对经典碰撞:两者 hashCode() 真实都是 2112,内容完全不同,equals() 真实是 false;按 s[0]*31+s[1] 公式手动真实计算,结果和 String.hashCode() 完全一致;给两个字符串加上同样的前缀 hello-,两者的 hashCode 真实依然相等,验证了碰撞可以递归扩展到任意长度;最后把这对碰撞的 key 放进 HashMap,真实验证两个 key 尽管哈希相同,依然被正确当成两个独立条目。
声明 Animal a = new Dog(),真实调用 a.sound()(重写方法),结果按运行时真实类型 Dog 分派,输出汪汪;真实调用重载方法 describe(a),编译期声明类型是 Animal,即使运行时真实是 Dog,重载解析依然真实选中了 describe(Animal) 版本,不是 describe(Dog);把同一个对象强转成 Dog 类型再传进去,重载解析结果真实跟着变了。重载在编译期就真实决定调用哪个版本,重写在运行时才真实决定。
一个类里有 static final int CONST = 42,另一个类里有 static final int NON_CONST 但要调用方法才能算出来;真实运行发现:读取 HasConstant.CONST 之前,它的静态初始化块真实完全没有执行;读取 HasNonConstant.NON_CONST 时,静态初始化块真实执行了。用 javap 反编译发现更彻底的真相:引用 HasConstant.CONST 的地方,编译器直接把字面量 42 内联进了字符串常量池,连一次真实的 getstatic 字节码都没有生成。
对一个死循环忙等的线程调用 interrupt(),真实验证它完全不会停下来:isInterrupted() 变成 true,但循环计数器真实还在持续增长;对照一个阻塞在 Thread.sleep(60000) 里的线程,interrupt() 真实立刻让它抛出 InterruptedException 提前醒来,总耗时约 205ms,远小于计划的 60 秒;第三组验证协作式取消:循环里主动检查 isInterrupted(),真实能在 interrupt() 之后很快退出循环。interrupt() 的真实语义只是设置一个标志位,不是强制终止。
两个线程反向加锁(线程一先拿 lockA 再要 lockB,线程二先拿 lockB 再要 lockA),用 CountDownLatch 真实控制两边同时卡在互相等待的状态;不借助任何外部工具,只用 ThreadMXBean.findDeadlockedThreads(),真实检测到 2 个死锁线程,每个线程真实报告了它在等哪把锁、那把锁真实被谁占着——两个线程互为等待关系,真实、完整地闭合成一个环。
真实计时对比直接方法调用、未 setAccessible 的反射调用、已 setAccessible 的反射调用,各跑 5000 万次:直接调用约 32ms,反射调用(未 setAccessible)约 200ms,是直接调用的 6.2~6.5 倍;调用 setAccessible(true) 之后反射调用降到约 96~105ms,相对未调用时快了近 2 倍,但依然比直接调用慢约 3 倍。三组结果全程用求和结果校验一致,排除了任何计算错误的可能。
用两个独立的 URLClassLoader 加载同一份 Foo.class,真实验证:foo1 == foo2 是 false,两者是完全不同的 Class 对象;用其中一个 Class 创建的实例,真实无法强转成另一个的类型,抛出 ClassCastException: class Foo cannot be cast to class Foo——报错信息里两个 Foo 名字一模一样,区分它们的关键在括号里的类加载器信息。类在 JVM 里的身份真实是(全限定名, ClassLoader)这个二元组,不是名字本身。
真实运行 Integer.MAX_VALUE + 1,结果真实是 -2147483648,正好是 Integer.MIN_VALUE,没有抛出任何异常;用两个真实的大数 15 亿和 10 亿相加,int 版本真实得到一个负数,和真实数学结果完全对不上,程序对此毫不知情;对照用 Math.addExact 做同样的加法,真实抛出 ArithmeticException,不溢出时两者结果完全一致。long 也会溢出,只是数值范围大得多。
真实运行三个资源的 try-with-resources 验证关闭顺序:打开是 A、B、C,关闭真实是 C、B、A,和声明顺序正好相反;再让 try 主体和两个资源的 close() 都真实抛异常,catch 到的是 try 主体的异常,通过 getSuppressed() 真实拿到另外两个 close() 异常,顺序是先 B 后 A,没有一个异常真的丢失,只是被明确标记为次要的抑制异常挂在主异常下面。
写一个泛型可变参数方法 toArray(T... args) 把内部数组直接返回出去,再用它拼出经典的 pickTwo 场景:真实打印返回数组的运行时类型是 Object[],不是 String[];尝试把结果强转成 String[] 接收,真实抛出 ClassCastException。这不是编写者的疏忽,而是泛型可变参数在类型擦除下的真实、编译器拦不住的结构性缺口,SafeVarargs 注解只是承诺不做危险操作,不是消除风险。
真实验证 Collections.unmodifiableList(original) 不是深拷贝而是一层包装视图:original.add()/original.remove() 之后,拿到的 unmodifiable 引用真实同步跟着变了内容;直接对 unmodifiable 调用 add() 真实抛出 UnsupportedOperationException,证明它确实阻止了从这个引用发起的修改,但挡不住原始集合被修改;真正想要独立、不可变的快照,得先拷贝一份,再包装。