Java中的快捷“或赋值”(|=)运算符

96

我在Java中有一长串比较语句,希望知道其中一个或多个是否为真。由于长且难以阅读,我将其分解以提高可读性,并自动使用了快捷操作符|=而非negativeValue = negativeValue || boolean

boolean negativeValue = false;
negativeValue |= (defaultStock < 0);
negativeValue |= (defaultWholesale < 0);
negativeValue |= (defaultRetail < 0);
negativeValue |= (defaultDelivery < 0);

如果默认的<something>值中有任何一个是负数,我希望negativeValue为true。这样做是否有效?它会按照我的期望工作吗?我在Sun的网站或stackoverflow上没有看到相关信息,但是Eclipse似乎没有问题,代码可以编译和运行。


同样地,如果我想要执行多个逻辑交集,我能否使用&=代替&&


13
为什么不尝试一下呢? - Roman
16
不,这里有特定的行为。Java 可以 选择使|=短路,这样如果LHS已经为真,它就不会评估RHS - 但它没有这样做。 - Jon Skeet
哎呀,糟糕,我错过了那个。但在这个例子中,RHS没有副作用,所以除了性能之外,这种行为在这里并不起作用,对吧? - Dykam
3
@Jon Skeet:短路运算符对于不存在的||=操作符是合适的,但是|=是按位或运算符的组合形式。 - David Thornley
1
@Jon Skeet:当然,但是让|=短路会与其他复合赋值运算符不一致,因为a |= b;不等同于a = a | b;,通常要注意评估a两次(如果有必要)。在我看来,大的语言行为决策是没有||=,所以我不明白你的观点。 - David Thornley
显示剩余3条评论
8个回答

219

|=是一个复合赋值运算符(JLS 15.26.2),用于布尔逻辑运算符|JLS 15.22.2),不要与条件或||JLS 15.24)混淆。对应于布尔逻辑&^的复合赋值版本也有&=^=

换句话说,对于boolean b1,b2,这两个语句是等价的:

 b1 |= b2;
 b1 = b1 | b2;
逻辑运算符(&|)与它们的条件运算符(&&||)相比,不同之处在于前者不会“短路”,而后者会。也就是说:
  • &| 总是评估两个操作数
  • &&|| 仅在右操作数的值可能影响二元运算结果时才有条件地评估右操作数;这意味着当以下情况发生时,不会评估右操作数:
    • && 的左操作数评估为 false
      • (因为无论右操作数评估为什么,整个表达式都是 false)
    • || 的左操作数评估为 true
      • (因为无论右操作数评估为什么,整个表达式都是 true)

所以回到你最初的问题,是的,那种构造方法是有效的,虽然 |= 不完全等同于 =|| 的快捷方式,但它确实可以计算你想要的结果。由于你的用法中 |= 运算符右侧是一个简单的整数比较操作,因此 | 不短路是无关紧要的。

有时确实需要或者必须进行短路运算,但是在你的场景下不需要进行短路运算。

