How Can Kotlin Operator Overloading Help You?

Check out this article to learn how Kotlin operator overloading- a custom behavior for operators, like +,-,*, and unary plus- can help you.
This image is a featured image for article titled "How Can Kotlin Operator Overloading Help You?" and contains Kotlin logo in the foreground and a blurred photo of a person sitting next to a computer.

1. Introduction

In this article, I will show you what exactly Kotlin operator overloading is, how we can use it, and what’s even more important- what value can it bring to your project?

Video Tutorial

If you prefer video content, then check out my video:

 

 

If you find this content useful, please leave a subscription  😉

2. What is Kotlin Operator Overloading?

Well, operator overloading is nothing else, than a possibility to provide a custom implementation for a predefined set of operators on Kotlin types.

Let’s take a look at the following example:

data class Example(
    val value: Int
) {
  operator fun plus(otherValue: Int): Example =
    Example(this.value + otherValue)
}

fun main(args: Array<String>) {
  val example = Example(10)
  val addedExample = example + 11
  addedExample // Example(value=21)
}

When working with Kotlin, we can implement our own plus operator function, and later simply make use of the + sign in our code. (And of course, this is only one of the many operators we can work with).

What’s worth mentioning here is that we can implement our operator using both member functions and extension functions:

operator fun Example.plus(otherValue: Int): Example =
  Example(this.value + otherValue)

And I am well aware that this feature will never be a deal-breaker when selecting the right programming language for your project. Nevertheless, I still think that when properly used, it can help us maintain code readability and achieve a more expressive and natural codebase.

3. Invoke Operator

With all of that being said, let’s start the practice part and learn Kotlin operator overloading.

And as the first one, let’s take a look at the invoke operator, which simply translates parenthesis into the call to the invoke function:

class SomeHandler {
    operator fun invoke(): String = "Some String"
    operator fun invoke(value: String): String = "Some String with String value: $value"
    operator fun invoke(value: Int): String = "Some String with Int value: $value"
}

fun main(args: Array<String>) { 
  val handler = SomeHandler()

  handler()        // Some String
  handler("one")   // Some String with String value: one
  handler(1)       // Some String with Int value: 1
}

As we can see, with this approach we don’t have to specify the function name anymore. When dealing with classes, which truly have a single responsibility and a descriptive name, this becomes an interesting syntactic sugar.

Of course, when overriding invoke() we can implement functions taking multiple arguments, as well.

4. Unary Operators

Nextly, let’s take a look at unary operators:

  • +someVariable -> someVariable.unaryPlus()
  • -someVariable -> someVariable.unaryMinus()
  • !someVariable -> someVariable.not()
  • ++someVariable / someVariable++ -> someVariable.inc()
  • -- someVariable / someVariable-- -> someVariable.dec()

Note: when incrementing/decrementing we have to remember about the way postfix and prefix approach differ. In a moment, I will show you an example.

So, let’s see an example class with the first 3 operators:

data class SomeText(
    val value: String
) {
    operator fun unaryPlus(): SomeText =
        SomeText(this.value.uppercase())

    operator fun unaryMinus(): SomeText =
        SomeText(this.value.lowercase())

    operator fun not(): SomeText =
        SomeText(this.value.reversed())
}

fun main(args: Array<String>) { 
  val someText = SomeText("This is My text")

  +someText // SomeText(value=THIS IS MY TEXT)
  -someText // SomeText(value=this is my text)
  !someText // SomeText(value=txet yM si sihT)
}

This time, for each unary operator, we simply return a new instance with an appropriately modified value.

When it comes to the inc() and dec(), the class implementation looks, as follows:

data class CustomPoint(
    val x: Int,
    val y: Int
) {
  operator fun inc(): CustomPoint =
    CustomPoint(this.x + 1, this.y + 1)

  operator fun dec(): CustomPoint =
    CustomPoint(this.x - 1, this.y - 1)
}

And although this looks just like a previous example, this time we have to return instances o CustomPoint in both cases.

Additionally, there’s a bit of a “magic” happening depending on whether we decide to use a suffix or prefix form:

fun main(args: Array<String>) { 
  var point = CustomPoint(0, 0)

  ++point    // CustomPoint(x=1, y=1)
  point++    // CustomPoint(x=1, y=1)
  point      // CustomPoint(x=2, y=2)
  --point    // CustomPoint(x=1, y=1)
  point--    // CustomPoint(x=1, y=1)
  point      // CustomPoint(x=0, y=0)
}

So basically:

  • when using the prefix form, the inc()/dec() function is invoked first and then the result is simply returned,
  • however, when using the suffix form, then the result is firstly stored in temporary storage, returned as a result of the expression, and assigned to the initial variable.

5. Binary Operators

Following, let’s learn operator overloading with Kotlin binary operators.

As an example, let’s learn how do the comparison work:

  • x < y -> x.compareTo(y) < 0
  • x > y -> x.compareTo(y) > 0
  • x <= y -> x.compareTo(y) <= 0
  • x >= y -> x.compareTo(y) >= 0

So, our previously implemented class- SomeText– can be a great candidate for that:

data class SomeText(
    val value: String
) {
  operator fun compareTo(other: SomeText): Int {
    val thisLength = this.value.length
    val otherLength = other.value.length

    return thisLength - otherLength
  }
}

fun main(args: Array<String>) { 
  val textA = SomeText("123")
  val textB = SomeText("456")

  textA > textB  // false
  textA < textB // false 
  textA >= textB // true
  textA <= textB // true
}

As we can see, with such a simple approach the code become much more readable and intuitive.

Of course, there are much more binary (and not only) Kotlin operators, which we can use for operator overloading and I will add a link to the documentation at the end of this article.

6. Infix Notation

And although the infix notation is technically not a part of operator overloading, I feel obliged to mention it.

Basically, the infix notation is nothing else than the possibility to implement custom infix operations. And even though they do not bring as much, as the operators overloading, I still believe they are a pretty good syntactic sugar.

Let’s take a look at the following code:

data class SomeText(
    val value: String
) {
  infix fun codersee(other: SomeText): SomeText =
    SomeText("Codersee ${this.value}, ${other.value}")

  infix fun codersee(other: String): String =
    "Codersee ${this.value}, $other"
}

fun main(args: Array<String>) { 
  val anotherText = SomeText("another")
  
  anotherText codersee SomeText("value")   // SomeText(value=Codersee another, value)
  anotherText codersee "value"             // "Codersee another, value"
}

This time, we can see that instead of invoking the anotherText.codersee(), we can simply use an infix notation.

Nevertheless, we have to keep in mind some limitations:

  • they must have only one parameter,
  • and the parameter itself cannot accept a variable number of arguments and or have a default value.

7. Kotlin Operator Overloading Summary

And that’s all for this article about Kotlin operator overloading and what this feature brings to the table. As promised, right here you can find the documentation, where you can learn more about this functionality.

Lastly, if you enjoyed this article, then you might want to check out my other Kotlin articles.

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy