在Java中,(a==1 && a==2 && a==3) 可以是真的吗?

175

我们知道在JavaScript中可以这样做

但是在Java中,如下所示的条件是否可以打印出“成功”消息?

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

有人建议:

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

但是这样做我们正在改变实际的变量。还有其他方式吗?


5
&& 是逻辑与运算符,意味着 a 同时应该具有值 1、2 和 3,这在逻辑上是不可能的。答案是否定的,不可能实现。你想编写一个 if 语句来检查 a 是否具有值 1、2 或 3 中的一个吗? - Boris Pavlović
16
注意:这个问题可能来自于Javascript的同一个问题:https://dev59.com/HFYM5IYBdhLWcg3w3TCF ,如果是这种情况,答案是“是”。 - nos
30
不是重复问题,那个问题是针对JavaScript而不是Java的。 - nos
13
在Java中,使用空格作为变量名也是可以的,就像使用下划线一样,只不过你看不见它而已。 - tobias_k
8
@Seelenvirtuose 的说法没错,但是 if(a==1 && a==2 && a==3) 不一定在同一时间被计算。这可以用来使代码工作而无需使用 Unicode 技巧。 - Erwin Bolwidt
显示剩余15条评论
8个回答

329

如果您将变量a声明为volatile,使用多个线程很容易实现这一点。

一个线程不断地将变量a从1更改为3,另一个线程不断测试a == 1 && a == 2 && a == 3。它发生得足够频繁,可以在控制台上持续输出"Success"。

(请注意,如果添加else {System.out.println("Failure");}子句,您会发现测试失败的次数远远超过成功的次数。)

实际上,在不将a声明为volatile的情况下,它也可以工作,但在我的MacBook上只能运行21次。没有volatile,编译器或HotSpot可以缓存a或将if语句替换为if (false)。最有可能的是,HotSpot会在一段时间后启动并将其编译为缓存a值的汇编指令。 使用volatile,它将永远输出"Success"。

public class VolatileRace {
    private volatile int a;

    public void start() {
        new Thread(this::test).start();
        new Thread(this::change).start();
    }

    public void test() {
        while (true) {
            if (a == 1 && a == 2 && a == 3) {
                System.out.println("Success");
            }
        }
    }

    public void change() {
        while (true) {
            for (int i = 1; i < 4; i++) {
                a = i;
            }
        }
    }

    public static void main(String[] args) {
        new VolatileRace().start();
    }
}

84
这是一个很棒的例子!我想下次和初级开发人员讨论可能存在的并发问题时,我会借鉴它 :) - Alex Pruss
6
没有 volatile 关键字的情况下,这种情况不太可能发生,但原则上即使没有 volatile 关键字也可以发生,而且可能会发生任意次数,但另一方面,即使使用了 volatile 关键字也不能保证它一定会发生。当然,在实践中发生这种情况是相当令人印象深刻的。 - Holger
5
@AlexPruss,我刚刚对公司所有开发人员进行了测试,不仅仅是初级开发人员(我知道这可能是可能的),在答案方面有87%的成功率和失败率。 - Eugene
3
@Holger,没有任何保证。在典型的多核心或多CPU架构中,在两个线程都被分配到不同核心的情况下,使用volatile是很可能发生的。为了实现volatile而创建的内存屏障会降低线程速度,并增加它们在短时间内同步操作的可能性。这种情况比我预期的要频繁得多。虽然这非常依赖于时间,但我大约看到0.2%至0.8%的(a == 1 && a == 2 && a == 3)计算结果返回true - Erwin Bolwidt
为什么匿名类在变量a不是final或有效final的情况下仍然可以访问它?如果我错了,请纠正我,您的代码似乎与Java 8文档相矛盾。 - dkonayuki
1
@dkonayuki a 是一个字段,而不是一个局部变量。'final' 和 'effectively final' 规则只适用于局部变量。 - Erwin Bolwidt

88

