Operators
Kotlin’s operator surface is substantially conventional — arithmetic, comparison, logical, bitwise — with several distinctive additions: the Elvis operator (?:) for nil-coalescing, the safe call (?.) for optional chaining, the not-null assertion (!!) for force unwrap, the range operators (.., until, ..<, downTo, step), and operator overloading via the operator modifier on functions. The equality distinction (== for value, === for identity) admits substantial Java-style reference comparison. Kotlin admits infix function calls — methods declared infix admit a infix b syntax. The combination — conventional operators, the optional-handling operators, the substantial range surface, the operator overloading — is the substance of Kotlin’s expression surface.
Arithmetic
a + b // addition
a - b // subtraction
a * b // multiplication
a / b // division (integer for Int, float for Double)
a % b // remainder
-a // unary negation
+a // unary plus
Integer division truncates toward zero:
println(7 / 2) // 3
println(-7 / 2) // -3
println(7 % 2) // 1
There is no ** (power) operator in Kotlin; use Math.pow() for floats:
import kotlin.math.pow
2.0.pow(10.0) // 1024.0
2.0.pow(10) // 1024.0 (Int admitted)
For integer power, the conventional uses are Math.pow().toInt() or recursive multiplication.
The arithmetic operators are methods — admit overloading via operator fun:
data class Point(val x: Double, val y: Double) {
operator fun plus(other: Point) = Point(x + other.x, y + other.y)
}
val p = Point(1.0, 2.0) + Point(3.0, 4.0) // Point(4.0, 6.0)
Compound assignment
a += b // a = a + b
a -= b
a *= b
a /= b
a %= b
The operators may be overloaded via operator fun plusAssign, etc., admitting substantial in-place mutation patterns.
Increment and decrement
var x = 5
x++ // increment (returns old value)
++x // increment (returns new value)
x--
--x
Custom increment via operator fun inc() and operator fun dec().
Comparison
a == b // equality (calls equals())
a != b
a < b // calls compareTo()
a > b
a <= b
a >= b
The == calls the overloadable equals(); for reference identity, ===:
val a = String("hello".toCharArray()) // explicit construction
val b = String("hello".toCharArray())
a == b // true (value equality)
a === b // false (different objects)
val c = a
a === c // true (same object)
The == is the conventional equality; === is rare in idiomatic Kotlin.
For ordering, the operators delegate to compareTo():
class Distance(val meters: Int) : Comparable<Distance> {
override fun compareTo(other: Distance) = meters.compareTo(other.meters)
}
Distance(100) < Distance(200) // true
Logical operators
a && b // AND (short-circuit)
a || b // OR (short-circuit)
!a // NOT
The operands must be Boolean; Kotlin admits no truthiness coercion:
val n = 5
if (n) { } // ERROR
if (n != 0) { } // OK
The strictness eliminates the C-family truthiness pitfalls.
The non-short-circuit forms (rarely needed):
a and b // bitwise AND on Boolean = logical AND, no short-circuit
a or b
a xor b
Bitwise operators
Kotlin uses named functions for bitwise operations on integers (no symbolic operators):
val a = 0b1100
val b = 0b1010
a and b // 0b1000 (bitwise AND)
a or b // 0b1110 (bitwise OR)
a xor b // 0b0110 (bitwise XOR)
a.inv() // bitwise NOT
a shl 2 // left shift
a shr 1 // right shift (arithmetic)
a ushr 1 // right shift (logical)
The forms admit substantial readability over symbolic alternatives.
Range operators
The .. and ..< admit ranges:
1..10 // IntRange (1, 2, ..., 10)
1 until 10 // IntRange (1, 2, ..., 9)
1..<10 // IntRange (1, 2, ..., 9) (since 1.9)
10 downTo 1 // IntProgression (10, 9, ..., 1)
1..10 step 2 // IntProgression (1, 3, 5, 7, 9)
10 downTo 1 step 3 // (10, 7, 4, 1)
'a'..'z' // CharRange
"abc".."xyz" // ClosedRange<String>; admits in/contains
The principal forms:
| Form | Description |
|---|---|
a..b | inclusive: a to b |
a until b / a..<b | exclusive: a to b-1 |
a downTo b | descending: a to b |
range step n | with step |
in and !in
The in admits range and collection membership tests:
val n = 5
n in 1..10 // true
n !in 1..10 // false
n in listOf(1, 5, 10) // true
// In when:
when (n) {
in 1..10 -> "small"
in 11..100 -> "medium"
!in 1..1000 -> "out of range"
else -> "large"
}
// In for loops:
for (i in 1..10) { /* ... */ }
for (c in 'a'..'z') { /* ... */ }
The in operator admits substantial conciseness in conditionals and iteration.
Safe call ?.
The safe call admits “access this if non-null”:
val name: String? = "Alice"
val length = name?.length // Int?
val upper = name?.uppercase() // String?
val user: User? = getCurrentUser()
val city = user?.address?.city // String?
// With method calls and properties:
val result = list?.first()?.uppercase()
val item = arr?.get(0)
The expression returns null if any link in the chain is null.
Elvis operator ?:
The ?: admits “use this value, or the right operand if null”:
val name: String? = null
val display = name ?: "anonymous" // "anonymous"
val port = config.port ?: 8080
val timeout = options.timeout ?: defaultTimeout
// With throw:
val name = nullable ?: throw IllegalStateException("name is required")
// With return:
fun process(input: String?) {
val nonNull = input ?: return
// nonNull is String (non-null)
}
The Elvis admits substantial conciseness for default-or-throw/default-or-return patterns.
Not-null assertion !!
The !! extracts the value or throws NullPointerException:
val name: String? = "Alice"
val length = name!!.length // 5
val nilName: String? = null
val oops = nilName!!.length // NPE: null
The conventional discipline avoids !! — ?., ?:, let, and smart casts are conventionally safer.
let, also, apply, run, with
Kotlin’s scoped functions admit substantial fluent patterns:
val length = nullable?.let { it.length } // operate on non-null
val updated = config.also { println("modified $it") }
val builder = StringBuilder().apply {
append("Hello")
append(", ")
append("World")
}
val result = with(builder) {
append("!")
toString()
}
Treated in Standard library.
Range and is in when
The is/!is admit type checks (with smart cast):
val any: Any = "hello"
if (any is String) {
println(any.length) // smart cast to String
}
if (any !is Int) {
println("not an int")
}
// In when:
when (value) {
is Int -> println("int: $value")
is String -> println("string of length ${value.length}")
is List<*> -> println("list")
else -> println("other")
}
The smart cast admits using the value as the target type within the conditional branch.
as and as?
val any: Any = "hello"
val s: String = any as String // throws ClassCastException on failure
val s: String? = any as? String // returns null on failure
The as? admits safe casting; the as is throwing.
Operator overloading
Kotlin admits overloading via operator fun:
data class Vec2(val x: Double, val y: Double) {
operator fun plus(other: Vec2) = Vec2(x + other.x, y + other.y)
operator fun minus(other: Vec2) = Vec2(x - other.x, y - other.y)
operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar)
operator fun unaryMinus() = Vec2(-x, -y)
operator fun get(index: Int): Double = when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException()
}
}
val a = Vec2(1.0, 2.0)
val b = Vec2(3.0, 4.0)
val c = a + b // Vec2(4.0, 6.0)
val d = -a // Vec2(-1.0, -2.0)
val first = a[0] // 1.0
The principal overloadable operators:
| Operator | Function |
|---|---|
+ | plus |
- | minus |
* | times |
/ | div |
% | rem |
.. | rangeTo |
..< | rangeUntil |
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
==, != | equals (only equals) |
>, <, >=, <= | compareTo |
+= | plusAssign |
a[i] | get |
a[i] = v | set |
a(), a(args) | invoke |
in, !in | contains |
Infix functions
Functions marked infix admit a name b syntax:
infix fun Int.add(other: Int): Int = this + other
5 add 3 // 8 (infix call)
5.add(3) // also admitted
// Examples in stdlib:
val pair = "key" to "value" // Pair("key", "value") via infix to
val list = (1 until 10).toList() // until is infix
The conventional uses are concise expressions (to, until, step).
Common patterns
Default with Elvis
val port = config.port ?: 8080
val name = user?.name ?: "anonymous"
val timeout = options.timeout ?: defaults.timeout
Validation with throw
val n = parseInt(input) ?: throw IllegalArgumentException("invalid: $input")
val user = findUser(id) ?: throw NoSuchElementException("user $id")
Early return
fun process(input: String?): String {
val nonNull = input ?: return "default"
return nonNull.uppercase()
}
Chained safe calls
val city = user?.profile?.address?.city ?: "unknown"
val first = list?.firstOrNull()?.uppercase()
Range iteration
for (i in 1..10) print(i) // 1 to 10
for (i in 1..<10) print(i) // 1 to 9
for (i in 10 downTo 1) print(i) // 10 to 1
for (i in 1..100 step 5) print(i) // 1, 6, 11, ..., 96
for (c in 'a'..'z') print(c) // a to z
// Reverse range:
for (i in (1..10).reversed()) print(i)
Range membership in when
val score = 85
val grade = when (score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
else -> "F"
}
Operator overloading for fluent API
data class Money(val amount: BigDecimal, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "currency mismatch" }
return Money(amount + other.amount, currency)
}
}
val total = Money(100.toBigDecimal(), "USD") + Money(50.toBigDecimal(), "USD")
Infix for DSL
infix fun <T> T.shouldBe(expected: T) {
require(this == expected) { "expected $expected, got $this" }
}
5 shouldBe 5 // OK
"hello" shouldBe "hello"
The pattern is conventional in test DSLs (Kotest, etc.).
Smart cast with is
fun describe(value: Any): String = when (value) {
is Int -> "int: $value (${value.toString(2)})" // smart cast to Int
is String -> "string of length ${value.length}" // smart cast to String
is List<*> -> "list of ${value.size}"
null -> "null"
else -> "other: $value"
}
Operator overloading for indexable
class Matrix(val rows: Int, val cols: Int) {
private val data = Array(rows) { DoubleArray(cols) }
operator fun get(i: Int, j: Int): Double = data[i][j]
operator fun set(i: Int, j: Int, value: Double) { data[i][j] = value }
}
val m = Matrix(3, 3)
m[0, 0] = 1.0
val v = m[0, 0]
Invoke for callable objects
class Logger(val prefix: String) {
operator fun invoke(message: String) {
println("[$prefix] $message")
}
}
val log = Logger("APP")
log("started") // calls invoke
log("running")
A note on the conventional discipline
The contemporary Kotlin operator advice:
- Use
==for value equality;===rarely (identity). - Use
?:for default values. - Use
?.for optional chaining. - Use
!!sparingly — only when null is impossible. - Use
as?overasfor casts that may fail. - Use
..,until,downTo,stepfor ranges. - Use
in/!infor membership tests. - Use
is/!isfor type checks (admits smart cast). - Use
letwith safe call for null-safe operations. - Use named bitwise operators (
and,or,xor,shl,shr). - Use operator overloading sparingly — only for substantially natural mathematical operations.
- Use infix sparingly — for substantially natural DSL syntax.
The combination — conventional arithmetic and comparison, the optional-handling operators (?., ?:, !!), the substantial range surface, the type-check is/!is with smart casts, the operator overloading mechanism, the named bitwise operators, the infix modifier — is the substance of Kotlin’s expression surface. The discipline produces concise, type-safe expressions with substantial protection against the conventional null-pointer pitfalls.