Operator Overloading and Other Conventions

Kotlin

2026-08-21 09:00

Where we are

Two loose ends

Unit 2: in tested membership and drove for loops.

Unit 3: 1 to "one" was a function, not syntax.

They are the same thing.

Conventions

Certain language constructs are defined as calls to functions

with specific names.

Define such a function on your type, and the syntax works for it.

What you will be able to do

  1. Explain the convention principle.
  2. Overload arithmetic operators.
  3. Choose between plus and plusAssign.
  4. Implement equals and compareTo.
  5. Implement get, set, contains, rangeTo, iterator.
  6. Use destructuring and explain componentN.
  7. Use delegated properties.
  8. Implement your own delegate.
  9. Use map-backed and framework delegates.

Why not interfaces

Java: to be iterable, implement Iterable.

Kotlin: define a function named iterator.

Which can be an extension — so a class you do not own becomes iterable.

Between Java and Scala

Java — operators on built-in types only.

Scala — arbitrary new operators, and libraries nobody can read.

Kotlin — a fixed set of operators, and you decide what each means.

Arithmetic operators

plus

Defining one

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

println(p1 + p2)      // Point(x=40, y=60)

Why operator is required

It prevents a function that happens to be named plus

from accidentally gaining operator syntax.

And it tells a reader that operator use is intended.

Three extensions of the idea

operator fun Point.plus(other: Point) = ...      // on a type you do not own

operator fun Point.times(scale: Double): Point   // mixed operands

operator fun Char.times(count: Int): String      // unconstrained result

Note: p * 1.5 works. 1.5 * p does not, unless you define that too.

Unary

The set

unaryPlus · unaryMinus · not · inc · dec

And no bitwise symbols — infix functions instead:

0x0F and 0xF0
1 shl 3

Arguably more readable, and no precedence surprises.

Compound assignment

Two possible meanings

plus — new object, reassign. Requires a var.

plusAssign — modify in place, return Unit. Works on a val.

Define one, never both

With both, += is ambiguous and the compiler reports an error.

The standard library’s convention

Read-only collections define plus — a new collection.

Mutable ones define plusAssign — modified in place.

Which operator is available tells you which kind you have.

Comparison operators

== calls equals

So it compares contents. Reference comparison is ===.

The opposite of Java, and the better way round —

content comparison is what you want, so it gets the shorter syntax.

And it checks null first

a == b    →    a?.equals(b) ?: (b === null)

In Java, a.equals(b) throws when a is null.

Which is why defensive Java puts the constant first.

Two implementation notes

equals is marked override, not operator — it is already marked in Any.

And it cannot be an extension: the inherited member would always win.

Unit 3’s dispatch rule, showing its consequences.

compareTo

All four from one function

class Person(val firstName: String, val lastName: String)
        : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other,
                               Person::lastName, Person::firstName)
    }
}

Plus sorting, max and min from the standard library.

A performance note

The field-by-field version is faster than compareValuesBy.

Write the readable form. Optimise if a profiler says to.

Collection conventions

get

set

Not just integers

operator fun Point.get(index: Int): Int

A map’s get takes a key. Yours can take whatever suits.

Multiple parameters too: matrix[row, col].

in

Reads as you would say it

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
           p.y in upperLeft.y until lowerRight.y
}

println(Point(20, 30) in rect)

rangeTo

val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation)     // true

Any Comparable gets one free — so compareTo already earned you this.

iterator

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = ...

for (dayOff in newYear..daysOff) { println(dayOff) }

LocalDate is a JDK class. That extension makes it loop-able.

Now unit 2 makes sense

for (c in "abc")CharSequence has an iterator extension.

c in 'a'..'z'.. built a range, in called its contains.

One keyword, two functions.

And they can all be extensions

Java ties this to implementing interfaces.

You cannot make somebody else’s class implement your interface.

Kotlin can give a library class indexing, iteration and ranges without touching it.

Destructuring

The convention

Data classes generate them

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

One more item on the data list, alongside equals, hashCode, toString, copy.

Returning two values

data class NameComponents(val name: String, val extension: String)

val (name, ext) = splitFilename("example.kt")

A real improvement on an out-parameter or an array.

In loops

for ((key, value) in map) { }

Which is what map iteration was doing all along.

Map.Entry has component1 and component2 extensions.

The limitation

Destructuring is positional, not by name.

Reorder a data class’s constructor and every destructuring silently changes meaning.

Still compiles. Values now wrong.

Delegated properties

The idea

A property does not have to store its value in a field.

With by, its accessors are someone else’s job.

by lazy

private var _emails: List<Email>? = null
val emails: List<Email>
    get() {
        if (_emails == null) _emails = loadEmails(this)
        return _emails!!
    }
val emails by lazy { loadEmails(this) }

What you got

Runs on first access. Result cached.

Thread-safe by default.

No nullable field, no !!.

Delegates.observable

var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)

The notification logic exists in exactly one place.

A third observable property is one line.

Implementing your own

class ObservableProperty(var propValue: Int, ...) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

The KProperty parameter matters

It lets a delegate behave differently per property.

By using the property’s name as a key.

Which is what the map-backed case relies on entirely.

The translation rule

That is the whole mechanism

A hidden field holding the delegate.

A getter calling getValue. A setter calling setValue.

Any delegate you meet, however clever, is doing only this.

A map as a delegate

class Person {
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(name: String, value: String) { _attributes[name] = value }

    val name: String by _attributes
}

Fixed and dynamic attributes, accessed identically.

In frameworks

class User(id: EntityID) : Entity(id) {
    var name: String by Users.name       // reads and writes the database
    var age: Int by Users.age
}

The class reads like a plain object. The delegate does the database work.

Summary

The six things to carry away

  • Kotlin ties syntax to function names, not interfaces — so conventions can be extensions.
  • The operator set is fixed; only the meanings are yours.
  • == calls equals and checks null first, which removes a Java bug class.
  • Two loose ends closed: in was two functions; to was an infix extension.
  • Destructuring is positional, and reordering a data class is a real hazard.
  • Delegated properties have one translation rule and unlimited reach.

Where next

Higher-Order Functions returns to the functional thread.

Unit 5 taught you to use lambdas.

This one is about writing functions that take and return them — and inline.