利用来自一篇出色的代码高尔夫答案的概念(和代码),可以玩弄 Integer 值。

在这种情况下,它可以使得被转换为 Integer int 在通常情况下不相等时相等:

import java.lang.reflect.Field;

public class Test
{
    public static void main(String[] args) throws Exception
    {
        Class cache = Integer.class.getDeclaredClasses()[0];
        Field c = cache.getDeclaredField("cache");
        c.setAccessible(true);
        Integer[] array = (Integer[]) c.get(cache);
        // array[129] is 1
        array[130] = array[129]; // Set 2 to be 1
        array[131] = array[129]; // Set 3 to be 1

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

不幸的是,它不像 Erwin Bolwidt的多线程答案 那样优雅 (因为这个需要Integer转型),但仍然有一些有趣的骗局发生。


非常有趣。我一直在玩弄 Integer 的滥用。虽然转换很遗憾,但它仍然很酷。 - Michael
@Michael 我还不能完全想明白如何消除显式类型转换。当a设置为1/2/3时,它满足a == 1,但反过来则不一定成立。 - phflack
4
几天前我在 JS/Ruby/Python 和 Java 中回答了这个问题。我找到的最好的版本是使用 a.equals(1) && a.equals(2) && a.equals(3),这将强制 123 被自动装箱为 Integer - Eric Duminil
@EricDuminil 这可能会让一些乐趣消失,因为如果您将a设置为具有boolean equals(int i){return true;}的类,那该怎么办? - phflack
1
就像我说的那样,这是最不丑陋的方法,但仍然不是最优的。如果在前一行使用 Integer a = 1;,那么很明显 a 真的是一个整数。 - Eric Duminil
据我了解,“缓存(cache)”是Hotspot JVM中的实现优化细节,可能在其他JVM上无法使用。 - Thorbjørn Ravn Andersen

52

这个问题中,@aioobe建议(并反对)在Java类中使用C预处理器。

虽然这非常不正当,但那是我的解决方案:

#define a evil++

public class Main {
    public static void main(String[] args) {
        int evil = 1;
        if (a==1 && a==2 && a==3)
            System.out.println("Success");
    }
}
如果使用以下命令执行,它将输出确切地一个Success:
cpp -P src/Main.java Main.java && javac Main.java && java Main

6
这有点像在编译代码之前运行“查找和替换”的操作,但我可以看出有人可能会将其作为工作流程的一部分。 - phflack
1
@phflack 我喜欢,可以说这个问题是展示阅读代码时做出假设的危险。在具有预处理器的语言中,很容易假设 a 是一个变量,但它可以是任何任意的代码片段。 - kevin
2
这不是Java。它是经过C预处理器处理后的“Java语言”。类似的漏洞被答案使用。(注意:现在对于Code Golf来说,[tag:underhanded]是离题的) - user202729
1
我相信这正是Java没有预处理器的确切原因。 - Thorbjørn Ravn Andersen

40

正如我们已经知道的,由于Erwin Bolwidt和phflack提供的出色答案,使得这段代码能够评估为true。我想展示的是,在处理类似于问题中呈现的条件时,需要保持高度的注意力,因为有时您看到的可能不完全是您认为的。

这是我尝试显示此代码将Success!打印到控制台。 我承认我有点作弊,但我仍然认为这是一个好地方来展示它。

无论编写此类代码的目的是什么 - 最好了解如何处理以下情况以及如何检查您是否对所看到的东西感到困惑。

我使用了Cyrillic 'a',它是与拉丁字母'a'不同的单独字符。您可以检查if语句中使用的字符here

这起作用是因为变量名称来自不同的字母表。它们是不同的标识符,创建了两个具有不同值的不同变量。

请注意,如果要使此代码正常工作,则需要更改字符编码以支持两个字符,例如所有Unicode编码(UTF-8、UTF-16(BE或LE)、UTF-32,甚至UTF-7),或Windows-1251、ISO 8859-5、KOI8-R(非常感谢Thomas Weller和Paŭlo Ebermann指出):

public class A {
    public static void main(String[] args) {
        int а = 0;
        int a = 1;
        if(а == 0 && a == 1) {
            System.out.println("Success!");
        }
    }
}

我希望你将来永远不必面对那种问题。


@Michael 你说它显示效果不好是什么意思?我是在手机上写的,这里看起来还可以,即使切换到桌面视图也没问题。如果编辑一下能让我的回答更好,请随意修改,让它更清晰易懂,因为我现在觉得有点困难。 - Przemysław Moskal
@mohsenmadi 是的,我不能在手机上检查它,但我很确定在编辑之前,最后一句话是关于在我的示例中使用不同的语言:P - Przemysław Moskal
为什么 █==0 应该与 a==1 相似? - Thomas Weller
1
更加挑剔的是:你不一定需要UTF-8(尽管这仍然是推荐的),只要有аa的任何字符编码都可以,只要你告诉你的编辑器它使用的是哪种编码(可能还要告诉编译器)。所有Unicode编码都可以工作(UTF-8,UTF-16(BE或LE),UTF-32,甚至UTF-7),例如Windows-1251,ISO 8859-5,KOI8-R也可以。 - Paŭlo Ebermann
我已经遇到过这个问题。我的一个计算机代数系统(Derive 6)中,mu是µ的同义词(即,如果我输入mu并让它简化表达式,mu会变成µ)。但是,有人搞错了,简化后的mu和Alt GR µ是不同的字母... - Sudix
显示剩余4条评论

29

除了我之前发布的易失性数据竞争方法,还有另一种方法可以解决这个问题,那就是使用PowerMock的功能。PowerMock允许用其他实现来替换方法。当这与自动拆箱相结合时,原始表达式(a == 1 && a == 2 && a == 3)可以保持不变而成为真。

@phflack的答案依赖于修改Java中使用Integer.valueOf(...)调用的自动装箱过程。下面的方法依赖于修改自动拆箱方式,通过更改Integer.intValue()调用。

以下方法的优点是使用了OP在问题中给出的原始if语句,这是我认为最优雅的方法。

import static org.powermock.api.support.membermodification.MemberMatcher.method;
import static org.powermock.api.support.membermodification.MemberModifier.replace;

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@PrepareForTest(Integer.class)
@RunWith(PowerMockRunner.class)
public class Ais123 {
    @Before
    public void before() {
        // "value" is just a place to store an incrementing integer
        AtomicInteger value = new AtomicInteger(1);
        replace(method(Integer.class, "intValue"))
            .with((proxy, method, args) -> value.getAndIncrement());
    }

    @Test
    public void test() {
        Integer a = 1;

        if (a == 1 && a == 2 && a == 3) {
            System.out.println("Success");
        } else {
            Assert.fail("(a == 1 && a == 2 && a == 3) != true, a = " + a.intValue());
        }
    }

}

Powermock能否替代像是access$nnn这样的合成方法来读取内部/外部类的私有字段?这将允许一些其他有趣的变体(甚至适用于int变量)... - Holger
@Holger 有趣的想法,我明白你的意思。它还没有起作用 - 不清楚是什么阻止了它的工作。 - Erwin Bolwidt
真不错,这正是我想做的事情,但我发现在不操作字节码的情况下,改变不可变类的静态方法会相当困难。看来我错了,虽然我从未听说过PowerMock,但我想知道它是如何做到的。顺便说一句,phflack的答案并不依赖于自动装箱:他改变了缓存整数的地址(因此==实际上比较的是Integer对象的地址而不是值)。 - Asoub
2
@Asoub 强制转换((Integer)2)将int装箱。在更深入地研究反射时,看起来使用反射无法进行拆箱,但可能可以使用Instrumentation代替_(或者像这个答案中一样使用PowerMock)_。 - phflack
@Holger 可能不会拦截合成方法,尽管它允许我注册 access$0 的替换项,并在注册时检查方法的存在。但是替换从未被调用。 - Erwin Bolwidt

18

因为这似乎是此 JavaScript 问题的后续,值得注意的是,这个技巧和类似的方法在 Java 中也适用:

public class Q48383521 {
    public static void main(String[] args) {
        int aᅠ = 1;
        int2 = 3;
        int a = 3;
        if(aᅠ==1 && a==ᅠ2 && a==3) {
            System.out.println("success");
        }
    }
}

在Ideone上查看


但请注意,这不是您可以使用Unicode做的最糟糕的事情。使用作为有效标识符部分的空格或控制字符,或者使用外观相同的不同字母,仍会创建不同的标识符,并且可以在进行文本搜索时被发现。

但这个程序

public class Q48383521 {
    public static void main(String[] args) {
        int ä = 1;
        int ä = 2;
        if(ä == 1 && ä == 2) {
            System.out.println("success");
        }
    }
}

从Unicode的角度来看,ä使用了两个相同的标识符,只是用不同的方式进行编码,其中一种是 U+00E4,另一种是 U+0061 U+0308

在Ideone上查看

因此,取决于你使用的工具,它们可能不仅外观相同,而且启用Unicode的文本工具甚至可能不报告任何差异,始终在搜索时发现两者。你甚至可能会遇到这样的问题:在将源代码复制给其他人以获取帮助解决“奇怪行为”时,不同的表示会丢失,从而使帮助者无法再现。


3
这个回答是否已经很好地涵盖了 Unicode 滥用的问题? - Michael
2
int ᅠ2 = 3; 这是有意为之的吗?因为我看到周围都是非常奇怪的代码。 - Ravi
1
@Michael 不完全是,因为那个回答是关于使用两个不同的字母(当IDE在搜索“a”时会将其视为不同) ,而这是关于空格的,并且它在相关的JavaScript问题上给出了更早的答案。请注意,我的评论比Java问题(几天前)甚至还要旧... - Holger
@Ravi:是的,它基于我更早的评论,在类似的JavaScript问题上。 - Holger
2
我看不出区别。两个答案都提供了一个解决方案,其中if语句中的三个标识符在视觉上相似但在技术上通过使用不寻常的Unicode字符而是不同的。无论是空格还是西里尔字母都没有关系。 - Michael
2
@Michael,你可以这样看待它,这取决于你。正如我所说,我想表明五天前针对JavaScript给出的答案也适用于Java,只是考虑到问题的历史。是的,这有点多余,因为另一个答案并没有涉及到链接的JavaScript问题。无论如何,在此期间,我更新了我的答案,添加了一个不涉及视觉上相似字符,而是不同编码方式的情况,甚至不是一个“不寻常的Unicode字符”;这是我每天都在使用的一个字符... - Holger

4
受 @Erwin 出色的 答案 启发,我写了一个类似的示例,但使用了 Java 的流 API。有趣的是,我的解决方案有效,但只在非常罕见的情况下(因为just-in-time编译器会优化这样的代码)。诀窍是使用以下VM选项禁用任何JIT优化:
-Djava.compiler=NONE

在这种情况下,成功案例的数量显著增加。以下是代码:
class Race {
    private static int a;

    public static void main(String[] args) {
        IntStream.range(0, 100_000).parallel().forEach(i -> {
            a = 1;
            a = 2;
            a = 3;
            testValue();
        });
    }

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

P.S. 并行流在底层使用 ForkJoinPool,变量 a 在多个线程之间共享且没有任何同步,因此结果是不确定的。


1

类似的,通过将浮点数(或双精度数)强制进行除法(或乘法)操作,并使其下溢(或上溢):

点击此处了解更多。

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

2
@PatrickRoberts - 这种特定的行为是浮点数下溢,根据Java文档。还可以参考这个这个这个这个 - Aloke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接