Skip to main content

Kotlin Operator Overloading

Introduction

Operator overloading is a powerful feature in Kotlin that allows you to provide custom implementations for standard operators like +, -, *, etc. Instead of calling methods with names like add() or multiply(), you can use familiar mathematical symbols that make your code more concise and readable.

In many programming languages, operators are reserved for built-in types. However, Kotlin extends this capability to user-defined classes, allowing you to write more expressive and intuitive code. This is especially useful when working with custom data types where mathematical operations make conceptual sense.

Understanding Operator Overloading

In Kotlin, operators correspond to specific functions with predefined names. When you use an operator, Kotlin translates it into a call to the corresponding function. To overload an operator, you simply define the corresponding function using the operator keyword.

Let's explore how this works in practice.

Basic Arithmetic Operators

The Plus (+) Operator

To overload the + operator, you define a function named plus:

kotlin
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}

fun main() {
val p1 = Point(10, 20)
val p2 = Point(30, 40)
val p3 = p1 + p2 // Calls p1.plus(p2)

println(p3) // Output: Point(x=40, y=60)
}

In this example, we've created a Point class that represents a 2D point with x and y coordinates. By implementing the plus function with the operator modifier, we can use the + operator to add two points together.

The Minus (-) Operator

Similarly, we can overload the - operator by defining a function named minus:

kotlin
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}

operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
}

fun main() {
val p1 = Point(50, 60)
val p2 = Point(20, 30)
val p3 = p1 - p2 // Calls p1.minus(p2)

println(p3) // Output: Point(x=30, y=30)
}

Multiplication (*) and Division (/) Operators

Let's extend our Point class to support multiplication and division operations:

kotlin
data class Point(val x: Int, val y: Int) {
// ... previous operators

operator fun times(scale: Int): Point {
return Point(x * scale, y * scale)
}

operator fun div(divisor: Int): Point {
return Point(x / divisor, y / divisor)
}
}

fun main() {
val p1 = Point(10, 20)

// Multiply a point by a scalar
val p2 = p1 * 3 // Calls p1.times(3)
println(p2) // Output: Point(x=30, y=60)

// Divide a point by a scalar
val p3 = p2 / 2 // Calls p2.div(2)
println(p3) // Output: Point(x=15, y=30)
}

Comparison Operators

Kotlin allows you to overload comparison operators as well. Let's implement comparison operators for our Point class:

kotlin
data class Point(val x: Int, val y: Int) {
// ... previous operators

operator fun compareTo(other: Point): Int {
// Compare based on the sum of coordinates
val thisSum = this.x + this.y
val otherSum = other.x + other.y
return thisSum.compareTo(otherSum)
}
}

fun main() {
val p1 = Point(10, 20) // Sum: 30
val p2 = Point(15, 25) // Sum: 40
val p3 = Point(5, 25) // Sum: 30

println(p1 < p2) // true (30 < 40)
println(p1 <= p3) // true (30 <= 30)
println(p1 >= p2) // false (30 >= 40)
println(p3 > p2) // false (30 > 40)
println(p1 == p3) // false (equality compares x and y individually)
}

Note that the == operator in Kotlin translates to the equals() method, which is automatically generated for data classes to compare all properties.

Unary Operators

Kotlin also supports unary operators like +, -, !, and ++:

kotlin
data class Point(var x: Int, var y: Int) {
// ... previous operators

operator fun unaryMinus(): Point {
return Point(-x, -y)
}

operator fun inc(): Point {
return Point(x + 1, y + 1)
}
}

fun main() {
val p1 = Point(10, 20)

// Unary minus
val p2 = -p1 // Calls p1.unaryMinus()
println(p2) // Output: Point(x=-10, y=-20)

// Increment
var p3 = Point(5, 5)
val p4 = p3++ // Calls p3.inc() and assigns the original value to p4

println(p3) // Output: Point(x=6, y=6)
println(p4) // Output: Point(x=5, y=5)
}

Indexing Operators

Kotlin allows you to implement indexing operators for your classes, which makes them behave like arrays or maps:

kotlin
class Matrix(private val size: Int) {
private val data = Array(size) { IntArray(size) { 0 } }

operator fun get(row: Int, col: Int): Int {
return data[row][col]
}

operator fun set(row: Int, col: Int, value: Int) {
data[row][col] = value
}

override fun toString(): String {
return data.joinToString("\n") { it.joinToString(" ") }
}
}

fun main() {
val matrix = Matrix(3)

// Using the set operator
matrix[0, 0] = 1 // Calls matrix.set(0, 0, 1)
matrix[0, 1] = 2
matrix[1, 1] = 3

// Using the get operator
println("Element at (0,1): ${matrix[0, 1]}") // Calls matrix.get(0, 1)

// Print the whole matrix
println("Matrix:")
println(matrix)
}

Output:

Element at (0,1): 2
Matrix:
1 2 0
0 3 0
0 0 0

Contains Operator (in)

You can also overload the in operator by implementing the contains function:

kotlin
data class Range(val start: Int, val end: Int) {
operator fun contains(value: Int): Boolean {
return value in start..end
}
}

