In Kotlin, we can add an element to a list using the + 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
}
Using operators between objects is possible thanks to the Kotlin feature called operator overloading, which allows special kinds of methods to be defined that can be used as operators. Let's see this in a custom class example.
An example of operator overloading
Let's say that you need to represent complex numbers in your application. These are special kinds of numbers in mathematics that are represented by two parts: real and imaginary. Complex numbers are useful for a variety of kinds of calculations in physics and engineering.
data class Complex(val real: Double, val imaginary: Double)
In mathematics, there is a range of operations that we can do on complex numbers. For instance, you can add two complex numbers or subtract a complex number from another complex number. This is done using the + 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)
}
Using the + 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)
Kotlin defines a concrete set of operators, for each of which there is a specific name and a number of supported arguments. Additionally, all operators need to be a method (so, either a member function or an extension function), and these methods need the operator modifier.
Well-used operators can help us improve our code readability as much as poorly used operators can harm it[^18_1]. Let's discuss all the Kotlin operators.
Arithmetic operators
Let's start with arithmetic operators, like plus or times. These are easiest for the Kotlin compiler because it just needs to transform the left column to the right.
| Expression | Translates to |
|------------|-------------------|
| 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) |
Notice that % 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].
It is also worth mentioning .. 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
}
The in operator
One of my favorite operators is in. The expression a in b translates to b.contains(a). There is also !in, which translates to negation.
| Expression | Translates to |
|------------|------------------|
| a in b | b.contains(a) |
| a !in b | !b.contains(a) |
There are a few ways to use this operator. Firstly, for collections, instead of checking if a list contains an element, you can check if the element is in the list.
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
}
Why would you do that? Primarily for readability. Would you ask "Does the fridge contain a beer?" or "Is there a beer in the fridge?"? Using the in operator gives us the possibility to choose.
We also often use the 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
}
You can make a range from any objects that are comparable, and the result 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
}
The iterator operator
You can use for-loop to iterate over any object that has an 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>
}
You can define objects that can be iterated over, but do not implement 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
To better understand how a for-loop works, consider the code below.
fun main() {
for (e in Tree()) {
// body
}
}
class Tree {
operator fun iterator(): Iterator<String> = ...
}
Under the hood, a for-loop is compiled into bytecode that uses a while-loop to iterate over the object's iterator, as presented in the snippet below.
fun main() {
val iterator = Tree().iterator()
while (iterator.hasNext()) {
val e = iterator.next()
// body
}
}
The equality and inequality operators
In Kotlin, there are two types of equality:
Structural equality - checked with the equals method or the == operator (and its negated counterpart !=). a == b translates to a.equals(b) when a is not nullable, otherwise it translates to a?.equals(b) ?: (b === null). Structural equality is generally preferred over referential equality. The equals method can be overridden in custom class.
Referential equality - checked with the === operator (and its negated counterpart !==); returns true when both sides point to the same object. === and !== (identity checks) are not overloadable.
Since equals is implemented in Any, which is the superclass of every class, we can check the equality of any two objects.
| Expression | Translates to |
|------------|-----------------------------------|
| a == b | a?.equals(b) ?: (b === null) |
| a != b | !(a?.equals(b) ?: (b === null)) |
Comparison operators
Some classes have natural order, which is the order that is used by default when we compare two instances of a given class. Numbers are a good example: 10 is a smaller number than 100. There is a popular Java convention that classes with natural order should implement a 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
}
As a result, there is a convention that we should compare two objects using the 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 <=.
| Expression | Translates to |
|------------|-----------------------|
| a > b | a.compareTo(b) > 0 |
| a < b | a.compareTo(b) < 0 |
| a >= b | a.compareTo(b) >= 0 |
| a <= b | a.compareTo(b) <= 0 |
I often use comparison operators to compare amounts kept in objects of type BigDecimal or BigInteger.
I also like to compare time references the same way.
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
}
The indexed access operator
In programming, there are two popular conventions for getting or setting elements in collections. The first uses box brackets, while the second uses the 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.
| Expression | Translates to |
|------------------------|---------------------------|
| 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
}
Square brackets are translated to 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.
Augmented assignments
When we set a new value for a variable, this new value is often based on its previous value. For instance, we might want to add a value to the previous one. For this, augmented assignments were introduced[^18_3]. For example, a += b is a shorter notation of a = a + b. There are similar notations for other arithmetic operations.
| Expression | Translates to |
|------------|---------------|
| 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 |
Notice that augmented assignments can be used for all types that support the appropriate arithmetic operation, including lists or strings. Such augmented assignments need a variable to be read-write, namely 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]
}
Augmented assignments can be used in another way: to modify a mutable object. For instance, we can use += to add an element to a mutable list. In such a case, a += b translates to a.plusAssign(b).
| Expression | Translates to |
|------------|--------------------|
| 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}
}
If both kinds of augmented assignment can be applied, Kotlin chooses to modify a mutable object by default.
Unary prefix operators
A plus, minus, or negation in front of a single value is also an operator. Operators that are used with only a single value are called unary operators[^18_4]. Kotlin supports operator overloading for the following unary operators:
Here is an example of overloading the 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)
}
The unaryPlus operator is often used as part of Kotlin DSLs, which are described in detail in the next book of this series, Functional Kotlin.
Increment and decrement
As part of many algorithms used in older languages, we often needed to add or subtract the value 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.
Both increment and decrement can be used before or after a variable, and this determines the value returned by this operation.
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
}
Based on the 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.
| Expression | Translates to (simplified) |
|------------|---------------------------------|
| ++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 |
The invoke operator
Objects with the 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.
The 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
}
Precedence
What is the result of the expression 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.
Precedence is also extremely important in programming because when the compiler evaluates an expression such as 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.
On the basis of this table, can you predict what the following code will print?
fun main() {
println(-1.plus(1))
}
This is a popular Kotlin puzzle. The answer is -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
}
Summary
We use a lot of operators in Kotlin, many of which can be overloaded. This can be used to improve our code’s readability. From the cognitive standpoint, using an intuitive operator can be a huge improvement over using methods everywhere. Therefore, iit’s good to know what options are available and to be open to using operators defined by Kotlin stdlib, but it’s also good to be able to define our own operators.
[^18_0]: This operator was previously called 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.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.
Software architect with 15 years of experience, currently working on building infrastructure for AI. I think Kotlin is one of the best programming languages ever created.
Emanuele is passionate about Android and has been fascinated by it since 2010: the more he learns, the more he wishes to share what he knows with others, which is why he started maintaining his own blog.
In his current role as Senior Android Developer at Mozio, he is now focusing on Kotlin Multiplatform Mobile: he has already given a couple of talks on this topic on various occasions, so far.
Interested in everything Android and Kotlin related, including architecture patterns, TDD, functional programming and Jetpack Compose. Author of several articles about Android and Kotlin Coroutines.