Do you use these kotlin features right?
I have been using Kotlin for more than a year now, mainly for backend development. I found it an efficient language for general-purpose software development and a thoughtful crossover between Java and Scala.
The compile-time null safety, reduced verbosity, coroutines, tail-recursion and enhanced collection API with Java are great features that you miss in even the latest versions of Java (I am talking about Java 16). On the other hand, Kotlin is less intimidating than Scala for developers who do not have much experience in functional programming.
However, I have seen many developers use the language features without understanding what’s happening under the hood. By the way, these points are applicable for Kotlin on JVM.
Misuse of factory methods in the Collection API
The Collection API provides quite a lot of handy factory methods to create lists, sets and maps. However, these methods could be counterproductive due to their backing implementation.
The mapOf()
method creates a new Java LinkedHashMap
instance. But most of the time the developer wants a HashMap
. Unless you want features of a doubly-linked list, use hashMapOf()
function.
The setOf()
construct also provides an identical implementation. It instantiates a Java LinkedHashSet
, as above, you may want to use hashSetOf()
instead.
How do those functions pick the implementation?
If you look at Kotlin’s LinkedHashMap
or LinkedHashSet
classes, there’s a modifier keyword expect
in the class declaration. The expect
instructs Kotlin to wait for the platform specific implementation. On JVM it is java.util.LinkedHashMap
and java.util.LinkedHashSet
respectively.
Not cleaning up resources like IO streams after use
Kotlin provides quite a lot of convenient extention functions. However, extension functions such as readBytes()
on java.io.InputStream
should be used with care. For example; FileInputStream("foo.txt").readBytes()
is risky. Because they do not close the IO stream and could introduce a resource leak. The documentation specifically says the user should close it after use. The recommended approach is to handle the stream inside the use()
function as below.
val safeRead = FileInputStream("foo.txt").use {
it.readBytes()
}
Manipulation of large collections without using sequences
One of the best features of Java Stream
s is the intermediate operations (i.e filter
, map
) are lazy, that means any intermediate computations will not be performed until the terminal operation such as forEach
, count
is invoked.
On the other hand, Kotlin provides a streamlined and succinct collection API but there are not lazy unless you invoke them as a Sequences
. The sequences use multi-step collection processing, this simply means the intermediate operations such as filter
are not computed until the actual results of the pipeline is requested. So they have similar behaviour to Java Stream
s. This is something most of the new Kotlin developers miss.
Let’s look at the following rudimentary examples to demonstrate the difference between normal and sequence collection chains.
measureTimeMillis {
(1..100_000_000)
.filter { it % 2 == 0 }
.map { it * 2 }
.groupBy { it / 10 }
}
//21245ms
measureTimeMillis {
(1..100_000_000)
.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.groupBy { it / 10 }
}
//11874ms
measureTimeMillis {
(1..10)
.filter { it % 2 == 0 }
.map { it * 2 }
.groupBy { it / 10 }
}
//10ms
measureTimeMillis {
(1..10)
.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.groupBy { it / 10 }
}
//13ms
The JVM (comes with open-jdk-11.0.2) was warmed up before running those examples. And I ran them on a MacBook Pro (2.2 GHz Quad-Core Intel Core i7, 16GB RAM).
This is not by any means a benchmarking exercise. You will most probably see different execution times. I wanted to demonstrate that the sequences tend to perform efficiently on larger collections and poorly on smaller ones.
The take away is to choose the right collection pipeline implementation depending on the use case rather than just use the default
Lack of use of delegated properties
Kotlin natively supports (delegation) [https://en.wikipedia.org/wiki/Delegation_pattern], which is a pretty handy feature. Also (delegated properties)[https://kotlinlang.org/docs/delegated-properties.html] make constructs such as lazy or oberservable computation ready made for developers.
The lazy
properties are one of the most useful delegates. This is similar to the lazy evaluation concept in functional programming. The code inside the lazy
block only gets executed on first invocation and cache the value for subsequent uses.
val lazyReadLines: Sequence<String> by lazy {
File("foo.txt").useLines { it }
}
The above file will only be read when it’s required and not on application bootstrap. Imagine if this file is large and read eagerly on startup, the lazy properties are such a simple way to improve fast application startup.
Forcing Null Pointer Exceptions (NPE)
I feel sad when people use !!
(not-null assertion operator) to force nullability and invite NPE as in Java code. The (null safety)[https://kotlinlang.org/docs/null-safety.html] is a fundamental language feature in Kotlin and developer should handle nulls idiomatically.
I have found that delegating and bubbling up nullable objects like we handle exceptions is a effective way to write a readable less boilerplate code.
Kotlin provides number of aides to deal with nullable types. The elvis operator, safe calls and filtering nulls in collections help to avoid forcing NPEs.
I hope these tips will help you to write better Kotlin code. If you are new to Kotlin, the friendly (official reference)[https://kotlinlang.org/docs/home.html] is the best place start.