# Operator Overloading

In the context of computer programming, overloading means “adding extra meaning to something that already exists.”

Operator overloading allows you to take an operator like + and give it meaning for your new type, or extra meaning for an existing type.

Operator overloading has a tumultuous past. It was popularized in C++, but because C++ had no garbage collection, writing overloaded operators was difficult. As a result, the early Java designers deemed operator overloading “bad” and didn’t allow it in Java, even though Java’s garbage collection would have made it relatively easy. The simplicity of operator overloading when supported by garbage collection was demonstrated in the Python language, which constrained you to a limited (familiar) set of operators, as did C++. Scala then experimented with allowing you to invent your own operators, causing some programmers to abuse this feature and create incomprehensible code. Kotlin learned from these languages, and has simplified the process of operator overloading but restricts your choices to a reasonable and familiar set of operators. In addition, the rules of operator precedence cannot be changed.

We’ll create a small class Num and add an overloaded + as an extension function. To overload an operator you use the operator keyword before fun, followed by the special predefined function name for that operator. For example, the special function name for the + operator is plus():

// OperatorOverloading/Num.kt
package operatoroverloading
import atomictest.eq

data class Num(val n: Int)

operator fun Num.plus(rval: Num) =
  Num(n + rval.n)

fun main() {
  Num(4) + Num(5) eq Num(9)
  Num(4).plus(Num(5)) eq Num(9)
}

If you were defining a normal (non-operator) function for use between two operands, you’d use the infix keyword, but operators are already infix. Because plus() is an ordinary function, you can also call it in the conventional way.

When you define an operator as a member function, you can access private elements in a class that an extension function cannot:

// OperatorOverloading/MemberOperator.kt
package operatoroverloading
import atomictest.eq

data class Num2(private val n: Int) {
  operator fun plus(rval: Num2) =
    Num2(n + rval.n)
}

// Cannot access 'n': it is private in 'Num2':
// operator fun Num2.minus(rval: Num2) =
//   Num2(n - rval.n)

fun main() {
  Num2(4) + Num2(5) eq Num2(9)
}

In some contexts it’s helpful to create special meaning for an operator. Here, we model a Molecule with a + that attaches it to another Molecule. The attached property is the link between Molecules:

// OperatorOverloading/Molecule.kt
package operatoroverloading
import atomictest.eq

data class Molecule(
  val id: Int = idCount++,
  var attached: Molecule? = null
) {
  companion object {
    private var idCount = 0
  }
  operator fun plus(other: Molecule) {
    attached = other
  }
}

fun main() {
  val m1 = Molecule()
  val m2 = Molecule()
  m1 + m2                       // [1]
  m1 eq "Molecule(id=0, attached=" +
    "Molecule(id=1, attached=null))"
}
  • [1] Reads like a familiar math expression, but to the person using the model it might be an especially meaningful syntax.

This example is incomplete; if you add the line m2 + m1, then try to display m2, you’ll get a stack overflow (can you fix the problem?).

# Equality

Invoking == (equality) or != (inequality) calls the equals() member function. data classes automatically redefine equals() to compare the stored data, but if you don’t redefine equals() for non-data classes, the default version compares references rather than contents:

// OperatorOverloading/DefaultEquality.kt
package operatoroverloading
import atomictest.eq

class A(val i: Int)
data class D(val i: Int)

fun main() {
  // Normal class:
  val a = A(1)
  val b = A(1)
  val c = a
  (a == b) eq false
  (a == c) eq true
  // Data class:
  val d = D(1)
  val e = D(1)
  (d == e) eq true
}

a and b refer to different objects in memory, so the references are different and a == b is false, even though the two objects store identical data. a and c refer to the same object in memory, so comparing them produces true. Because the data class D automatically generates an equals() that looks at the contents of D, d == e produces true.

equals() is the only operator that cannot be an extension function; it must be overridden as a member function. When defining your own equals(), you are overriding the default equals(other: Any?). Notice that the type of other is Any? rather than the specific type of your class. This allows you to compare your type with other types, which means you must choose the types allowed for comparison:

// OperatorOverloading/DefiningEquality.kt
package operatoroverloading
import atomictest.eq

class E(var v: Int) {
  override fun equals(other: Any?) = when {
    this === other -> true           // [1]
    other !is E -> false             // [2]
    else -> v == other.v             // [3]
  }
  override fun hashCode(): Int = v
  override fun toString() = "E($v)"
}

fun main() {
  val a = E(1)
  val b = E(2)
  (a == b) eq false   // a.equals(b)
  (a != b) eq true    // !a.equals(b)
  // Reference equality:
  (E(1) === E(1)) eq false
}
  • [1] This is an optimization: if other refers to the same object in memory, the result is automatically true. The triple equality symbol === tests for reference equality.
  • [2] This determines that the type of other must be the same as the current type. For E to be compared to other types, add further match expressions.
  • [3] This compares the stored data. At this point the compiler knows that other is of type E, so we can access other.v without a cast.

When overriding equals() you should also override hashCode(). This is a complex topic, but the basic rule is that if two objects are equal, they must produce the same hashCode() value. Standard data structures like Map and Set will fail without this rule. Things get even more complicated with an open class because you must compare an instance with all possible subclasses. You can learn more about the concept of hashing in Wikipedia (opens new window).

Defining a proper equals() and hashCode() is beyond the scope of this book—what we do here illustrates the concept and works for our simple example but won’t work for more complicated cases. This complexity is the reason that data classes create their own equals() and hashCode(). If you must define your own equals() and hashCode(), we recommend automatically generating them using IntelliJ IDEA or Android Studio with the action Generate -> equals and hashCode (opens new window).

When you compare nullable objects using ==, Kotlin enforces null-checking. This can be achieved using either if or the Elvis operator:

// OperatorOverloading/EqualsForNullable.kt
package operatoroverloading
import atomictest.eq

fun equalsWithIf(a: E?, b: E?) =
  if (a === null)
    b === null
  else
    a == b

fun equalsWithElvis(a: E?, b: E?) =
  a?.equals(b) ?: (b === null)

fun main() {
  val x: E? = null
  val y = E(0)
  val z: E? = null
  (x == y) eq false
  (x == z) eq true
  equalsWithIf(x, y) eq false
  equalsWithIf(x, z) eq true
  equalsWithElvis(x, y) eq false
  equalsWithElvis(x, z) eq true
}

equalsWithIf() first checks to see if the reference a is null, in which case the only way the two can be equal is if the reference b is also null. If a is not a null reference, the member equals() is used to compare the two. equalsWithElvis() achieves the same effect, but more succinctly using both ?. and ?:.

# Arithmetic operators

We can define basic arithmetic operators as extensions to class E:

// OperatorOverloading/ArithmeticOperators.kt
package operatoroverloading
import atomictest.eq

// Unary operators:
operator fun E.unaryPlus() = E(v)
operator fun E.unaryMinus() = E(-v)
operator fun E.not() = this

// Increment/decrement:
operator fun E.inc() = E(v + 1)
operator fun E.dec() = E(v - 1)

fun unary(a: E) {
  +a               // unaryPlus()
  -a               // unaryMinus()
  !a               // not()

  var b = a
  b++             // inc() (must be var)
  b--             // dec() (must be var)
}

// Binary operators:
operator fun E.plus(e: E) = E(v + e.v)
operator fun E.minus(e: E) = E(v - e.v)
operator fun E.times(e: E) = E(v * e.v)
operator fun E.div(e: E) = E(v % e.v)
operator fun E.rem(e: E) = E(v / e.v)

fun binary(a: E, b: E) {
  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)
}

// Augmented assignment:
operator fun E.plusAssign(e: E) { v += e.v }
operator fun E.minusAssign(e: E) { v - e.v }
operator fun E.timesAssign(e: E) { v *= e.v }
operator fun E.divAssign(e: E) { v /= e.v }
operator fun E.remAssign(e: E) { v %= e.v }

