Bitwise vs Logical - AND OR

Jun 18th, 2022

9 min read

kotlinjavapython

The focus of this post is not about how a bitwise operator works, so here's a quick review if you need a refresher.

Instead, we'll be covering what happens when Kotlin's bitwise operators are called with Boolean values, thereby invoking their logical equivalents. We'll only focus on the bitwise operations AND and inclusive OR.

The Initial Confusion

The motivation for this post came while looking over Kotlin's operator functions and its flexibility for the custom overloading of any predefined symbolic representations.

The day-to-day operators all have functions that are resolved by the compiler. For example, a + b is translated to a.plus(b), a++ to a.inc(), and a..b to a.rangeTo(b). The compiler even performs a translation for augmented assignments (a /= b becomes a.divAssign(b)) and comparison operators (a > b becomes a.compareTo(b) > 0). These functions are, of course, accessible as part of the standard library.

This brought to mind two questions:

  1. Can we then assume that, if a and b are Boolean values, a && b would translate to a.and(b)?
  2. Can a && b be used interchangeably with a and b?

⚠️ The answers are no and sort of, but also no.

The Facts

Java has the bitwise operators & and |, as well as the conditional operators && and ||.

Unlike Java, Kotlin technically has no bitwise operators; instead, infix functions and() and or() are used with Byte, Int, Short, or Long types. Kotlin does have the same logical operators && and ||.

If the infix function and() is invoked with two Boolean values, instead of the previously mentioned Number subclasses, it performs a logical AND operation instead of a bitwise operation; however, the official docs make it very clear that this operation is not equivalent to that performed by the && operator:

... Unlike the && operator, this function does not perform short-circuit evaluation. Both this and other will always be evaluated.

So that's a no to both questions, easily found in the documentation.

To confirm this, we can write some basic lines that use the three versions in question:

1fun foo(a: Boolean, b: Boolean) {
2 val bool1 = a.and(b)
3 val bool2 = a and b
4 val bool3 = a && b
5 println("$bool1 $bool2 $bool3")
6}
kt

Then we can use IntelliJ IDEA's handy Tools > Kotlin > Show Kotlin Bytecode > Decompile, to see the Java equivalent of our decompiled code:

1public static final void foo(boolean a, boolean b) {
2 boolean bool1 = a & b;
3 boolean bool2 = a & b;
4 boolean bool3 = a && b;
5 String var5 = "" + bool1 + ' ' + bool2 + ' ' + bool3;
6 System.out.println(var5);
7}
java

The infix function and(), as well as its alternative notation and, corresponds to the Java operator &, which performs a logical AND operation on two boolean values using non-short-circuit evaluation. While &&, in both Java and Kotlin, performs the same operation, but using minimal evaluation.

The same pattern applies for a.or(b), a or b, and a || b.

Bitwise Augmented Assignment

In addition to not having bitwise operators, Kotlin does not have the bitwise assignment operators that Java has: &= |= ^= <<= >>=. Any assignment from the result of a bitwise operation has to be written out in full instead of in shorthand.

1enum class Element(val flag: Int) {
2 EARTH(1 shl 0), // 0b0001
3 WIND(1 shl 1), // 0b0010
4 FIRE(1 shl 2), // 0b0100
5 WATER(1 shl 3) // 0b1000
6}
7
8fun main() {
9 var stone = 0
10 for (element in Element.values()) {
11 // add a flag
12 stone = stone or element.flag
13 }
14
15 // check for a flag
16 println("Stone contains fire: ${stone and Element.FIRE.flag != 0}")
17
18 // remove 1 flag
19 stone = stone and Element.FIRE.flag.inv()
20 println("Stone contains fire: ${stone and Element.FIRE.flag != 0}")
21
22 // remove 2 flags
23 stone = stone and (Element.WIND.flag or Element.EARTH.flag).inv()
24
25 print("Stone does not contain:")
26 for (element in Element.values()) {
27 // check for absence of flag
28 if (stone and element.flag == 0) {
29 print(" ${element.name}")
30 }
31 }
32 println()
33}
kt
Stone contains fire: true
Stone contains fire: false
Stone does not contain: EARTH WIND FIRE

Short-Circuit Evaluation

My answer to question #2 was "sort of, but also no". Technically, using && and and will produce the same boolean result:

1val a = 10
2val ans1 = (a > 10) && (a % 2 == 0)
3val ans2 = (a > 10) and (a % 2 == 0)
4println(ans1) // false
5println(ans2) // false
kt

Both variables ans1 and ans2 have the same value, but there is a distinguishing factor that isn't obvious due to the simplicity of this codeblock. On line 2, moving left-to-right while evaluating the right-hand side, a > 10 is first evaluated as false. The rest of the right-hand side is not reached (as both operands must evaluate to true for the result to be true) and ans1 is assigned its value. On line 3, in spite of the same first evaluation occurring, a % 2 == 0 is still executed before assignment occurs.

So, in spite of the same final result, the overall effect (be it execution speed, memory use, or side effects) may potentially differ depending on how your code is structured. To better understand this, let's look at some more detailed examples of short-circuit evaluation.

