Understanding Operator Overloading in Kotlin: A Comprehensive Guide

Categories:
5 minute read
Operator overloading is a powerful feature in Kotlin that allows you to provide implementations for a predefined set of operators on your types. This guide explores how to effectively use operator overloading, its conventions, and best practices.
Introduction to Operator Overloading
In Kotlin, operators are represented by specific functions marked with the operator
modifier. Each operator corresponds to a function name following predefined conventions.
Basic Syntax
data class Vector2D(val x: Double, val y: Double) {
operator fun plus(other: Vector2D): Vector2D {
return Vector2D(x + other.x, y + other.y)
}
}
// Usage
val v1 = Vector2D(1.0, 2.0)
val v2 = Vector2D(3.0, 4.0)
val sum = v1 + v2 // Calls v1.plus(v2)
Arithmetic Operators
Binary Operators
class ComplexNumber(val real: Double, val imaginary: Double) {
// Addition (+)
operator fun plus(other: ComplexNumber): ComplexNumber {
return ComplexNumber(real + other.real, imaginary + other.imaginary)
}
// Subtraction (-)
operator fun minus(other: ComplexNumber): ComplexNumber {
return ComplexNumber(real - other.real, imaginary - other.imaginary)
}
// Multiplication (*)
operator fun times(other: ComplexNumber): ComplexNumber {
return ComplexNumber(
real * other.real - imaginary * other.imaginary,
real * other.imaginary + imaginary * other.real
)
}
// Division (/)
operator fun div(other: ComplexNumber): ComplexNumber {
val denominator = other.real * other.real + other.imaginary * other.imaginary
return ComplexNumber(
(real * other.real + imaginary * other.imaginary) / denominator,
(imaginary * other.real - real * other.imaginary) / denominator
)
}
}
Unary Operators
data class Temperature(var celsius: Double) {
// Unary minus (-)
operator fun unaryMinus(): Temperature {
return Temperature(-celsius)
}
// Increment (++)
operator fun inc(): Temperature {
return Temperature(celsius + 1.0)
}
// Decrement (--)
operator fun dec(): Temperature {
return Temperature(celsius - 1.0)
}
}
// Usage
var temp = Temperature(23.0)
val negated = -temp // unaryMinus()
val increased = ++temp // inc()
val decreased = --temp // dec()
Comparison Operators
Equality and Comparison
data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> {
// Implement compareTo for all comparison operators (<, >, <=, >=)
override operator fun compareTo(other: Version): Int {
return when {
major != other.major -> major - other.major
minor != other.minor -> minor - other.minor
else -> patch - other.patch
}
}
// equals() is automatically generated by data class
// hashCode() is automatically generated by data class
}
// Usage
val v1 = Version(1, 0, 0)
val v2 = Version(2, 0, 0)
println(v1 < v2) // true
println(v1 >= v2) // false
Index Operators
Array-like Access
class Matrix(private val data: Array<Array<Double>>) {
// Get value operator []
operator fun get(row: Int, col: Int): Double {
return data[row][col]
}
// Set value operator []=
operator fun set(row: Int, col: Int, value: Double) {
data[row][col] = value
}
}
// Usage
val matrix = Matrix(Array(3) { Array(3) { 0.0 } })
matrix[0, 0] = 1.0 // set()
val value = matrix[0, 0] // get()
Function Call Operator
Invoke Operator
class Multiplier(private val factor: Int) {
operator fun invoke(x: Int): Int {
return x * factor
}
}
// Usage
val double = Multiplier(2)
val result = double(4) // Returns 8
Collection Operators
Contains and Iterator
class DateRange(val start: Date, val end: Date) {
// Contains operator (in)
operator fun contains(date: Date): Boolean {
return date >= start && date <= end
}
// Iterator operator (for-in loop)
operator fun iterator(): Iterator<Date> {
return object : Iterator<Date> {
private var current = start
override fun hasNext(): Boolean = current <= end
override fun next(): Date {
val result = current
current = Date(current.time + 86400000) // Add one day
return result
}
}
}
}
// Usage
val range = DateRange(startDate, endDate)
if (someDate in range) { // contains()
println("Date is in range")
}
for (date in range) { // iterator()
println(date)
}
Property Delegation Operators
getValue and setValue
class ObservableProperty<T>(private var value: T) {
private val observers = mutableListOf<(T) -> Unit>()
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
observers.forEach { it(newValue) }
}
fun addObserver(observer: (T) -> Unit) {
observers.add(observer)
}
}
// Usage
class User {
var name: String by ObservableProperty("") {
addObserver { println("Name changed to: $it") }
}
}
Best Practices
1. Maintain Expected Behavior
data class Money(val amount: BigDecimal, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) {
"Cannot add money with different currencies"
}
return Money(amount + other.amount, currency)
}
}
2. Preserve Operator Properties
class Counter(var value: Int) {
operator fun plus(other: Counter): Counter {
return Counter(value + other.value)
}
// Commutative property: a + b == b + a
operator fun plus(other: Int): Counter {
return Counter(value + other)
}
}
// Extension function for commutative property
operator fun Int.plus(counter: Counter): Counter {
return counter + this
}
3. Handle Edge Cases
data class SafeNumber(val value: Double) {
operator fun div(other: SafeNumber): SafeNumber {
if (other.value == 0.0) {
throw ArithmeticException("Division by zero")
}
return SafeNumber(value / other.value)
}
}
Common Patterns and Examples
1. Builder Pattern with Operators
class QueryBuilder {
private val conditions = mutableListOf<String>()
operator fun plus(condition: String): QueryBuilder {
conditions.add(condition)
return this
}
fun build(): String = conditions.joinToString(" AND ")
}
// Usage
val query = QueryBuilder() + "age > 18" + "name LIKE 'John%'"
2. Resource Management
class Resource(val name: String) {
operator fun plusAssign(other: Resource) {
// Combine resources
}
operator fun minusAssign(other: Resource) {
// Release resource
}
}
Conclusion
Operator overloading in Kotlin provides a powerful way to make your code more expressive and intuitive. Key points to remember:
- Use operator overloading when it makes the code more readable and intuitive
- Follow mathematical and logical conventions
- Handle edge cases and errors appropriately
- Maintain consistency with standard library operators
- Document any non-obvious behavior
When used appropriately, operator overloading can significantly improve code readability and maintainability. However, it should be used judiciously to avoid confusion and maintain code clarity.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.