程序员老鬼

热文:if (a == 1 && a == 2 && a == 3),有没有可能为true?

如果你跟我一样,第一次看到这个问题——if (a == 1 && a == 2 && a == 3),下意识反应就是:你是不是在逗我玩?

Image

毕竟,理性告诉我们,这不可能啊,一个变量 a 怎么可能在同一个时间点等于三个不同的值?这不是“精神分裂”了吗?要真能这样,那 Java 这门语言岂不是瞬间变成魔法语言了?但别急,真有“闲得发慌”的网友,愣是让这句看似荒诞的 if 判断,跑通了。

别眨眼,接下来咱们就来好好掰扯掰扯这个“整活”级别的问题,看看到底有没有可能让这段代码成立——并且,还真有办法做到。

第一眼看到的时候,我也想不信

要我说,这种问题放在 JavaScript 社区玩玩还行,那语言本来就“灵活得离谱”。你跟 Java 玩这个?我一开始是拒绝的。

不过当我看到网上那一段段“骚操作”,我就忍不住一边骂着“变态”,一边自己试了试。

没错,这种问题,光用逻辑是推不出来的,得靠点“非人类手段”。

JavaScript 玩的是 valueOf

在聊 Java 之前,咱先看看 JS 是怎么整活的。JS 是一个动态语言,对象在转为原始类型时,会调用 valueOf 方法。

所以你可以写出这样的骚操作:

let a = {
i: 1,
valueOf: function() {
returnthis.i++;
  }
};

if (a == 1 && a == 2 && a == 3) {
console.log("Success");
}

这玩意真的能跑,甚至还能打印出 "Success"。

这不魔法是啥?因为 JS 在做类型转换的时候,会调用 valueOf,而这个 valueOf 每次返回一个不同的值,满足了三个比较条件。

所以啊,有时候你以为你在写代码,其实你是在调戏编译器。

Java 想搞这个,得动点手段

再说回 Java,这玩意可没那么随便,它是编译型语言,类型系统死板得要命,Integer a = 1 那就是个常量池中的值,哪有机会耍小聪明?

可偏偏还真有人钻进了 Java 的“骨头缝”,拿着反射加内存篡改,把 Java 的常量池给整花了。

来,看看这个经典操作:

Class cache = Integer.class.getDeclaredClasses()[0];
Field c = cache.getDeclaredField("cache");
c.setAccessible(true);
Integer[] array = (Integer[]) c.get(cache);

// 把 IntegerCache 中的值改掉
array[129] = array[1]; // 2 -> 1
array[130] = array[1]; // 3 -> 1

Integer a = 1;

if (a == 1 && a == 2 && a == 3) {
    System.out.println("Success");
}

是不是觉得有点恐怖?

这段代码背后的原理,其实是在篡改 Java 的 IntegerCache,这个 cache 是 JVM 优化性能用的缓存池([-128, 127] 范围内的 Integer 会自动缓存)。通过反射,我们把原本表示 2 和 3 的对象,强行指向了表示 1 的对象。

所以当你写 (Integer)2 的时候,它实际上是 (Integer)1,也就是说你对比的是 a == 1 && a == 1 && a == 1,能不 true 吗?

听起来挺牛,其实风险极高

我知道你现在可能已经在敲键盘,想去试试了。

停一下。

我得提醒你,这种操作纯属炫技,日常开发千万别碰。

这不是“黑魔法”这么简单了,这是“入侵核心”,你要是真在生产环境玩这种骚操作,迟早被打回重构地狱。而且,高版本 JDK(比如 Java 17 开始)对这种反射操作已经加强了限制,要想用这招得先把 JVM 的安全策略搞定。

而且更重要的是:你为了让一个看起来像“脑筋急转弯”的条件成立,非得把整个 JVM 的整数缓存搞乱,你不觉得这本身就有点反人类吗?

还有人用 PowerMock 实现了

还有一种方法,真的是骚。

有人直接用 PowerMock,Mock 掉了 Integer.intValue() 方法的返回值——每次调用 intValue(),就返回一个递增的值。

代码如下:

@PrepareForTest(Integer.class)
@RunWith(PowerMockRunner.class)
publicclassAis123
{

@Before
publicvoidbefore(){
        AtomicInteger value = new AtomicInteger(1);
        replace(method(Integer.class, "intValue"))
            .with((proxy, method, args) -> value.getAndIncrement())
;
    }

@Test
publicvoidtest(){
        Integer a = 1;
if (a == 1 && a == 2 && a == 3) {
            System.out.println("Success");
        } else {
            Assert.fail("条件不成立,a=" + a);
        }
    }
}

这波操作属于是彻底“玩疯了”:你以为你在比较 a 和 1,其实是你在和 intValue() 每次返回的“最新数字”比。

唯一的缺点是——你要引入 PowerMock,要准备好一堆注解,要搭好测试环境,就为了一句 if (a == 1 && a == 2 && a == 3)。

所以,这种问题到底有什么意义?

很多人看到这种题第一反应是“这啥弱智题目?”其实不然,它背后藏着不少值得我们学习的点:

  1. 类型转换和缓存机制的细节:你要知道 Integer 缓存是怎么回事,才知道怎么篡改它;
  2. 反射的底层原理:会反射不稀奇,能通过反射篡改底层缓存,这才是“见识到了牙缝里的世界”;
  3. 测试框架的能力边界:PowerMock 这招,虽然离谱,但也展示了测试框架在 Mock 静态方法、final 方法时的“强制手段”。

别看这题看起来像是闹着玩,真能玩懂,也算是你 Java 基础吃得够透。

最后

说到底,这题是个彩蛋,不是用来做面试题的。

但如果你能用它带着自己去探索语言的边界、底层原理、反射机制,那它也算是一个很棒的引子。

END

我为大家打造了一份RPA教程,完全免费:https://www.songshuhezi.com/rpa.html

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领取,也可以链接我领取,微信:hls404