Skip to main content

Kotlin Operator Functions

Introduction

Operator functions are a powerful feature in Kotlin that enables operator overloading, allowing you to define how operators like +, -, *, and others work with your custom classes. This makes your code more readable and intuitive by enabling you to use familiar mathematical and other symbolic operations with your own types.

Instead of writing something like point1.add(point2), you can write point1 + point2, which is more natural and expressive. In this tutorial, we'll explore how to implement and use operator functions in Kotlin.

Understanding Operator Functions

In Kotlin, operator overloading is not done by special operator overloading syntax as in some other languages. Instead, specific named functions marked with the operator modifier correspond to specific operators.

Basic Syntax

kotlin
operator fun functionName(parameters): ReturnType {
// Implementation
}

The operator keyword tells the Kotlin compiler that this function can be used through its corresponding symbolic operator.

Common Operator Functions

Let's explore some of the most commonly used operator functions in Kotlin:

Arithmetic Operators

OperatorFunction NameDescription
+plusAddition
-minusSubtraction
*timesMultiplication
/divDivision
%remRemainder

Example: Implementing Arithmetic Operators for a Vector Class

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

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

operator fun times(scalar: Double): Vector2D {
return Vector2D(x * scalar, y * scalar)
}

operator fun div(scalar: Double): Vector2D {
return Vector2D(x / scalar, y / scalar)
}
}

fun main() {
val v1 = Vector2D(3.0, 4.0)
val v2 = Vector2D(1.0, 2.0)

val sum = v1 + v2
println("v1 + v2 = Vector2D(${sum.x}, ${sum.y})") // Output: v1 + v2 = Vector2D(4.0, 6.0)

val diff = v1 - v2
println("v1 - v2 = Vector2D(${diff.x}, ${diff.y})") // Output: v1 - v2 = Vector2D(2.0, 2.0)

val scaled = v1 * 2.0
println("v1 * 2.0 = Vector2D(${scaled.x}, ${scaled.y})") // Output: v1 * 2.0 = Vector2D(6.0, 8.0)

val divided = v1 / 2.0
println("v1 / 2.0 = Vector2D(${divided.x}, ${divided.y})") // Output: v1 / 2.0 = Vector2D(1.5, 2.0)
}

Unary Operators

OperatorFunction NameDescription
+aunaryPlusUnary plus
-aunaryMinusUnary minus
!anotLogical NOT
++a, a++incIncrement
--a, a--decDecrement

Example: Implementing Unary Operators

kotlin
data class Counter(var value: Int) {
operator fun unaryMinus(): Counter {
return Counter(-value)
}

operator fun inc(): Counter {
return Counter(value + 1)
}

operator fun dec(): Counter {
return Counter(value - 1)
}
}

fun main() {
var counter = Counter(5)

val negated = -counter
println("Negated counter: ${negated.value}") // Output: Negated counter: -5

val incremented = counter++
println("After increment: ${counter.value}") // Output: After increment: 6
println("Incremented value: ${incremented.value}") // Output: Incremented value: 5

val preIncremented = ++counter
println("After pre-increment: ${counter.value}") // Output: After pre-increment: 7
println("Pre-incremented value: ${preIncremented.value}") // Output: Pre-incremented value: 7
}

Comparison Operators

OperatorFunction NameDescription
a == bequalsEquality (No need to mark with operator)
a > bcompareToGreater than
a < bcompareToLess than
a >= bcompareToGreater than or equal to
a <= bcompareToLess than or equal to

Example: Implementing Comparison Operators

kotlin
data class Product(val name: String, val price: Double) : Comparable<Product> {
override operator fun compareTo(other: Product): Int {
return price.compareTo(other.price)
}
}

fun main() {
val product1 = Product("Laptop", 1200.0)
val product2 = Product("Smartphone", 800.0)

println("product1 > product2: ${product1 > product2}") // Output: product1 > product2: true
println("product1 < product2: ${product1 < product2}") // Output: product1 < product2: false
println("product1 >= product2: ${product1 >= product2}") // Output: product1 >= product2: true
}

Indexing and Invocation Operators

OperatorFunction NameDescription
a[i]getAccess by index
a[i] = bsetSet by index
a()invokeFunction invocation

Example: Implementing Indexing and Invocation Operators

kotlin
class Matrix(private val data: Array<Array<Int>>) {
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
}
}

class Calculator {
operator fun invoke(a: Int, b: Int): Int {
return a + b
}
}

