Open Source Your Knowledge, Become a Contributor
Technology knowledge has to be shared and made accessible for free. Join the movement.
Introduction
Kotlin documentation said that if you define an operator
function plus
, you can use +
, by convention, to call
that function.
Conventions are not just for operators, you'll find them in Collections, Ranges, Destructuring Declaration and Invocation.
Let's see the details !!!
Operator overloading
Kotlin allows us to overload some operators on any object we have created, or that we know of (through extensions).
The concept of operator overloading provides a way to invoke functions to perform arithmetic
operation, equality checks or comparison on whatever object we want, through symbols like +
, -
, /
, *
, %
,
<
, >
. But, obviously, those overloading should be defined when it make sense to use them.
For the following parts, let's assume we have the data class
:
data class Point(val x: Double, val y: Double)
Arithmetic operators
To overload the +
operator we need to implement the function plus
, with the keyword operator
. This function
takes one parameter of any kind, even it make sense in most cases to use the same type.
// Here how to provide `+` operator on our object Point
operator fun plus(p: Point) = Point(this.x + p.x, this.y + p.x)
// return type is inferred to Point
To go further we can apply all the following operator overloading on the object Point
.
expression | function called |
---|---|
p1 + p2 | p1.plus (p2) |
p1 - p2 | p1.minus (p2) |
p1 * p2 | p1.times (p2) |
p1 / p2 | p1.div (p2) |
p1 % p2 | p1.rem (p2) |
p1++ | p1.inc () |
p1-- | p1.dec () |
Here the implementation on our previous data class
:
Note that those examples are quiet simple, you may be able to implement more complex operator, depending on your own object's definition.
Let's practice a little
Assuming the following data class
:
data class Fraction(val numerator: Int, val denominator: Int)
Show the answer
operator fun Fraction.plus(add: Fraction): Fraction =
if (this.denominator == add.denominator) {
Fraction(this.numerator + add.numerator, denominator)
} else {
Fraction(numerator * add.denominator + add.numerator * denominator,
denominator * add.denominator)
}
Show the answer
operator fun Fraction.times(num: Int) = Fraction(numerator * num, denominator)
Equality and inequality
As a Java developer, I always felt confused about equality, sometimes you have to use ==
/ !=
(on primitives),
sometimes you have to use equals()
. (reminder, the usage of ==
/ !=
on non-primitive checks the reference of the
object not its value).
Kotlin makes it more simple by reserving the symbols ==
and !=
to check the objects' values (to check
references you may use ===
/ !==
).
To overload the equality (and inequality) checks, you may override the well known equals()
function.
override fun equals(other: Any?): Boolean {
if (other == null ||
other !is Point ||
x != other.x || y != other.y) return false
return true
}
Exception: As you may know, in Kotlin objects can be non-null. In that case,
x == null
will always befalse
, andequals
will never be called.
Tips: As you may know, in Kotlin, data class already implements
equals()
, as other useful functions (getters/setters
,hashCode()
,copy()
andtoString()
)
Let's practice a little
Show the answer
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EqFraction
return decimal == other.decimal
}
NB: As a good practice, when you override
equals()
function, you should overridehashCode()
too
Comparison
Comparison, <
, >
, <=
and >=
, are all based on one function, compareTo()
. This function returns an Int
,
that define if the left-side of an expression is greater
, smaller
or equals
to the right-side of that
same expression.
Expression | function called |
---|---|
left > right | left.compareTo(right) > 0 |
left < right | left.compareTo(right) < 0 |
left >= right | left.compareTo(right) >= 0 |
left <= right | left.compareTo(right) <= 0 |
Here is an examples on how we could compare Point
s:
You just saw how to use an operator to implement comparison between two instances of your object. But, there is
another way to implement such mechanism, you could implement the Comparable
interface, and overrides its
compareTo
method, it would have the same result.
data class Point(val x: Double, val y: Double) : Comparable<Point> {
override fun compareTo(other: Point): Int = when {
y != other.y -> (y - other.y).toInt()
else -> (x - other.x).toInt()
}
}
Let's practice a little
Show the answer
override fun compareTo(other: ComparableFraction) = decimal.compareTo(other.decimal)
Collections
Kotlin's Collections bring two type of conventions, the interaction with a specific data by using getter
and
setter
with indexes, and the ability to check if an object belongs to a given list through the keyword in
.
get
/set
You can implement get
and set
operators and then use square brackets to interact with your objects.
Expression | Function called |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
in
/!in
in
is an operator that calls the function contains()
, that you may implement to check if a value if part of your
object. Obviously, !in
will call !contains()
. You'll be glad to know that Collection
have already an
implementation of the contains
function, so you could already check if 1
is part of the listOf(1, 2, 3)
(hint:
this return true
^_^).
Here is an example implementing operators for collections
Let's practice a little
Show the answer
operator fun ComparableFraction.get(index: Int) =
when (index) {
0 -> numerator
1 -> denominator
else -> IllegalArgumentException("Index must be 0 or 1")
}
Ranges
Ranges are part of the nice features that make you love Kotlin. It's no magic, there is (nearly) no complexity, but
when you
think about it, it just make sense. To build ranges you can use the operator ..
, translated to rangeTo()
.
The Kotlin standard library provides some extra features around type that we know. So integrals have their own
implementation of Ranges, like IntRange
(as LongRange
and CharRange
), that allow you to write 1..10
that will
build a collection of value from 1 to 10 included, by calling the following snippet 1.rangeTo(10)
.
Custom ranges
Let's get back to our Point
class.
data class Point(val x: Double, val y: Double) : Comparable<Point> {
override fun compareTo(other: Point): Int = when {
y != other.y -> (y - other.y).toInt()
else -> (x - other.x).toInt()
}
}
Kotlin provides an extension rangeTo()
on Comparable<T>
interface, so we don't need to overload it for our Point
class.
Iterating over ranges
To go further we may implement an operator to iterate over Point
ranges, the iterator()
:
operator fun ClosedRange<Point>.iterator() = object : Iterator<Point> {
var currentPoint = start
override fun hasNext() = currentPoint <= endInclusive
override fun next() = currentPoint++
}
By doing that, we can now iterate over a Point
collection:
Let's practice a little
Show the answer
operator fun ClosedRange<RangeFraction>.iterator() = object : Iterator<RangeFraction> {
var currentFraction: RangeFraction = start
override fun hasNext() = currentFraction <= endInclusive
override fun next() = currentFraction++
}
Destructuring Declaration
Many times, you certainly had to decompose objects to play with:
val name = person.name
val age = person.age
val address = person.address
// and so on
With Kotlin, you are able to decompose your data class
, Map(s)
, and own object in a simple way.
data class
data class
get rid of a lot of boilerplate code (getter/setter
, equals
, hashCode
, toString
, copy
...), but it
also brings the ability to decompose our object according to the properties we have defined.
Kotlin create a component
operator for each of those properties.
Let's take this data class as
example
data class Person(val name: String, val age: Int, val address: String)
The compiler will automatically provide componentN()
for name
(component1), age
(component2) and address
(component3). Give you the power to destructure the Person
object into variables.
data class Person(val name: String, val age: Int, val address: String) {
// behind the scene
operator fun component1() = this.name
operator fun component2() = this.age
operator fun component3() = this.address
}
Usage
tips: if you don't need some elements you can omit them by using an underscore
_
Map(s)
Map implements iterator()
, component1()
for key, and component2()
for value. That help us to write nice
more convenient iteration like:
Let's practice a little
Show the answer
val (name, brand, size, price) = product
Summary
Here is a reminder of what you can do
by convention
Expression | Invoke |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) , a.mod(b) (deprecated) |
a > b | a.compareTo(b) |
a++ | a.inc() |
b-- | b.dec() |
a == b | a.equals(b) |
a != b | !a.equals(b) |
a < b | a.compareTo(b) |
a >= b | a.compareTo(b) |
a <= b | a.compareTo(b) |
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a..b | a.rangeTo(b) |
Go further with the full documentation !
Thanks to Simon Wirtz for his inspiring article !