When it concerns a logical conjunction (AND), short-circuit evaluation using && means that the second argument in the expression is not evaluated if the first evaluates to false.

1fun Int.isSafelyDivisibleBy(d: Int): Boolean {
2 return (d != 0) && (1.0 * this / d == 1.0 * (this / d))
3}
4
5fun Int.isDangerouslyDivisibleBy(d: Int): Boolean {
6 return (d != 0) and (1.0 * this / d == 1.0 * (this / d))
7}
8
9fun main() {
10 println(10.isSafelyDivisibleBy(0)) // false
11 println(10.isDangerouslyDivisibleBy(0)) // throws ArithmeticException
12}
kt

Short-circuit evaluation can be used to avoid runtime errors or second argument side effects. While less needed in Kotlin due to its null safety options, minimal evaluation with && can still be used to explicitly check for nulls in conditional blocks, thereby avoiding null reference exceptions.

When it concerns a logical disjunction (OR), short-circuit evaluation using || means that the second argument in the expression is not evaluated if the first evaluates to true.

1fun Long.isPrimeMR(): Boolean {
2 println("Performing expensive operation...")
3 // ✂️
4 return true
5}
6
7fun main() {
8 val primes = mutableListOf<Long>()
9 // ✂️ Generate the first 1000 primes and store in list
10 val num = 2801L // already stored in list
11 println("Using short-circuit eval...")
12 if ((primes.binarySearch(num) != -1) || num.isPrimeMR()) {
13 println("Found a prime!")
14 }
15 println()
16 println("Using non-short-circuit eval...")
17 if ((primes.binarySearch(num) != -1) or num.isPrimeMR()) {
18 println("Found a prime!")
19 }
20}
kt
Using short-circuit eval...
Found a prime!
Using non-short-circuit eval...
Performing expensive operation...
Found a prime!

Short-circuit evaluation can also be used to create more efficient conditional blocks by guaranteeing that expensive functions are not called unnecessarily.

When To Not Short-Circuit

Depending on an individual's programming style or the problem space in question, there may come a time when side effects in a boolean statement are desired. Let's look at an example of short-circuit evaluation bypassing logic that we actually want to execute.

Pretend that there is a super fancy system that requires two keys for an override to be initiated. If a provided key is found to be invalid, the override fails and the attempt is recorded with the key details. The latter is the side effect that we want.

Short-circuit evaluation would correctly ensure that the override does not occur if either of the keys is invalid, but it would only log the details of the first key if both are invalid. This is more annoying than problematic as the logged invalid key would be replaced and the override attempted again, only to still fail when the second key is finally evaluated.

Non-short-circuit evaluation would save time by ensuring that both invalid keys are logged for replacement.

1data class Key(private val id: String) {
2 fun isValid(): Boolean {
3 return id.startsWith("A").also { valid ->
4 if (!valid) logInvalidKey(this)
5 }
6 }
7}
8
9fun logInvalidKey(key: Key) {
10 println("Invalid key encountered: $key")
11}
12
13fun enableOverrideSC(key1: Key, key2: Key) {
14 println(if (key1.isValid() && key2.isValid()) {
15 "System override initiated"
16 } else {
17 "Override attempt failed"
18 })
19}
20
21fun enableOverrideNSC(key1: Key, key2: Key) {
22 println(if (key1.isValid() and key2.isValid()) {
23 "System override initiated"
24 } else {
25 "Override attempt failed"
26 })
27}
28
29fun main() {
30 val key1 = Key("Beta")
31 val key2 = Key("Gamma")
32 println("Using short-circuit eval...")
33 enableOverrideSC(key1, key2)
34 println()
35 println("Using non-short-circuit eval...")
36 enableOverrideNSC(key1, key2)
37}
kt
Using short-circuit eval...
Invalid key encountered: Key(id=Beta)
Override attempt failed
Using non-short-circuit eval...
Invalid key encountered: Key(id=Beta)
Invalid key encountered: Key(id=Gamma)
Override attempt failed

A different programming style could of course replace this, for example, by forcing both keys to be checked before entering the conditional block:

1fun enableOverride(key1: Key, key2: Key) {
2 val (key1V, key2V) = key1.isValid() to key2.isValid()
3 println(if (key1V && key2V) {
4 "System override engaged"
5 } else {
6 "Override attempt failed"
7 })
8}
kt

Summary

As a recap, here's a table detailing the operators used for bitwise operations and for both non-short-circuit and short-circuit (SC) evaluation:

JAVAKOTLINPYTHON
Bitwise AND1 & 101 and 101 & 10
Logical ANDfalse & truefalse and trueFalse & True
Logical AND (SC)false && truefalse && trueFalse and True
Bitwise OR1 | 101 or 101 | 10
Logical ORtrue | falsetrue or falseTrue | False
Logical OR (SC)true || falsetrue || falseTrue or False

I threw Python into the mix for fun 🤗 🐍 and to emphasize the pattern to remember:

When you want to perform a logical operation without short-circuit evaluation, use the operator that you would normally use for a corresponding bitwise operation. Otherwise, your short-circuit logical operations will use the other set of operators that you most likely use the majority of the time anyway.


Thanks for reading!🏝️

|© 2023 bog-walk