fun assignment(a: E, b: E) {
  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 a = E(2)
  val b = E(3)
  a + b eq E(5)
  a * b eq E(6)
  val x = E(1)
  x += b * b
  x eq E(10)
}

When writing an extension, remember that the properties and functions of the extended type are implicitly available. In the definition of unaryPlus(), for example, the v in E(v) is the v property from the E that’s being extended.

Note that x += e can be resolved to either x = x.plus(e) if x is a var or to x.plusAssign(e) if x is val and the corresponding plusAssign() member is available. If both options work, the compiler emits an error indicating that it can’t choose.

The parameter can be of a different type than the type the operator extends. Here, the + operator extension for E takes an Int parameter:

// OperatorOverloading/DifferentTypes.kt
package operatoroverloading
import atomictest.eq

operator fun E.plus(i: Int) = E(v + i)

fun main() {
  E(1) + 10 eq E(11)
}

Operator precedence is fixed, and is identical for both built-in types and custom types. For example, multiplication has a higher precedence than addition, and both have higher precedence than equality; thus 1 + 2 * 3 == 7 is true. You can find the operator precedence table in the documentation (opens new window).

Sometimes when you mix arithmetic and programming operators, the result isn’t obvious. Here, we combine + and the Elvis operator:

// OperatorOverloading/ConfusingPrecedence.kt
package operatoroverloading
import atomictest.eq

fun main() {
  val x: Int? = 1
  val y: Int = 2
  val sum = x ?: 0 + y
  sum eq 1
  (x ?: 0) + y eq 3    // [1]
  x ?: (0 + y) eq 1    // [2]
}

In sum, + has higher precedence than the Elvis operator ?: so the result is 1?: (0 + 2) == 1. This might be not what the programmer intended. When mixing different operations where precedence is not obvious, we recommend adding parentheses as in lines [1] and [2].

# Comparison

All comparison operations <, >, <=, >= are automatically available when you define compareTo():

// OperatorOverloading/Comparison.kt
package operatoroverloading
import atomictest.eq

operator fun E.compareTo(e: E): Int =
  v.compareTo(e.v)

fun main() {
  val a = E(2)
  val b = E(3)
  (a < b) eq true     // a.compareTo(b) < 0
  (a > b) eq false    // a.compareTo(b) > 0
  (a <= b) eq true    // a.compareTo(b) <= 0
  (a >= b) eq false   // a.compareTo(b) >= 0
}

compareTo() must return an Int indicating:

  • 0 if the elements are equal.
  • A positive value if the first element (the receiver) is bigger than the second (the argument).
  • A negative value if the first element is smaller than the second.

# Ranges and Containers

rangeTo() overloads the .. operator for creating ranges, while contains() indicates whether a value is within a range:

// OperatorOverloading/Ranges.kt
package operatoroverloading
import atomictest.eq

data class R(val r: IntRange) { // Range
  override fun toString() = "R($r)"
}

operator fun E.rangeTo(e: E) = R(v..e.v)

operator fun R.contains(e: E): Boolean =
  e.v in r

fun main() {
  val a = E(2)
  val b = E(3)
  val r = a..b        // a.rangeTo(b)
  (a in r) eq true    // r.contains(a)
  (a !in r) eq false  // !r.contains(a)
  r eq R(2..3)
}

# Container Access

Overloading contains() allows you to check whether a value is in a container, while get() and set() support reading and assigning elements in a container using square brackets:

// OperatorOverloading/ContainerAccess.kt
package operatoroverloading
import atomictest.eq

data class C(val c: MutableList<Int>) {
  override fun toString() = "C($c)"
}

operator fun C.contains(e: E) = e.v in c

operator fun C.get(i: Int): E = E(c[i])

operator fun C.set(i: Int, e: E) {
  c[i] = e.v
}

