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
:
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
:
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:
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:
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 ++
:
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:
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:
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:
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:
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:
// 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
-
Maintain intuitive semantics: Only overload operators when it makes logical sense. For example, use
+
for addition-like operations, not for unrelated actions. -
Ensure consistent behavior: Your operator implementations should follow expected mathematical properties when applicable (e.g., commutativity, associativity).
-
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.
-
Be cautious with comparison operators: When implementing comparison operators, ensure they establish a total order to avoid inconsistent results.
-
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
-
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. -
Implement a
Fraction
class with operators for addition, subtraction, multiplication, and division of fractions. -
Create a
Temperature
class that supports different units (Celsius, Fahrenheit) and implement conversion operators. -
Design a
StringBuilderEx
class that extends the functionality ofStringBuilder
with operator overloading, allowing you to use+
to append strings.
Additional Resources
- Kotlin Official Documentation on Operator Overloading
- Kotlin Operator Functions Reference
- Book: "Kotlin in Action" by Dmitry Jemerov and Svetlana Isakova (Chapter on operator overloading)
- Book: "Programming Kotlin" by Venkat Subramaniam (Chapter on operator overloading and DSLs)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)