Polyglot
Languages Kotlin operators
Kotlin § operators

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:

FormDescription
a..binclusive: a to b
a until b / a..<bexclusive: a to b-1
a downTo bdescending: a to b
range step nwith 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:

OperatorFunction
+plus
-minus
*times
/div
%rem
..rangeTo
..<rangeUntil
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
--a, a--dec
==, !=equals (only equals)
>, <, >=, <=compareTo
+=plusAssign
a[i]get
a[i] = vset
a(), a(args)invoke
in, !incontains

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? over as for casts that may fail.
  • Use .., until, downTo, step for ranges.
  • Use in/!in for membership tests.
  • Use is/!is for type checks (admits smart cast).
  • Use let with 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.