Operator Overloading Workbook

Ten short exercises on Kotlin operator overloading — plus, get and set, invoke, contains, compareTo, compound assignment, unaryMinus, and making a type iterable.

Practice problems for Operator Overloading, Kept on a Leash. Each takes a minute or two, and every one auto-checks: implement the operator in the editor and press Run — hidden tests exercise it through the real operator (a + b, p in rect, for (x in …)) and go green when you’re right, red (with a hint) when you’re not, all on JetBrains’ Kotlin server. Click Show answer to compare. Nothing here is a trick question, just direct practice of the syntax from the lesson.

arithmetic and indexing

1. Add two vectors

Give data class Vec(val x: Int, val y: Int) a + operator that adds component-wise, so Vec(1, 2) + Vec(3, 4) is Vec(4, 6).

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun plus() { Assert.assertEquals("add vectors component-wise", Vec(4, 6), Vec(1, 2) + Vec(3, 4)) } } //sampleStart data class Vec(val x: Int, val y: Int) { operator fun plus(other: Vec) = Vec(0, 0) // TODO: add component-wise } //sampleEnd
Show answer Hide answer
data class Vec(val x: Int, val y: Int) {
    operator fun plus(other: Vec) = Vec(x + other.x, y + other.y)
}

Vec(1, 2) + Vec(3, 4)   // Vec(4, 6)

a + b compiles to a.plus(b).

2. Index into a grid

Give Grid a get(x, y) so grid[x, y] reads a cell.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun get() { val grid = Grid(intArrayOf(1, 2, 3, 4), width = 2) Assert.assertEquals("read cells[y * width + x]", 2, grid[1, 0]) } } //sampleStart class Grid(private val cells: IntArray, val width: Int) { operator fun get(x: Int, y: Int) = 0 // TODO: read cells[y * width + x] } //sampleEnd
Show answer Hide answer
class Grid(private val cells: IntArray, val width: Int) {
    operator fun get(x: Int, y: Int) = cells[y * width + x]
}

3. Index assignment

Add a set(x, y, value) so grid[x, y] = 9 writes a cell.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun set() { val grid = Grid(intArrayOf(0, 0, 0, 0), width = 2) grid[1, 0] = 9 Assert.assertEquals("write value into cells[y * width + x]", 9, grid[1, 0]) } } //sampleStart class Grid(private val cells: IntArray, val width: Int) { operator fun get(x: Int, y: Int) = cells[y * width + x] operator fun set(x: Int, y: Int, value: Int) { // TODO: write value into cells[y * width + x] } } //sampleEnd
Show answer Hide answer
operator fun set(x: Int, y: Int, value: Int) {
    cells[y * width + x] = value
}

4. Call an object

Give Adder(val by: Int) an invoke(x) so add5(10) works like a function call.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun invoke() { val add5 = Adder(5) Assert.assertEquals("calling the object works like x + by", 15, add5(10)) } } //sampleStart class Adder(val by: Int) { operator fun invoke(x: Int) = x // TODO: return x + by } //sampleEnd
Show answer Hide answer
class Adder(val by: Int) {
    operator fun invoke(x: Int) = x + by
}

val add5 = Adder(5)
add5(10)   // 15

membership, comparison, assignment

5. Support the in keyword

Give Rectangle a contains(p: Point) so p in rect works.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun contains() { val rect = Rectangle(0, 0, 10, 10) Assert.assertTrue("a point inside is in the rectangle", Point(5, 5) in rect) Assert.assertFalse("a point outside is not", Point(20, 20) in rect) } } //sampleStart data class Point(val x: Int, val y: Int) data class Rectangle(val x0: Int, val y0: Int, val x1: Int, val y1: Int) operator fun Rectangle.contains(p: Point): Boolean = false // TODO: true when p is inside //sampleEnd
Show answer Hide answer
operator fun Rectangle.contains(p: Point): Boolean =
    p.x in x0..x1 && p.y in y0..y1

p in rect compiles to rect.contains(p).

6. Comparison operators

Make Money(val cents: Int) support < and > by implementing Comparable.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun compareTo() { Assert.assertTrue("less-than compares by cents", Money(100) < Money(200)) Assert.assertFalse("greater is not less", Money(200) < Money(100)) } } //sampleStart data class Money(val cents: Int) : Comparable<Money> { override fun compareTo(other: Money) = 0 // TODO: compare by cents } //sampleEnd
Show answer Hide answer
data class Money(val cents: Int) : Comparable<Money> {
    override fun compareTo(other: Money) = cents.compareTo(other.cents)
}