fun main() {
val range = Range(10, 20)

println(5 in range) // Calls range.contains(5) - Output: false
println(15 in range) // Calls range.contains(15) - Output: true
println(25 in range) // Calls range.contains(25) - Output: false
}

Invoke Operator

The invoke operator (()) allows you to call your object as if it were a function:

kotlin
class Greeter(val greeting: String) {
operator fun invoke(name: String) {
println("$greeting, $name!")
}
}

fun main() {
val hello = Greeter("Hello")
hello("Alice") // Calls hello.invoke("Alice") - Output: Hello, Alice!

val hi = Greeter("Hi")
hi("Bob") // Calls hi.invoke("Bob") - Output: Hi, Bob!
}

Real-World Example: Money Operations

Let's implement a practical Money class with operator overloading to handle currency operations:

kotlin
data class Money(val amount: Double, val currency: String) {
init {
require(amount >= 0) { "Amount can't be negative" }
}

operator fun plus(other: Money): Money {
require(currency == other.currency) { "Cannot add different currencies" }
return Money(amount + other.amount, currency)
}

operator fun minus(other: Money): Money {
require(currency == other.currency) { "Cannot subtract different currencies" }
return Money(amount - other.amount, currency)
}

operator fun times(percentage: Double): Money {
return Money(amount * percentage, currency)
}

operator fun div(divisor: Int): Money {
return Money(amount / divisor, currency)
}

operator fun compareTo(other: Money): Int {
require(currency == other.currency) { "Cannot compare different currencies" }
return amount.compareTo(other.amount)
}

override fun toString(): String = "%.2f %s".format(amount, currency)
}

fun main() {
// Shopping cart example
val shirt = Money(29.99, "USD")
val pants = Money(59.95, "USD")

// Calculate total
val totalCost = shirt + pants
println("Total cost: $totalCost") // Output: Total cost: 89.94 USD

// Apply 15% discount
val discount = totalCost * 0.15
println("Discount: $discount") // Output: Discount: 13.49 USD

// Final price after discount
val finalPrice = totalCost - discount
println("Final price: $finalPrice") // Output: Final price: 76.45 USD

// Split payment between 2 people
val splitPayment = finalPrice / 2
println("Split payment: $splitPayment per person") // Output: Split payment: 38.22 USD per person

// Compare prices
println("Is shirt more expensive than pants? ${shirt > pants}") // Output: false
}

This example demonstrates how operator overloading can make financial calculations more intuitive and readable in real-world applications.

Extension Operators

In Kotlin, you can also define operators as extension functions. This is particularly useful when you want to add operators to existing classes that you don't own:

kotlin
// Adding vector operations to the built-in Pair class
operator fun Pair<Int, Int>.plus(other: Pair<Int, Int>): Pair<Int, Int> {
return Pair(first + other.first, second + other.second)
}

operator fun Pair<Int, Int>.minus(other: Pair<Int, Int>): Pair<Int, Int> {
return Pair(first - other.first, second - other.second)
}

operator fun Pair<Int, Int>.times(scalar: Int): Pair<Int, Int> {
return Pair(first * scalar, second * scalar)
}

fun main() {
val v1 = Pair(3, 4)
val v2 = Pair(1, 2)

println(v1 + v2) // Output: (4, 6)
println(v1 - v2) // Output: (2, 2)
println(v1 * 2) // Output: (6, 8)
}

Operator Overloading Best Practices

  1. Maintain intuitive semantics: Only overload operators when it makes logical sense. For example, use + for addition-like operations, not for unrelated actions.

  2. Ensure consistent behavior: Your operator implementations should follow expected mathematical properties when applicable (e.g., commutativity, associativity).

  3. Document your operators: Even though operators make code more readable, it's still important to document what your custom operators do, especially when their behavior might not be immediately obvious.

  4. Be cautious with comparison operators: When implementing comparison operators, ensure they establish a total order to avoid inconsistent results.

  5. Consider performance: Some operators might be called frequently. Ensure your implementations are efficient.

Summary

Operator overloading is a powerful Kotlin feature that allows you to write more expressive and concise code by giving custom meaning to standard operators when applied to user-defined types. In this guide, we've explored:

  • Basic arithmetic operators (+, -, *, /)
  • Comparison operators (>, <, >=, <=)
  • Unary operators (-, ++, --)
  • Indexing operators ([])
  • The contains operator (in)
  • The invoke operator (())
  • Extension operators for existing classes

When used appropriately, operator overloading makes your code more readable and intuitive, especially when working with domain-specific types like vectors, matrices, complex numbers, or financial calculations.

Exercises

  1. Create a Vector3D class representing a 3D vector with x, y, and z components. Implement the basic arithmetic operators and a dot product using the * operator.

  2. Implement a Fraction class with operators for addition, subtraction, multiplication, and division of fractions.

  3. Create a Temperature class that supports different units (Celsius, Fahrenheit) and implement conversion operators.

  4. Design a StringBuilderEx class that extends the functionality of StringBuilder with operator overloading, allowing you to use + to append strings.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)