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
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
Operator | Function Name | Description |
---|---|---|
+ | plus | Addition |
- | minus | Subtraction |
* | times | Multiplication |
/ | div | Division |
% | rem | Remainder |
Example: Implementing Arithmetic Operators for a Vector Class
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
Operator | Function Name | Description |
---|---|---|
+a | unaryPlus | Unary plus |
-a | unaryMinus | Unary minus |
!a | not | Logical NOT |
++a , a++ | inc | Increment |
--a , a-- | dec | Decrement |
Example: Implementing Unary Operators
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
Operator | Function Name | Description |
---|---|---|
a == b | equals | Equality (No need to mark with operator ) |
a > b | compareTo | Greater than |
a < b | compareTo | Less than |
a >= b | compareTo | Greater than or equal to |
a <= b | compareTo | Less than or equal to |
Example: Implementing Comparison Operators
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
Operator | Function Name | Description |
---|---|---|
a[i] | get | Access by index |
a[i] = b | set | Set by index |
a() | invoke | Function invocation |
Example: Implementing Indexing and Invocation Operators
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:
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:
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
- Keep operations intuitive: Only overload operators when the meaning is clear and intuitive.
- Maintain consistency: Your operator implementations should behave in ways users would expect.
- Don't abuse: Just because you can overload operators doesn't mean you should do it everywhere.
- Consider performance: Some operators might be called frequently in loops, so keep implementations efficient.
Common Mistakes to Avoid
- Forgetting the
operator
keyword - Overloading operators that make the code less readable
- Implementing operators with unexpected behavior
- 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
-
Complex Number Implementation: Create a
Complex
class representing complex numbers and implement arithmetic operators for addition, subtraction, multiplication, and division. -
Custom Collection: Develop a simple collection class that overloads the indexing operators and implements the
Iterable
interface. -
Time Duration: Implement a
Duration
class with operators that allow for adding and subtracting durations, as well as multiplying durations by scalars. -
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! :)