Operator Overloading
Reviewed & published by Brayan K
Operator overloading lets you give symbols like +, [ ], and < a custom meaning for your own types by writing specially-named functions marked with the operator keyword.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll overload arithmetic, indexing, comparison, and the call operator, building a clean Vector2 type along the way.
What You'll Learn in This Lesson
- Backing + - * with plus, minus, times
- Indexing with get and set ([ ])
- Calling objects like functions with invoke
- Powering <, >, <=, >= with a single compareTo
- Why == usually comes free from data classes
- When overloading helps readability — and when it hurts
1️⃣ Arithmetic: plus, minus, times
Each arithmetic symbol maps to a fixed function name: + → plus, - → minus, * → times. Mark the function operator and Kotlin rewrites a + b into a.plus(b).
// Mark a function 'operator' with a special name to back an operator.
// 'plus' backs the + symbol.
data class Vector2(val x: Int, val y: Int) {
operator fun plus(other: Vector2) = Vector2(x + other.x, y + other.y)
operator fun minus(other: Vector2) = Vector2(x - other.x, y - other.y)
operator fun times(factor: Int) = Vector2(x * factor, y * factor)
}
fun main() {
val a = Vector2(1, 2)
val b = Vector2(3, 4)
println(a + b) // -> Vector2(x=4, y=6)
println(b - a) // -> Vector2(x=2, y=2)
println(a * 3) // -> Vector2(x=3, y=6)
}2️⃣ Indexing with get and set
The square-bracket syntax is two operators. Reading g[r, c] calls get(r, c); writing g[r, c] = v calls set(r, c, v). You can take any number of index arguments, which is perfect for a 2-D grid.
// 'get' backs the [ ] read; 'set' backs the [ ] = write.
class Grid(val width: Int, val height: Int) {
private val cells = IntArray(width * height)
operator fun get(row: Int, col: Int): Int = cells[row * width + col]
operator fun set(row: Int, col: Int, value: Int) {
cells[row * width + col] = value
}
}
fun main() {
val g = Grid(2, 2)
g[0, 0] = 5 // calls set(0, 0, 5)
g[1, 1] = 9 // calls set(1, 1, 9)
println(g[0, 0]) // calls get(0, 0) -> 5
println(g[1, 1]) // calls get(1, 1) -> 9
println(g[0, 1]) // never set, defaults to 0 -> 0
}The set on a cell never touched returns the array's default 0 — handy for sparse data.
3️⃣ Comparison and invoke
A single compareTo returning an Int backs all four of <, >, <=, >=: negative means less, zero equal, positive greater. Separately, invoke lets you call an object like a function.
// 'compareTo' backs <, >, <=, >= and returns an Int:
// negative = less, zero = equal, positive = greater.
class Money(val cents: Int) {
operator fun compareTo(other: Money): Int = cents - other.cents
override fun toString() = "$" + (cents / 100) + "." + (cents % 100)
}
fun main() {
val cheap = Money(150) // $1.50
val pricey = Money(999) // $9.99
println(cheap < pricey) // -> true
println(cheap > pricey) // -> false
println(cheap <= cheap) // -> true (compareTo returns 0)
}// 'invoke' lets you call an object like a function: obj(args)
class Multiplier(val factor: Int) {
operator fun invoke(value: Int): Int = value * factor
}
fun main() {
val triple = Multiplier(3)
// Because of invoke, we can "call" the object directly:
println(triple(5)) // -> 15
println(triple(10)) // -> 30
}Your turn. Fill in the ___ blank, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
data class Point(val x: Int, val y: Int) {
// 1) Make + work by overloading 'plus'
operator fun ___(other: Point) = // 👉 put: plus
Point(x + other.x, y + other.y)
}
fun main() {
val p = Point(2, 3)
val q = Point(4, 5)
println(p + q) // 👉 should print: Point(x=6, y=8)
// ✅ Expected output:
// Point(x=6, y=8)
}Mini-Challenge: A Scalable Volume Type
Overload plus and times so volumes add and scale naturally.
// 🎯 MINI-CHALLENGE: a scalable Volume type.
// Save as Volume.kt and run main().
//
// 1. Class Volume(val ml: Int).
// 2. Overload 'plus' so two Volumes add their ml.
// 3. Overload 'times' so Volume * Int scales the ml.
// 4. Give it a toString() that prints "<ml>ml".
//
// ✅ Expected output:
// 750ml
// 1000ml
class Volume(val ml: Int) {
// your operators here
override fun toString() = "${ml}ml"
}
fun main() {
val a = Volume(250)
val b = Volume(500)
println(a + b) // 750ml
println(b * 2) // 1000ml
}Common Errors (and the fix)
- ❌ "'operator' modifier is required" — you named the function right but forgot the keyword. ✅ Add operator before fun.
- ❌ "Unresolved reference: pluss" — only the exact names work. ✅ Use the precise name (plus, not pluss or add).
- ❌ compareTo returns Boolean — comparisons break. ✅ compareTo must return an Int (negative / zero / positive).
- ❌ Surprising operators — overloading + to do something unexpected. ✅ Only overload when the symbol's meaning is obvious to a reader.
Pro Tips
- 💡 Implement Comparable: for sortable types, implement Comparable<T> so they work with sorted() and maxOf() too.
- 💡 Data classes give == free: they generate a correct equals and hashCode for you.
- 💡 Augmented ops: += uses plusAssign if present, otherwise falls back to plus.
📋 Quick Reference — Operator → Function
| Operator | Function name |
|---|---|
| a + b | a.plus(b) |
| a * b | a.times(b) |
| a[i] | a.get(i) |
| a[i] = v | a.set(i, v) |
| a < b | a.compareTo(b) < 0 |
| a(x) | a.invoke(x) |
🎉 Lesson Complete!
- ✅ Mark specially-named functions operator to back symbols
- ✅ plus/minus/times back + - *
- ✅ get/set back [ ]; invoke backs the call ( )
- ✅ One compareTo powers all four comparisons
- ✅ Overload only when the symbol's meaning is obvious
Practice quiz
Which function name backs the + operator?
- add
- plus
- sum
- concat
Answer: plus. a + b is rewritten as a.plus(b), so + is backed by the plus function.
What keyword must mark a function so it can back an operator?
- override
- infix
- operator
- open
Answer: operator. You must mark the specially-named function with the operator keyword.
What does a[i] = v call under the hood?
- a.get(i, v)
- a.put(i, v)
- a.assign(i, v)
- a.set(i, v)
Answer: a.set(i, v). Indexed assignment a[i] = v is backed by the set function.
How many functions do you implement to support <, >, <=, and >=?
- One compareTo function returning Int
- Four separate functions
- Two functions: less and greater
- None, they come free
Answer: One compareTo function returning Int. A single compareTo returning an Int powers all four comparison operators.
What should compareTo return when the receiver is less than the argument?
- A positive number
- true
- Zero
- A negative number
Answer: A negative number. Negative means less, zero means equal, and positive means greater.
What does the invoke operator let you do?
- Compare two objects
- Index into an object with brackets
- Call an object like a function, as in obj(args)
- Add two objects
Answer: Call an object like a function, as in obj(args). invoke makes an object callable, so obj(x) calls obj.invoke(x).
Which function name backs the * operator?
- mul
- product
- times
- multiply
Answer: times. a * b is rewritten as a.times(b).
How do most classes get a correct == for free?
- By overloading equals manually
- By being a data class
- By implementing Comparable
- By marking the class operator
Answer: By being a data class. A data class generates a correct equals (and hashCode), backing ==.
What happens if you name the function pluss instead of plus?
- + still works
- It backs the ++ operator
- The compiler reports an unresolved reference for +
- It backs the += operator
Answer: The compiler reports an unresolved reference for +. Only the exact predefined names work; pluss does not back any operator.
When is operator overloading a good idea?
- Whenever it saves typing
- When the symbol's meaning is obvious and matches intuition
- Only for collection types
- Never, it is always discouraged
Answer: When the symbol's meaning is obvious and matches intuition. Overload only when the operator's meaning is clear, like + on vectors or < on money.
Continue this course
- Previous: Annotations & Reflection
- Next: Type Aliases & Value Classes — Add zero-overhead type safety with @JvmInline value classes
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What is operator overloading?
Operator overloading lets you give symbols like +, -, *, [ ], <, and even the call syntax () a custom meaning for your own types. In Kotlin you do this by writing a function with a fixed special name (plus, minus, times, get, set, compareTo, invoke, and so on) and marking it with the 'operator' keyword. After that, writing a + b on your type calls a.plus(b) under the hood. It makes domain types like vectors, money, or matrices read naturally instead of forcing method calls everywhere.
Which function name backs which operator?
The mapping is fixed by the language. + is plus, - is minus, * is times, / is div, % is rem. Indexing a[i] is get and a[i] = v is set. The call syntax a(x) is invoke. The comparison operators <, >, <=, >= all route through a single compareTo function that returns an Int. The augmented assignments like += can use plusAssign, or fall back to plus. You cannot invent new symbols — you can only give meaning to the ones Kotlin already defines.
How does compareTo power all four comparison operators?
You write one operator fun compareTo(other): Int that returns a negative number when the receiver is less than the argument, zero when they are equal, and a positive number when it is greater. Kotlin then translates a < b into a.compareTo(b) < 0, a > b into a.compareTo(b) > 0, and similarly for <= and >=. So a single method makes all four comparisons work. Implementing the Comparable interface formalises this and lets your type be used with sorting functions.
Should I overload == myself?
Usually no. The == operator is backed by equals, and for most classes you get a correct equals (and hashCode) for free by making it a data class. Only override equals manually for non-data classes where you need custom equality — and if you do, you must also override hashCode to keep them consistent, or hash-based collections like HashSet and HashMap will misbehave. Note that == calls equals safely even on nulls, while === checks reference identity and is never overloadable.
When should I avoid operator overloading?
Use it only when the symbol's meaning is obvious and matches intuition — adding two vectors with + or comparing two money amounts with < reads beautifully. Avoid it when the operator would surprise readers: making + concatenate unrelated objects, or giving * a side effect, hurts readability more than a clearly named method like combine() or scaleBy() would help. The goal is code that reads like the problem domain, not cleverness for its own sake.