fun main() {
val matrix = Matrix(arrayOf(
arrayOf(1, 2, 3),
arrayOf(4, 5, 6),
arrayOf(7, 8, 9)
))

println("Element at (1,2): ${matrix[1, 2]}") // Output: Element at (1,2): 6

matrix[0, 0] = 99
println("Element at (0,0) after modification: ${matrix[0, 0]}") // Output: Element at (0,0) after modification: 99

val calculator = Calculator()
val result = calculator(10, 20)
println("Calculator result: $result") // Output: Calculator result: 30
}

Real-World Applications

Example 1: Money Operations

Consider a real-world scenario of handling monetary calculations:

kotlin
data class Money(val amount: BigDecimal, val currency: String) {
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(multiplier: Int): Money {
return Money(amount * BigDecimal(multiplier), currency)
}

operator fun div(divisor: Int): Money {
return Money(amount.divide(BigDecimal(divisor), 2, RoundingMode.HALF_EVEN), currency)
}
}

fun main() {
val wallet = Money(BigDecimal("125.50"), "USD")
val expense = Money(BigDecimal("25.75"), "USD")

val remaining = wallet - expense
println("Money remaining: ${remaining.amount} ${remaining.currency}") // Output: Money remaining: 99.75 USD

val doubledSalary = wallet * 2
println("Doubled salary: ${doubledSalary.amount} ${doubledSalary.currency}") // Output: Doubled salary: 251.00 USD

val sharedExpense = expense / 3
println("Shared expense: ${sharedExpense.amount} ${sharedExpense.currency}") // Output: Shared expense: 8.58 USD
}

Example 2: Date Range Iteration

Using operator functions to create a custom date range iterator:

kotlin
import java.time.LocalDate

data class DateRange(val start: LocalDate, val end: LocalDate) : Iterable<LocalDate> {
override fun iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> {
var current = start

override fun hasNext() = !current.isAfter(end)

override fun next(): LocalDate {
val date = current
current = current.plusDays(1)
return date
}
}

operator fun contains(date: LocalDate): Boolean {
return !date.isBefore(start) && !date.isAfter(end)
}
}

fun main() {
val dateRange = DateRange(
LocalDate.of(2023, 5, 1),
LocalDate.of(2023, 5, 5)
)

for (date in dateRange) {
println(date)
}
// Output:
// 2023-05-01
// 2023-05-02
// 2023-05-03
// 2023-05-04
// 2023-05-05

val checkDate = LocalDate.of(2023, 5, 3)
println("Is $checkDate in range? ${checkDate in dateRange}") // Output: Is 2023-05-03 in range? true

val outsideDate = LocalDate.of(2023, 6, 1)
println("Is $outsideDate in range? ${outsideDate in dateRange}") // Output: Is 2023-06-01 in range? false
}

Best Practices for Using Operator Functions

  1. Keep operations intuitive: Only overload operators when the meaning is clear and intuitive.
  2. Maintain consistency: Your operator implementations should behave in ways users would expect.
  3. Don't abuse: Just because you can overload operators doesn't mean you should do it everywhere.
  4. Consider performance: Some operators might be called frequently in loops, so keep implementations efficient.

Common Mistakes to Avoid

  1. Forgetting the operator keyword
  2. Overloading operators that make the code less readable
  3. Implementing operators with unexpected behavior
  4. Using operators for operations that would be more clear as named methods

Summary

Kotlin operator functions provide a powerful way to make your code more expressive and readable by allowing you to use standard operators with your custom classes. By implementing specific named functions marked with the operator keyword, you can define how operators like +, -, * work with your types.

We've explored various categories of operator functions, including:

  • Arithmetic operators (plus, minus, times, etc.)
  • Unary operators (unaryPlus, unaryMinus, inc, etc.)
  • Comparison operators (through compareTo)
  • Indexing and invocation operators (get, set, invoke)

With these tools, you can write more intuitive and expressive code that mimics natural mathematical or logical operations.

Additional Resources and Exercises

Resources

Exercises

  1. Complex Number Implementation: Create a Complex class representing complex numbers and implement arithmetic operators for addition, subtraction, multiplication, and division.

  2. Custom Collection: Develop a simple collection class that overloads the indexing operators and implements the Iterable interface.

  3. Time Duration: Implement a Duration class with operators that allow for adding and subtracting durations, as well as multiplying durations by scalars.

  4. Enhanced Matrix: Extend the Matrix example from this tutorial to support matrix addition, subtraction, and multiplication using operator overloading.



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