compareTo backs <, >, <=, >=.

7. Mutate in place with +=

Give IntBag a plusAssign operator so bag += 3 adds an element to it in place.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun plusAssign() { val bag = IntBag() bag += 3 bag += 5 Assert.assertEquals("+= adds an element in place", listOf(3, 5), bag.items) } } //sampleStart class IntBag(val items: MutableList<Int> = mutableListOf()) { operator fun plusAssign(x: Int) { // TODO: add x to items } } //sampleEnd
Show answer Hide answer
class IntBag(val items: MutableList<Int> = mutableListOf()) {
    operator fun plusAssign(x: Int) { items.add(x) }
}

+= calls plusAssign when it’s defined; otherwise it falls back to a = a + b via plus.

unary and iteration

8. Negate

Give Cents(val n: Int) a unary minus so -cents works.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun unaryMinus() { Assert.assertEquals("negate n", Cents(-5), -Cents(5)) } } //sampleStart data class Cents(val n: Int) { operator fun unaryMinus() = Cents(n) // TODO: negate n } //sampleEnd
Show answer Hide answer
data class Cents(val n: Int) {
    operator fun unaryMinus() = Cents(-n)
}

9. Make it for-loopable

Give Tree(private val nodes: List<String>) an iterator() so it works in a for loop.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun iterator() { val seen = mutableListOf<String>() for (node in Tree(listOf("a", "b"))) seen.add(node) Assert.assertEquals("iterate over the nodes in order", listOf("a", "b"), seen) } } //sampleStart class Tree(private val nodes: List<String>) { operator fun iterator(): Iterator<String> = emptyList<String>().iterator() // TODO: return nodes.iterator() } //sampleEnd
Show answer Hide answer
class Tree(private val nodes: List<String>) {
    operator fun iterator() = nodes.iterator()
}

for (node in Tree(listOf("a", "b"))) println(node)

10. Scale by an Int

Give Money(val cents: Int) a times operator so Money(100) * 3 returns Money(300).

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun times() { Assert.assertEquals("scale cents by n", Money(300), Money(100) * 3) } } //sampleStart data class Money(val cents: Int) { operator fun times(n: Int) = Money(cents) // TODO: scale cents by n } //sampleEnd
Show answer Hide answer
data class Money(val cents: Int) {
    operator fun times(n: Int) = Money(cents * n)
}

Money(100) * 3   // Money(300)

* maps to times — a good fit only because scaling money by a count has an obvious meaning.


Going deeper: get/set, contains, and plusAssign

12. Two-argument indexing

Give Grid get and set operators so grid[x, y] reads and grid[x, y] = v writes into a flat 3×3 IntArray.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun grid() { val g = Grid() g[1, 2] = 9 Assert.assertEquals(9, g[1, 2]) Assert.assertEquals(0, g[0, 0]) } } //sampleStart class Grid { private val cells = IntArray(9) // TODO: operator get(x, y) and operator set(x, y, value), index = y*3 + x } //sampleEnd
Show answer Hide answer
class Grid {
    private val cells = IntArray(9)
    operator fun get(x: Int, y: Int) = cells[y * 3 + x]
    operator fun set(x: Int, y: Int, value: Int) { cells[y * 3 + x] = value }
}

get/set back the [] syntax, and they take any number of indices.

13. Which way does in read?

You write 2 in vec, and Vec defines operator fun contains(n: Int). Which object is the receiver of contains, and with what argument? Reveal to check.

Show answer Hide answer

2 in vec compiles to vec.contains(2) — the operands reverse, because in asks “is 2 inside vec,” so the collection (vec) is the receiver and 2 is the argument. It’s the same reason key in map calls map.contains(key).

14. Mutate or reassign?

list += 3 mutates a MutableList in place, but x += 3 on a read-only val list reassigns. What two different functions are behind +=, and what happens if a type defines both? Reveal to check.

Show answer Hide answer

plusAssign (mutate in place) and plus (produce a new value, so a += b becomes a = a + b). A MutableList has plusAssign; a value type has plus. If a type defines both, a += b is ambiguous and the compiler rejects it — pick one.


Back to the lesson, Operator Overloading, Kept on a Leash, or on to the next one: coroutines.

Comments