6 min read

Operator overloading is a form of polymorphism. Some operators change behaviors on different types. The classic example is the operator plus (+). On numeric values, plus is a sum operation and on String is a concatenation. Operator overloading is a useful tool to provide your API with a natural surface. Let’s say that we’re writing a Time and Date library; it’ll be natural to have the plus and minus operators defined on time units.  In this article, we’ll understand how Operator Overloading works in Kotlin.

This article has been extracted from the book, Functional Kotlin, by Mario Arias and Rivu Chakraborty. 
Kotlin lets you define the behavior of operators on your own or existing types with functions, normal or extension, marked with the operator modifier:
class Wolf(val name:String) {
   operator fun plus(wolf: Wolf) = Pack(mapOf(name to this, wolf.name to wolf))
}
class Pack(val members:Map)

fun main(args: Array) {
val talbot = Wolf("Talbot")
val northPack: Pack = talbot + Wolf("Big Bertha") // talbot.plus(Wolf("..."))
}

The operator function plus returns a Pack value. To invoke it, you can use the infix operator way (Wolf + Wolf) or the normal way (Wolf.plus(Wolf)).

Something to be aware of about operator overloading in Kotlin—the operators that you can override in Kotlin are limited; you can’t create arbitrary operators.

Binary operators

Binary operators receive a parameter (there are exceptions to this rule—invoke and indexed access).

The Pack.plus extension function receives a Wolf parameter and returns a new Pack. Note that MutableMap also has a plus (+) operator:

operator fun Pack.plus(wolf: Wolf) = Pack(this.members.toMutableMap() + (wolf.name to wolf))
val biggerPack = northPack + Wolf("Bad Wolf")

The following table will show you all the possible binary operators that can be overloaded:

Operator

Equivalent Notes
x + y x.plus(y)
x - y x.minus(y)
x * y x.times(y)
x / y x.div(y)
x % y x.rem(y) From Kotlin 1.1, previously mod.
x..y x.rangeTo(y)
x in y y.contains(x)
x !in y !y.contains(x)
x += y x.plussAssign(y) Must return Unit.
x -= y x.minusAssign(y) Must return Unit.
x *= y x.timesAssign(y) Must return Unit.
x /= y x.divAssign(y) Must return Unit.
x %= y x.remAssign(y) From Kotlin 1.1, previously modAssign. Must return Unit.
x == y x?.equals(y) ?: (y === null) Checks for null.
x != y !(x?.equals(y) ?: (y === null)) Checks for null.
x x.compareTo(y) Must return Int.
x > y x.compareTo(y) > 0 Must return Int.
x x.compareTo(y) Must return Int.
x >= y x.compareTo(y) >= 0 Must return Int.

Invoke

When we introduce lambda functions, we show the definition of Function1:

/** A function that takes 1 argument. */
public interface Function1 : Function {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

The invoke function is an operator, a curious one. The invoke operator can be called without name.

The class Wolf has an invoke operator:

enum class WolfActions {
   SLEEP, WALK, BITE
}
class Wolf(val name:String) {
operator fun invoke(action: WolfActions) = when (action) {
WolfActions.SLEEP -> "$name is sleeping"
WolfActions.WALK -> "$name is walking"
WolfActions.BITE -> "$name is biting"
}
}

fun main(args: Array) {
val talbot = Wolf("Talbot")

talbot(WolfActions.SLEEP) // talbot.invoke(WolfActions.SLEEP)
}

That’s why we can call a lambda function directly with parenthesis; we are, indeed, calling the invoke operator.

The following table will show you different declarations of invoke with a number of different arguments:

Operator Equivalent Notes
x() x.invoke()
x(y) x.invoke(y)
x(y1, y2) x.invoke(y1, y2)
x(y1, y2..., yN) x.invoke(y1, y2..., yN)

Indexed access

The indexed access operator is the array read and write operations with square brackets ([]), that is used on languages with C-like syntax. In Kotlin, we use the get operators for reading and set for writing.

With the Pack.get operator, we can use Pack as an array:

operator fun Pack.get(name: String) = members[name]!!
val badWolf = biggerPack["Bad Wolf"]

Most of Kotlin data structures have a definition of the get operator, in this case, the Map returns a V?.

The following table will show you different declarations of get with a different number of arguments:

Operator Equivalent Notes
x[y] x.get(y)
x[y1, y2] x.get(y1, y2)
x[y1, y2..., yN] x.get(y1, y2..., yN)

The set operator has similar syntax:

enum class WolfRelationships {
   FRIEND, SIBLING, ENEMY, PARTNER
}
operator fun Wolf.set(relationship: WolfRelationships, wolf: Wolf) {
println("${wolf.name} is my new $relationship")
}

talbot[WolfRelationships.ENEMY] = badWolf
The operators get and set can have any arbitrary code, but it is a very well-known and old convention that indexed access is used for reading and writing. When you write these operators (and by the way, all the other operators too), use the principle of least surprise. Limiting the operators to their natural meaning on a specific domain, makes them easier to use and read in the long run. The following table will show you different declarations of set with a different number of arguments:
Operator Equivalent Notes
x[y] = z x.set(y, z) Return value is ignored
x[y1, y2] = z x.set(y1, y2, z) Return value is ignored
x[y1, y2..., yN] = z x.set(y1, y2..., yN, z) Return value is ignored

Unary operators

Unary operators don’t have parameters and act directly in the dispatcher.

We can add a not operator to the Wolf class:

operator fun Wolf.not() = "$name is angry!!!"
!talbot // talbot.not()

The following table will show you all the possible unary operators that can be overloaded:

Operator

Equivalent

Notes
+x x.unaryPlus()
-x x.unaryMinus()
!x x.not()
x++ x.inc() Postfix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn’t mutate the dispatcher.
x-- x.dec() Postfix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn’t mutate the dispatcher.
++x x.inc() Prefix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn’t mutate the dispatcher.
--x x.dec() Prefix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn’t mutate the dispatcher.

Postfix (increment and decrement) returns the original value and then changes the variable with the operator returned value. Prefix returns the operator’s returned value and then changes the variable with that value.

Now you know how Operator Overloading works in Kotlin. If you found this article interesting and would like to read more, head on over to get the whole book, Functional Kotlin, by Mario Arias and Rivu Chakraborty.

Read Next:

Extension functions in Kotlin: everything you need to know

Building RESTful web services with Kotlin

Building chat application with Kotlin using Node.js, the powerful Server-side JavaScript platform


Subscribe to the weekly Packt Hub newsletter. We'll send you the results of our AI Now Survey, featuring data and insights from across the tech landscape.

* indicates required
I'm a technology enthusiast who designs and creates learning content for IT professionals, in my role as a Category Manager at Packt. I also blog about what's trending in technology and IT. I'm a foodie, an adventure freak, a beard grower and a doggie lover.

LEAVE A REPLY

Please enter your comment!
Please enter your name here