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:
- Can we then assume that, if
a
andb
areBoolean
values,a && b
would translate toa.and(b)
? - Can
a && b
be used interchangeably witha 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. Boththis
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 b4 val bool3 = a && b5 println("$bool1 $bool2 $bool3")6}
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}
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), // 0b00013 WIND(1 shl 1), // 0b00104 FIRE(1 shl 2), // 0b01005 WATER(1 shl 3) // 0b10006}78fun main() {9 var stone = 010 for (element in Element.values()) {11 // add a flag12 stone = stone or element.flag13 }1415 // check for a flag16 println("Stone contains fire: ${stone and Element.FIRE.flag != 0}")1718 // remove 1 flag19 stone = stone and Element.FIRE.flag.inv()20 println("Stone contains fire: ${stone and Element.FIRE.flag != 0}")2122 // remove 2 flags23 stone = stone and (Element.WIND.flag or Element.EARTH.flag).inv()2425 print("Stone does not contain:")26 for (element in Element.values()) {27 // check for absence of flag28 if (stone and element.flag == 0) {29 print(" ${element.name}")30 }31 }32 println()33}
Stone contains fire: trueStone contains fire: falseStone 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 = 102val ans1 = (a > 10) && (a % 2 == 0)3val ans2 = (a > 10) and (a % 2 == 0)4println(ans1) // false5println(ans2) // false
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}45fun Int.isDangerouslyDivisibleBy(d: Int): Boolean {6 return (d != 0) and (1.0 * this / d == 1.0 * (this / d))7}89fun main() {10 println(10.isSafelyDivisibleBy(0)) // false11 println(10.isDangerouslyDivisibleBy(0)) // throws ArithmeticException12}
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 true5}67fun main() {8 val primes = mutableListOf<Long>()9 // ✂️ Generate the first 1000 primes and store in list10 val num = 2801L // already stored in list11 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}
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}89fun logInvalidKey(key: Key) {10 println("Invalid key encountered: $key")11}1213fun enableOverrideSC(key1: Key, key2: Key) {14 println(if (key1.isValid() && key2.isValid()) {15 "System override initiated"16 } else {17 "Override attempt failed"18 })19}2021fun 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}2829fun 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}
Using short-circuit eval...Invalid key encountered: Key(id=Beta)Override attempt failedUsing 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}
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:
JAVA | KOTLIN | PYTHON | |
---|---|---|---|
Bitwise AND | 1 & 10 | 1 and 10 | 1 & 10 |
Logical AND | false & true | false and true | False & True |
Logical AND (SC) | false && true | false && true | False and True |
Bitwise OR | 1 | 10 | 1 or 10 | 1 | 10 |
Logical OR | true | false | true or false | True | False |
Logical OR (SC) | true || false | true || false | True 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!☕🏝️