不幸的是,与其他一些编程语言不同,Java 没有 &&=||=。这在问题 为什么 Java 没有条件-和、条件-或运算符的复合赋值版本?(&&=||= 中讨论过。


+1,非常全面。如果编译器能够确定右侧表达式没有副作用,那么它很可能会转换为短路运算符。对此有什么线索吗? - Carl
我读到当右手边是微不足道的且顺序计算不必要时,“智能”的顺序计算运算符实际上会慢一些。如果属实,那么更有趣的是想知道某些编译器是否可以在特定情况下将顺序计算转换为非顺序计算。 - polygenelubricants
@polygenelubricants 短路运算符在底层涉及某种分支,因此如果使用运算符的真值模式和/或正在使用的架构没有良好/任何分支预测友好模式(并且假设编译器和/或虚拟机不会自行进行任何相关优化),那么是的,与非短路运算符相比,SC运算符将引入一些缓慢。找出编译器是否有任何操作的最佳方法是使用SC vs. NSC编译程序,然后比较字节码是否不同。 - JAB
另外,不要将其与按位或运算符混淆,后者也是 | - user253751

16

它不像 || 和 && 那样是“快捷”(或短路)运算符(如果基于 LHS 已经知道结果,则它们不会计算 RHS),但是它将按您所需的方式运行

作为区别的例子,如果 text 为空,则此代码将正常运行:

boolean nullOrEmpty = text == null || text.equals("")

然而这个不会:

boolean nullOrEmpty = false;
nullOrEmpty |= text == null;
nullOrEmpty |= text.equals(""); // Throws exception if text is null

(显然,针对这种特定情况,您可以使用"".equals(text) - 我只是想演示原则。)

(很明显您可以对于那个特定的情况使用"".equals(text) - 我只是试图阐述原则。)


3

你可以只有一条语句。将其分成多行后,它几乎与你的示例代码完全相同,只是不那么命令式:

boolean negativeValue
    = defaultStock < 0 
    | defaultWholesale < 0
    | defaultRetail < 0
    | defaultDelivery < 0;

对于最简单的表达式,使用|||更快,因为虽然它避免了进行比较,但它意味着隐式使用分支,而这可能会更加昂贵。


我最初只使用了一个,但正如原问题中所述,“比较字符串很长且难以阅读,因此我将其分解以提高可读性”。除此之外,在这种情况下,我更感兴趣的是学习 |= 的行为,而不是使这个特定的代码片段工作。 - David Mason
我似乎找不到这个特定运算符的好用例。我的意思是,如果你只关心结果,为什么不在满足条件的地方停止呢? - Farid
1
@Farid 有时候你不想停下来,比如说我想检测一系列任务中是否有任何一个已经完成。我有一个“繁忙”标志,因为其中一个任务已经完成并不意味着我不想运行其他任务,但如果没有任务在繁忙状态,我就想采取某种行动。 - Peter Lawrey

1

虽然这是一个老帖子,但为了为初学者提供不同的视角,我想举个例子。

我认为类似复合运算符最常见的用法是+=。我相信我们都写过这样的代码:

int a = 10;   // a = 10
a += 5;   // a = 15

这是什么意思?这是为了避免样板文件和消除重复的代码。
因此,下一行完全相同,避免在同一行中输入变量b1两次。
b1 |= b2;

1
在操作和运算符名称特别长的情况下,很难发现错误。longNameOfAccumulatorAVariable += 5;longNameOfAccumulatorAVariable = longNameOfAccumulatorVVariable + 5;相比较。 - Andrei

1
如果涉及可读性,我已经将测试数据与测试逻辑分离的概念进行了测试。代码示例:
// declare data
DataType [] dataToTest = new DataType[] {
    defaultStock,
    defaultWholesale,
    defaultRetail,
    defaultDelivery
}

// define logic
boolean checkIfAnyNegative(DataType [] data) {
    boolean negativeValue = false;
    int i = 0;
    while (!negativeValue && i < data.length) {
        negativeValue = data[i++] < 0;
    }
    return negativeValue;
}

代码看起来更冗长和自解释。你甚至可以在方法调用中创建一个数组,像这样:
checkIfAnyNegative(new DataType[] {
    defaultStock,
    defaultWholesale,
    defaultRetail,
    defaultDelivery
});

这个术语比“比较字符串”更易读,并且具有短路的性能优势(以数组分配和方法调用为代价)。

编辑: 通过使用可变参数,甚至可以更加易读:

方法签名将是:

boolean checkIfAnyNegative(DataType ... data)

而调用可能看起来像这样:

checkIfAnyNegative( defaultStock, defaultWholesale, defaultRetail, defaultDelivery );

1
数组分配和方法调用对于短路来说是一个相当大的代价,除非在比较中有一些昂贵的操作(虽然问题中的示例很便宜)。话虽如此,大多数情况下,代码的可维护性将优先于性能考虑。如果我要在许多不同的地方以不同的方式进行比较或比较超过4个值,我可能会使用类似这样的东西,但对于单个案例而言,它对我的口味来说有点啰嗦。 - David Mason
@DavidMason 我同意。但请记住,现代大多数计算器在不到几毫秒的时间内就能处理这种开销。个人认为,在出现性能问题之前,我不会关心开销是否合理(http://en.wikipedia.org/wiki/Program_optimization#When_to_optimize)。此外,代码冗长是一种优势,特别是当没有提供或由JAutodoc生成javadoc时。 - Krzysztof Jabłoński

1

虽然对于你的问题来说可能有些过度,但Guava库具有一些很好的语法和Predicate的短路评估或/和Predicate

本质上,比较被转换为对象,打包到集合中,然后进行迭代。对于or谓词,第一个true命中从迭代返回,反之亦然。


0
List<Integer> params = Arrays.asList (defaultStock, defaultWholesale, 
                                       defaultRetail, defaultDelivery);
int minParam = Collections.min (params);
negativeValue = minParam < 0;

我认为我更喜欢 negativeValue = defaultStock < 0 || defaultWholesale < 0 等。除了这里所有的装箱和包装的低效率之外,我并不觉得它很容易理解你的代码真正意味着什么。 - Jon Skeet
如果他甚至有超过4个参数,并且标准对所有参数都相同,那么我喜欢我的解决方案,但出于可读性的考虑,我会将其分成几行(即创建列表、查找最小值、将最小值与0进行比较)。 - Roman
这看起来对于大量比较非常有用。在这种情况下,我认为牺牲清晰度并不值得节省打字等方面的时间。 - David Mason

0

|| 逻辑或
| 按位或

|= 按位包含或赋值运算符

|= 不会短路的原因是它执行的是按位或而不是逻辑或。也就是说:

C |= 2 等同于 C = C | 2

Java 运算符教程


布尔值没有按位或运算!操作符 | 是整数按位或运算,同时也是逻辑或运算 - 参见Java语言规范15.22.2 - user85421
你是正确的,因为对于单个位(布尔值),按位与和逻辑或是等效的。虽然在实际中,结果是相同的。 - oneklc

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