
This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.
+ operator. In the same way, we can add two strings together. We can check if a collection contains an element using the in operator. We can also add, subtract or multiply elements of type BigDecimal, which is a JVM class that is used to represent possibly big numbers with unlimited precision.import java.math.BigDecimal fun main() { val list: List<String> = listOf("A", "B") val newList: List<String> = list + "C" println(newList) // [A, B, C] val str1: String = "AB" val str2: String = "CD" val str3: String = str1 + str2 println(str3) // ABCD println("A" in list) // true println("C" in list) // false val money1: BigDecimal = BigDecimal("12.50") val money2: BigDecimal = BigDecimal("3.50") val money3: BigDecimal = money1 * money2 println(money3) // 43.7500 }
data class Complex(val real: Double, val imaginary: Double)
+ and - operators. Therefore, it is reasonable that we should support these operators for our Complex class. To support the + operator, we need to define a method that has an operator modifier that is called plus and a single parameter. To support the - operator, we need to define a method that has an operator modifier called minus and a single parameter.data class Complex(val real: Double, val imaginary: Double) { operator fun plus(another: Complex) = Complex( real + another.real, imaginary + another.imaginary ) operator fun minus(another: Complex) = Complex( real = real - another.real, imaginary = imaginary - another.imaginary ) } // example usage fun main() { val c1 = Complex(1.0, 2.0) val c2 = Complex(2.0, 3.0) println(c1 + c2) // Complex(real=3.0, imaginary=5.0) println(c2 - c1) // Complex(real=1.0, imaginary=1.0) }
+ and - operators is equivalent to calling the plus and minus functions. These two can be used interchangeably.c1 + c2 // under the hood is c1.plus(c2) c1 - c2 // under the hood is c1.minus(c2)
operator modifier.|------------|-------------------|
|
a + b | a.plus(b) ||
a - b | a.minus(b) ||
a * b | a.times(b) ||
a / b | a.div(b) ||
a % b | a.rem(b) ||
a..b | a.rangeTo(b) ||
a..<b | a.rangeUntil(b) |% translates to rem, which is a short form of "remainder". This operator returns the remainder left over when one operand is divided by a second operand, so it is similar to the modulo operation[^18_0].fun main() { println(13 % 4) // 1 println(7 % 4) // 3 println(1 % 4) // 1 println(0 % 4) // 0 println(-1 % 4) // -1 println(-5 % 4) // -1 println(-7 % 4) // -3 }
.. and ..< operators, that are used to create ranges. We can use them between integers to create IntRange, over which we can iterate in for-loop. We can also use those operators between any values that implement Comparable interface, to define a range by extremes of this range.fun main() { val intRange: IntRange = 1..10 val comparableRange: ClosedRange<String> = "A".."Z" val openEndRange: OpenEndRange<Double> = 1.0..<2.0 }
in. The expression a in b translates to b.contains(a). There is also !in, which translates to negation.|------------|------------------|
|
a in b | b.contains(a) ||
a !in b | !b.contains(a) |fun main() { val letters = setOf("A", "B", "C") println("A" in letters) // true println("D" in letters) // false println(letters.contains("A")) // true println(letters.contains("D")) // false }
in operator gives us the possibility to choose.in operator together with ranges. The expression 1..10 produces an object of type IntRange, which has a contains method. This is why you can use in and a range to check if a number is in this range.fun main() { println(5 in 1..10) // true println(11 in 1..10) // false }
ClosedRange also has a contains method. This is why you can use a range check for any objects that are comparable, such as big numbers or objects representing time.import java.math.BigDecimal import java.time.LocalDateTime fun main() { val amount = BigDecimal("42.80") val minPrice = BigDecimal("5.00") val maxPrice = BigDecimal("100.00") val correctPrice = amount in minPrice..maxPrice println(correctPrice) // true val now = LocalDateTime.now() val actionStarts = LocalDateTime.of(1410, 7, 15, 0, 0) val actionEnds = actionStarts.plusDays(1) println(now in actionStarts..actionEnds) // false }
iterator operator method. Every object that implements an Iterable interface must support the iterator method.public interface Iterable<out T> { /** * Returns an iterator over the elements of this object. */ public operator fun iterator(): Iterator<T> }
Iterable interface. Map is a great example. It does not implement the Iterable interface, yet you can iterate over it using a for-loop. How so? It is thanks to the iterator operator, which is defined as an extension function in Kotlin stdlib.// Part of Kotlin standard library inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator() fun main() { val map = mapOf('a' to "Alex", 'b' to "Bob") for ((letter, name) in map) { println("$letter like in $name") } } // a like in Alex // b like in Bob
fun main() { for (e in Tree()) { // body } } class Tree { operator fun iterator(): Iterator<String> = ... }
fun main() { val iterator = Tree().iterator() while (iterator.hasNext()) { val e = iterator.next() // body } }
-
Structural equality - checked with the
equalsmethod or the==operator (and its negated counterpart!=).a == btranslates toa.equals(b)whenais not nullable, otherwise it translates toa?.equals(b) ?: (b === null). Structural equality is generally preferred over referential equality. Theequalsmethod can be overridden in custom class. -
Referential equality - checked with the
===operator (and its negated counterpart!==); returnstruewhen both sides point to the same object.===and!==(identity checks) are not overloadable.
equals is implemented in Any, which is the superclass of every class, we can check the equality of any two objects.|------------|-----------------------------------|
|
a == b | a?.equals(b) ?: (b === null) ||
a != b | !(a?.equals(b) ?: (b === null)) |Comparable interface, which requires the compareTo method, which is used to compare two objects.public interface Comparable<in T> { /** * Compares this object with the specified object for * order. Returns zero if this object is equal to the * specified [other] object, a negative number if it's * less than [other], or a positive number if it's * greater than [other]. */ public operator fun compareTo(other: T): Int }
compareTo method. However, using the compareTo method directly is not very intuitive. Let's say that you see a.compareTo(b) > 0 in code. What does it mean? Kotlin simplifies this by making compareTo an operator that can be replaced with intuitive mathematical comparison operators: >, <, >=, and <=.|------------|-----------------------|
|
a > b | a.compareTo(b) > 0 ||
a < b | a.compareTo(b) < 0 ||
a >= b | a.compareTo(b) >= 0 ||
a <= b | a.compareTo(b) <= 0 |BigDecimal or BigInteger.import java.math.BigDecimal fun main() { val amount1 = BigDecimal("42.80") val amount2 = BigDecimal("5.00") println(amount1 > amount2) // true println(amount1 >= amount2) // true println(amount1 < amount2) // false println(amount1 <= amount2) // false println(amount1 > amount1) // false println(amount1 >= amount1) // true println(amount1 < amount2) // false println(amount1 <= amount2) // false }
import java.time.LocalDateTime fun main() { val now = LocalDateTime.now() val actionStarts = LocalDateTime.of(2010, 10, 20, 0, 0) val actionEnds = actionStarts.plusDays(1) println(now > actionStarts) // true println(now <= actionStarts) // false println(now < actionEnds) // false println(now >= actionEnds) // true }
get and set methods. In Java, we use the first convention for arrays and the second one for other kinds of collections. In Kotlin, both conventions can be used interchangeably because the get and set methods are operators that can be used with box brackets.|------------------------|---------------------------|
|
a[i] | a.get(i) ||
a[i, j] | a.get(i, j) ||
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) ||
a[i] = b | a.set(i, b) ||
a[i, j] = b | a.set(i, j, b) ||
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |fun main() { val mutableList = mutableListOf("A", "B", "C") println(mutableList[1]) // B mutableList[2] = "D" println(mutableList) // [A, B, D] val animalFood = mutableMapOf( "Dog" to "Meat", "Goat" to "Grass" ) println(animalFood["Dog"]) // Meat animalFood["Cat"] = "Meat" println(animalFood["Cat"]) // Meat }
get and set calls with appropriate numbers of arguments. Variants of get and set functions with more arguments might be used by data processing libraries. For instance, you could have an object that represents a table and use box brackets with two arguments: x and y coordinates.a += b is a shorter notation of a = a + b. There are similar notations for other arithmetic operations.|------------|---------------|
|
a += b | a = a + b ||
a -= b | a = a - b ||
a *= b | a = a * b ||
a /= b | a = a / b ||
a %= b | a = a % b |var, and the result of the mathematical operation must have a proper type (to translate a += b to a = a + b, the variable a needs to be var, and a + b needs to be a subtype of type a).fun main() { var str = "ABC" str += "D" // translates to str = str + "D" println(str) // ABCD var l = listOf("A", "B", "C") l += "D" // translates to l = l + "D" println(l) // [A, B, C, D] }
+= to add an element to a mutable list. In such a case, a += b translates to a.plusAssign(b).|------------|--------------------|
|
a += b | a.plusAssign(b) ||
a -= b | a.minusAssign(b) ||
a *= b | a.timesAssign(b) ||
a /= b | a.divAssign(b) ||
a %= b | a.remAssign(b) |fun main() { val names = mutableListOf("Jake", "Ben") names += "Jon" names -= "Ben" println(names) // [Jake, Jon] val tools = mutableMapOf( "Grass" to "Lawnmower", "Nail" to "Hammer" ) tools += "Screw" to "Screwdriver" tools -= "Grass" println(tools) // {Nail=Hammer, Screw=Screwdriver} }
|------------|------------------|
|
+a | a.unaryPlus() ||
-a | a.unaryMinus() ||
!a | a.not() |unaryMinus operator.data class Point(val x: Int, val y: Int) operator fun Point.unaryMinus() = Point(-x, -y) fun main() { val point = Point(10, 20) println(-point) // Point(x=-10, y=-20) }
unaryPlus operator is often used as part of Kotlin DSLs, which are described in detail in the next book of this series, Functional Kotlin.1 from a variable, which is why increment and decrement were invented. The ++ operator is used to add 1 to a variable; so, if a is an integer, then a++ translates to a = a + 1. The -- operator is used to subtract 1 from a variable; so, if a is an integer, then a-- translates to a = a - 1.- If you use
++before a variable, it is called pre-increment; it increments the variable and then returns the result of this operation. - If you use
++after a variable, it is called post-increment; it increments the variable but then returns the value before the operation. - If you use
--before a variable, it is called pre-decrement; it decrements the variable and then returns the result of this operation. - If you use
--after a variable, it is called post-decrement; it decrements the variable but then returns the value before the operation.
fun main() { var i = 10 println(i++) // 10 println(i) // 11 println(++i) // 12 println(i) // 12 i = 10 println(i--) // 10 println(i) // 9 println(--i) // 8 println(i) // 8 }
inc and dec methods, Kotlin supports increment and decrement overloading, which should increment or decrement a custom object. I have never seen this capability used in practice, so I think it is enough to know that it exists.|------------|---------------------------------|
|
++a | a = a.inc(); a ||
a++ | val tmp = a; a = a.inc(); tmp ||
--a | a = a.dec(); a ||
a-- | val tmp = a; a = a.dec(); tmp |invoke operator can be called like functions, so with parentheses straight after the variable representing this object. Calling an object translates to the invoke method call with the same arguments.|--------------------|---------------------------|
|
a() | a.invoke() ||
a(i) | a.invoke(i) ||
a(i, j) | a.invoke(i, j) ||
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |invoke operator is used for objects that represent functions, such as lambda expressions[^18_2] or UseCases objects from Clean Architecture.class CheerUseCase { operator fun invoke(who: String) { println("Hello, $who") } } fun main() { val hello = { println("Hello") } hello() // Hello val cheerUseCase = CheerUseCase() cheerUseCase("Reader") // Hello, Reader }
1 + 2 * 3? The answer is 7, not 9, because in mathematics we multiply before adding. We say that multiplication has higher precedence than addition.1 + 2 == 3, it needs to know if it should first add 1 to 2, or compare 2 and 3. The following table compares the precedence of all the operators, including those that can be overloaded and those that cannot.|------------|-----------------|------------------------------------------|
| Highest | Postfix | ++, --, . (regular call), ?. (safe call) |
| | Prefix | -, +, ++, --, ! |
| | Type casting | as, as? |
| | Multiplicative | *, /, % |
| | Additive | +, - |
| | Range | .. |
| | Infix function | simpleIdentifier |
| | Elvis | ?: |
| | Named checks | in, !in, is, !is |
| | Comparison | <, >, <=, >= |
| | Equality | ==, !=, ===, !== |
| | Conjunction | && |
| | Disjunction | \ || |
| | Spread operator | * |
| Lowest | Assignment | =, +=, -=, *=, /=, %= |
fun main() { println(-1.plus(1)) }
-2, not 0, because a single minus in front of a function is an operator whose precedence is lower than an explicit plus method call. So, we first call the method and then call unaryMinus on the result, therefore we change from 2 to -2. To use -1 literally, wrap it with parentheses.fun main() { println((-1).plus(1)) // 0 }
mod, which comes from "modulo", but this name is now deprecated. In mathematics, both the remainder and the modulo operations act the same for positive numbers, but the difference lies in negative numbers. The result of -5 remainder 4 is -1, because -5 = 4 * (-1) + (-1). The result of -5 modulo 4 is 3, because -5 = 4 * (-2) + 3. Kotlin’s % operator implements the behavior of remainder, which is why its name needed to be changed from mod to rem.[^18_1]: You can find more about this in Effective Kotlin, Item 11: An operator’s meaning should be consistent with its function name and Item 12: Use operators to increase readability.
[^18_2]: There will be more about lambda expressions in the next book of the series, Functional Kotlin.
[^18_3]: I am not sure which language introduced augmented assignments first, but they are even supported by languages as old as C.
[^18_4]: Unary operators are used with only a single value (operand). Operators used with two values are known as binary operators; however, since most operators are binary, this type is often treated as the default. Operators used with three values are known as ternary operators. Since there is only one ternary operator in mainstream programming languages, namely the conditional operator, it is often referred as the ternary operator.
[^18_5]: Experimental support for
..< operator was first introduced in Kotlin 1.7.20, but this feature needed to wait until version 1.9 before it became stable.