In the previous item, I warned about the misuse of operator overloading. In this chapter, I would like to show the usefulness of operators for improving readability.
Let's start with a clear example. With operators, we can operate on BigDecimal and BigInteger similarly to regular numbers.
val netPrice = BigDecimal("10")
val tax = BigDecimal("0.23")
val currentBalance = BigDecimal("20")
val newBalance = currentBalance - netPrice * tax
println(newBalance) // 17.70
We can also add duration to time.
val now = ZonedDateTime.now()
val duration = Duration.ofDays(1)
val sameTimeTomorrow = now + duration
You can compare them by using explicit methods:
val newBalance = currentBalance.minus(netPrice.times(tax))
val sameTimeTomorrow = now.plus(duration)
I hope that the value of using operators here is clear.
All classes that are Comparable can also be compared with comparison operators (>, <, >= and <=) or with a range check (value in min..max). This includes big numbers (BigDecimal, BigInteger) and objects used to represent time and duration (Instant, ZonedDateTime, LocalDate, Duration, etc.). This is important because we often operate on these types, and we often need to compare them (I hope it is clear that if you represent money, you should use BigDecimal instead of Double, which might round some numbers and lose precision).
val now = LocalDateTime.now()
val start = LocalDate.parse("2021-10-17").atStartOfDay()
val end = LocalDate.parse("2021-10-21").atStartOfDay()
if(now > start) { /*...*/ }
if(now < end) { /*...*/ }
if(now in start..end) { /*...*/ }
val price = BigDecimal("100.00")
val minPrice = BigDecimal("10.00")
val maxPrice = BigDecimal("1000.00")
if(price > minPrice) { /*...*/ }
if(price < maxPrice) { /*...*/ }
if(price in minPrice..maxPrice) { /*...*/ }
The above code is an alternative to using the following methods:
Although isAfter and isBefore might be more readable than comparison operators, I hope it seems clear that operators are clearly easier to understand in other cases.
It is worth noticing that there is an inconsistency between the BigDecimal functions equals and compareTo. The equals function checks the number of decimal places, so BigDecimal("1.0") is not equal to BigDecimal("1.00"). This is something you should consider when you compare two numbers. This is one of the reasons why we tend to use BigDecimal numbers with the same precision across a whole project. The compareTo function does not look at the precision, so it is possible that A >= B, A <= B, but A != B (so the contract of compareTo is violated, as explained in Item 44: Respect the contract of compareTo).
The last typical case in which I introduce an operator is when we need to check if an element is in a collection or a set. The classic way to do this is using contains, but we could also use the in operator.
val SUPPORTED_TAGS = setOf("ADMIN", "TRAINER", "ATTENDEE")
val tag = "ATTENDEE"
println(SUPPORTED_TAGS.contains(tag)) // true
// or
println(tag in SUPPORTED_TAGS) // true
Compare the above approaches and consider which is more readable. For me and the colleagues I discussed this matter with, using in does not always increase readability. It all depends on what the active element is. Here, tag is active, and putting it up-front makes this code easier to read, just as "There’s a soda in the fridge" is more intuitive than "The fridge contains a soda". Now consider a case in which the collection is more important. For instance, "A human has a liver" seems more intuitive than "A liver is in a human". The same with the following code:
val ADMIN_TAG = "ADMIN"
val admins = users.map { user.tags.contains(ADMIN_TAG) }
// or
val admins = users.map { ADMIN_TAG in user.tags }
For me, using contains in this case makes the code clear. I understand that some might feel differently about this, so please do not treat this as a hard rule (do not force it on reviews). Writing really readable code is great art, so all the rules should be treated as suggestions.
Operators can also be added to our own classes, like units of measure, money wrappers, other kinds of numbers, and others.
@JvmInline
value class Centimeter(private val value: Double) {
operator fun plus(other: Centimeter): Centimeter =
Centimeter(value + other.value)
operator fun plus(other: Millimeter): Centimeter =
Centimeter(value + other.value * 10)
// ...
}
These are the most common cases where I introduce Kotlin operators, but there are clearly many more overloaded operators in the standard library.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.