fun main() {
  val c = C(mutableListOf(2, 3))
  (E(2) in c) eq true  // c.contains(E(2))
  (E(4) in c) eq false // c.contains(E(4))
  c[1] eq E(3)         // c.get(1)
  c[1] = E(4)          // c.set(2, E(4))
  c eq C(mutableListOf(2, 4))
}

In IntelliJ IDEA or Android Studio you can navigate (opens new window) to a declaration of a function or a class from its usage. This also works with operators: you can put the cursor on .. then navigate to its definition to see which operator function is called.

# Invoke

Placing parentheses after an object generates a call to invoke(), so the invoke() operator makes an object look like a function. You can define invoke() with any number of parameters:

// OperatorOverloading/Invoke.kt
package operatoroverloading
import atomictest.eq

class Func {
  operator fun invoke() = "invoke()"
  operator fun invoke(i: Int) = "invoke($i)"
  operator fun invoke(i: Int, j: String) =
    "invoke($i, $j)"
  operator fun invoke(
    i: Int, j: String, k: Double
  ) = "invoke($i, $j, $k)"
}

fun main() {
  val f = Func()
  f() eq "invoke()"
  f(22) eq "invoke(22)"
  f(22, "Hi") eq "invoke(22, Hi)"
  f(22, "Three", 3.1416) eq
    "invoke(22, Three, 3.1416)"
}

You can also define invoke() with vararg to work with any number of arguments of the same type (see [Variable Argument Lists](javascript:void(0))).

invoke() can be defined as an extension function. Here, it’s an extension for String, taking a function as a parameter and calling that function on the String:

// OperatorOverloading/StringInvoke.kt
package operatoroverloading
import atomictest.eq

operator fun String.invoke(
  f: (s: String) -> String
) = f(this)

fun main() {
  "mumbling" { it.toUpperCase() } eq
    "MUMBLING"
}

Because the lambda is the final invoke() argument, it can be called without parentheses.

If you have a function reference, you can use it to call the function directly using parentheses or via invoke():

// OperatorOverloading/InvokeFunctionType.kt
package operatoroverloading
import atomictest.eq

fun main() {
  val func: (String) -> Int = { it.length }
  func("abc") eq 3
  func.invoke("abc") eq 3

  val nullableFunc: ((String) -> Int)? = null
  if (nullableFunc != null) {
    nullableFunc("abc")
  }
  nullableFunc?.invoke("abc")  // [1]
}
  • [1] If a function reference is nullable, you can combine invoke() and safe access.

The most common use for a custom invoke() is when creating DSLs.

# Function Names in Backticks

Kotlin allows spaces, certain nonstandard characters, and reserved words in a function name by placing that function name inside backticks:

// OperatorOverloading/Backticks.kt
package operatoroverloading

fun `A long name with spaces`() = Unit

fun `*how* is this working?`() = Unit

fun `'when' is a keyword`() = Unit

// fun `Illegal characters :<>`() = Unit

fun main() {
  `A long name with spaces`()
  `*how* is this working?`()
  `'when' is a keyword`()
}

This can be particularly helpful for [Unit Testing](javascript:void(0)) because you can create readable test names that include details about those tests. It also simplifies interactions with Java code.

You can easily create incomprehensible code:

// OperatorOverloading/Swearing.kt
package operatoroverloading
import atomictest.eq

infix fun String.`#!%`(s: String) =
  "$this Rowzafrazaca $s"

fun main() {
  "howdy" `#!%` "Ma'am!" eq
    "howdy Rowzafrazaca Ma'am!"
}

Kotlin accepts this code, but what does it mean to the reader? Because code is read much more than it is written, you should make your programs as understandable as possible.

  • -

Operator overloading is not an essential feature, but is an excellent example of how a language is more than just a way to manipulate the underlying computer. The challenge is crafting the language to provide better ways to express your abstractions, so humans have an easier time understanding the code without getting bogged down in needless detail. It’s possible to define operators in ways that obscure meaning, so tread carefully.

Everything is syntactic sugar. Toilet paper is syntactic sugar, and I still want it.Barry Hawkins

Exercises and solutions can be found at www.AtomicKotlin.com.