Laksitha's Space

My pro tips, ramblings and random thoughts

14 Apr 2021

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 Streams 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 Streams. 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.