[Kotlin] Operator overloading

romainbsl
1,478 views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Introduction

Kotlin documentation said that if you define an operator function plus, you can use +, by convention, to call that function.

Conventions are not just for operators, you'll find them in Collections, Ranges, Destructuring Declaration and Invocation.

Let's see the details !!!

Operator overloading

Kotlin allows us to overload some operators on any object we have created, or that we know of (through extensions). The concept of operator overloading provides a way to invoke functions to perform arithmetic operation, equality checks or comparison on whatever object we want, through symbols like +, -, /, *, %, <, >. But, obviously, those overloading should be defined when it make sense to use them.

For the following parts, let's assume we have the data class:

data class Point(val x: Double, val y: Double)

Arithmetic operators

To overload the + operator we need to implement the function plus, with the keyword operator. This function takes one parameter of any kind, even it make sense in most cases to use the same type.

// Here how to provide `+` operator on our object Point
operator fun plus(p: Point) = Point(this.x + p.x, this.y + p.x)
// return type is inferred to Point

To go further we can apply all the following operator overloading on the object Point.

expressionfunction called
p1 + p2p1.plus(p2)
p1 - p2p1.minus(p2)
p1 * p2p1.times(p2)
p1 / p2p1.div(p2)
p1 % p2p1.rem(p2)
p1++p1.inc()
p1--p1.dec()

Here the implementation on our previous data class:

1
2
3
4
5
6
7
8
9
10
data class Point(val x: Double, val y: Double) {
operator fun plus(p: Point) = Point(x + p.x, y + p.y)
operator fun minus(p: Point) = Point(x - p.x, y - p.y)
operator fun times(p: Point) = Point(x * p.x, y * p.y)
operator fun div(p: Point) = Point(x / p.x, y / p.y)
operator fun inc() = Point(x + 1, y + 1)
operator fun dec() = Point(x - 1, y - 1)
}
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Note that those examples are quiet simple, you may be able to implement more complex operator, depending on your own object's definition.

Let's practice a little

Assuming the following data class:

data class Fraction(val numerator: Int, val denominator: Int)
Try to implement the `+` operator for `Fraction`.
1
2
3
operator fun Fraction.plus(add: Fraction): Fraction {
TODO("not implemented")
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
operator fun Fraction.plus(add: Fraction): Fraction =
  if (this.denominator == add.denominator) {
    Fraction(this.numerator + add.numerator, denominator)
  } else {
    Fraction(numerator * add.denominator + add.numerator * denominator,
        denominator * add.denominator)
  }
Try to implement the `*` operator for `Fraction`.
1
2
3
operator fun Fraction.times(number: Int): Fraction {
TODO("not implemented")
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
operator fun Fraction.times(num: Int) = Fraction(numerator * num, denominator)

Equality and inequality

As a Java developer, I always felt confused about equality, sometimes you have to use == / != (on primitives), sometimes you have to use equals(). (reminder, the usage of ==/ != on non-primitive checks the reference of the object not its value).

Kotlin makes it more simple by reserving the symbols ==and != to check the objects' values (to check references you may use ===/ !==).

To overload the equality (and inequality) checks, you may override the well known equals() function.

override fun equals(other: Any?): Boolean {
  if (other == null || 
      other !is Point ||
      x != other.x || y != other.y) return false
    
  return true
}

Exception: As you may know, in Kotlin objects can be non-null. In that case, x == null will always be false, and equals will never be called.

Tips: As you may know, in Kotlin, data class already implements eqauls(), as other useful functions (getters/setters, hashCode(), copy() and toString())

Let's practice a little
Try to implement the `==` operator for `EqFraction`.
1
2
3
4
5
6
7
8
9
class EqFraction(val numerator: Int, val denominator: Int) {
val decimal by lazy { numerator.toDouble() / denominator }
override fun equals(other: Any?): Boolean {
TODO("not implemented")
}
override fun toString() = "Fraction($numerator, $denominator) "
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
override fun equals(other: Any?): Boolean {
  if (this === other) return true
  if (javaClass != other?.javaClass) return false

  other as EqFraction

  return decimal == other.decimal
}

NB: As a good practice, when you override equals() function, you should override hashCode() too

Comparison

Comparison, <, >, <= and >=, are all based on one function, compareTo(). This function returns an Int, that define if the left-side of an expression is greater, smaller or equals to the right-side of that same expression.

Expressionfunction called
left > rightleft.compareTo(right) > 0
left < rightleft.compareTo(right) < 0
left >= rightleft.compareTo(right) >= 0
left <= rightleft.compareTo(right) <= 0

Here is an examples on how we could compare Points:

1
2
3
4
5
6
7
8
data class Point(val x: Double, val y: Double) {
operator fun compareTo(other: Point): Int = when {
y != other.y -> (y - other.y).toInt()
else -> (x - other.x).toInt()
}
}
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You just saw how to use an operator to implement comparison between two instances of your object. But, there is another way to implement such mechanism, you could implement the Comparable interface, and overrides its compareTo method, it would have the same result.

data class Point(val x: Double, val y: Double) : Comparable<Point> {
  override fun compareTo(other: Point): Int = when {
    y != other.y -> (y - other.y).toInt()
    else -> (x - other.x).toInt()
  }
}
Let's practice a little
Try to implement the compareTo() function for `ComparableFraction`
1
2
3
4
5
6
7
data class ComparableFraction(val numerator: Int, val denominator: Int) : Comparable<ComparableFraction> {
private val decimal by lazy { numerator.toDouble() / denominator }
override fun compareTo(other: ComparableFraction): Int {
TODO("not implemented")
}
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
 override fun compareTo(other: ComparableFraction) = decimal.compareTo(other.decimal)

Collections

Kotlin's Collections bring two type of conventions, the interaction with a specific data by using getter and setter with indexes, and the ability to check if an object belongs to a given list through the keyword in.

get / set

You can implement getand set operators and then use sqaure brackets to interact with your objects.

ExpressionFunction called
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i_1, ..., i_n]a.get(i_1, ..., i_n)

in / !in

in is an operator that calls the function contains(), that you may implement to check if a value if part of your object. Obviously, !in will call !contains(). You'll be glad to know that Collection have already an implementation of the containsfunction, so you could already check if 1 is part of the listOf(1, 2, 3) (hint: this return true ^_^).

Here is an example implementing operators for collections

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data class OperatingSystem(val name: String,
val products: MutableList<Product> = mutableListOf()) {
operator fun get(id: Int) = products[id]
operator fun set(id: Int, product: Product) {
products.add(id, product)
}
operator fun contains(p: Product) = p in products
}
data class Product(val name: String)
val android = OperatingSystem("Android")
val googlePixel2 = Product("Google Pixel 2")
val samsungGalaxyS8 = Product("Samsung Galaxy S8")
val xiaomiMiMix2 = Product("Xiaomi Mi Mix 2")
val iPhoneX = Product("iPhone X")
//{...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Let's practice a little
Try to implement the get() function for `ComparableFraction` to retrieve the numerator and the denominator
1
2
3
4
5
import java.io.Serializable
operator fun ComparableFraction.get(index: Int): Serializable {
TODO("not implemented")
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
operator fun ComparableFraction.get(index: Int) =
    when (index) {
      0 -> numerator
      1 -> denominator
      else -> IllegalArgumentException("Index must be 0 or 1")
    }

Ranges

Ranges are part of the nice features that make you love Kotlin. It's no magic, there is (nearly) no complexity, but when you think about it, it just make sense. To build ranges you can use the operator .., translated to rangeTo().

The Kotlin standard library provides some extra features around type that we know. So integrals have their own implementation of Ranges, like IntRange (as LongRange and CharRange), that allow you to write 1..10 that will build a collection of value from 1 to 10 included, by calling the following snippet 1.rangeTo(10).

1
4
5
//{...}
(1..10).forEach(::print)
//{...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Custom ranges

Let's get back to our Point class.

data class Point(val x: Double, val y: Double) : Comparable<Point> {
  override fun compareTo(other: Point): Int = when {
    y != other.y -> (y - other.y).toInt()
    else -> (x - other.x).toInt()
  }
}

Kotlin provides an extension rangeTo() on Comparable<T> interface, so we don't need to overload it for our Point class.

1
4
5
6
7
//{...}
val pointRange = Point(1.0, 1.0)..Point(5.0, 5.0)
val point = Point(2.5, 2.5)
println("Does $point is in $pointRange ? [= ${point in pointRange} ]")
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Iterating over ranges

To go further we may implement an operator to iterate over Point ranges, the iterator():

operator fun ClosedRange<Point>.iterator() = object : Iterator<Point> {
  var currentPoint = start
  override fun hasNext() = currentPoint <= endInclusive
  override fun next() = currentPoint++
}

By doing that, we can now iterate over a Point collection:

1
4
5
//{...}
for (point in Point(1.0, 1.0)..Point(5.0, 5.0)) println(point)
//{...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Let's practice a little
Try to implement the iterator() operator for `RangeFraction`
1
2
3
4
operator fun ClosedRange<RangeFraction>.iterator() = object : Iterator<RangeFraction> {
override fun hasNext() = TODO("not implemented")
override fun next() = TODO("not implemented")
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
operator fun ClosedRange<RangeFraction>.iterator() = object : Iterator<RangeFraction> {
  var currentFraction: RangeFraction = start
  override fun hasNext() = currentFraction <= endInclusive
  override fun next() = currentFraction++
}

Destructuring Declaration

Many times, you certainly had to decompose objects to play with:

val name = person.name
val age = person.age
val address = person.address
// and so on

With Kotlin, you are able to decompose your data class, Map(s), and own object in a simple way.

data class

data class get rid of a lot of boilerplate code (getter/setter, equals, hashCode, toString, copy...), but it also brings the ability to decompose our object according to the properties we have defined. Kotlin create a component operator for each of those properties.

Let's take this data class as example

data class Person(val name: String, val age: Int, val address: String)

The compiler will automatically provide componentN() for name (component1), age (component2) and address (component3). Give you the power to destructure the Person object into variables.

data class Person(val name: String, val age: Int, val address: String) {
  // behind the scene
  operator fun component1() = this.name
  operator fun component2() = this.age
  operator fun component3() =  this.address
}

Usage

1
2
3
6
7
8
data class Person(val name: String, val age: Int, val address: String)
// {...}
val (name, age, address) = buildPerson("Romain", "Belgium")
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

tips: if you don't need some elements you can omit them by using an underscore _

1
2
3
6
7
8
data class Person(val name: String, val age: Int, val address: String)
// {...}
val (name, _, address) = buildPerson("Romain", "Belgium")
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Map(s)

Map implements iterator(), component1() for key, and component2() for value. That help us to write nice more convenient iteration like:

1
4
5
6
// {...}
val map = mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four")
for((key, value) in map) println("Literal of $key is $value")
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Let's practice a little
Try to use a Desctructuring Declaration to get all value from `Product` in once
1
4
5
6
// {...}
val product = Product("Pixel 2", "Google", 5.0, 649.0)
val name = product.name
// {...}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Show the answer
val (name, brand, size, price) = product

Summary

Here is a reminder of what you can do by convention

ExpressionInvoke
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b), a.mod(b) (deprecated)
a > ba.compareTo(b)
a++a.inc()
b--b.dec()
a == ba.equals(b)
a != b!a.equals(b)
a < ba.compareTo(b)
a >= ba.compareTo(b)
a <= ba.compareTo(b)
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i] = ba.set(i, b)
a[i, j] = ba.set(i, j, b)
a in bb.contains(a)
a !in b!b.contains(a)
a..ba.rangeTo(b)

Go further with the full documentation !

Thanks to Simon Wirtz for his inspiring article !

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content