Pitfalls in Kotlin

Maxilect
7 min readDec 27, 2021

Kotlin language helps to avoid some Java problems. But like any language, it has its specifics. While developing our project, we came across several interesting moments. Some of them spoil your production if you overuse them. Others affect the performance of highly loaded systems. All these points are difficult to notice since plugins for the IDE do not highlight them, and in general, at first glance, the code looks like a valid one. In this article, we’ll talk about them.

Null Security

Kotlin provides mechanisms to protect the developer from Java’s standard NullPointerException — the so-called null security. The Kotlin system distinguishes between those that can be nullable and those that can never be null. But the built-in protection sometimes needs to be bypassed, and this is where the surprises begin.

Lateinit

Using a non-null variable in the code, we must immediately assign a value; otherwise, the code will not compile. But there are situations when you do not need to initialize a mutable non-null variable right away, for example, in Spring.

Just for such a case, Kotlin provides the lateinit modifier. It delays variable initialization.

In fact, by signing lateinit, we tell the compiler that we will take care of this later (and we will not write unnecessary checks for null here). Here we give the code that shows how IDEA highlights the syntax.

Lateinit is like a helpful detour from the creators of Kotlin. However, you can’t avoid it. But when the developer forgets to initialize a variable or refers to it before initializing it, an UninitializedPropertyAccessException exception is thrown in production. Therefore, you can’t abuse lateinit and put it wherever there is no “?” type. The compiler will not see such an error. It will appear later.

By the way, lateinit, in principle, cannot be used with primitive types (detailed explanations can be found here).

“!!” Operator

Another way to dominate the compiler is the “!!” operator.

If a variable is of a nullable type, Kotlin will not let you work with it without null-check. The code won’t compile. Operator “!!” allows you to bypass this limitation if the developer thinks that the check is unnecessary (if the variable, by logic, should not become null, despite the type).

However, if the variable is null during its call, NullPointerException cannot be avoided, not during the compilation but at runtime.

NullPointerException

As with lateinit, there is nothing wrong with this language feature, but you shouldn’t abuse it.

Be careful with extension functions

Let’s move on to the language features we discovered when profiling our high-load service.

Unlike Java, Kotlin can extend classes through extension functions. Since these functions are associated with a specific class, the behavior can vary depending on the class used. It’s hard to keep track of this.

Here is an example. Suppose we create a MutableMap, initialize it via ConcurrentHashMap, and call getOrPut:

Create a MutableMap, initialize it via ConcurrentHashMap, and call getOrPut

The code looks fine, but it won’t work.

The problem is that getOrPut is not a MutableMap method, which means that ConcurrentHashMap will not override it. getOrPut is a MutableMap extension function that is also not thread-safe.

Let’s see what’s under the hood…

For the code to work, you need to register ConcurrentHashMap explicitly:

We register ConcurrentHashMap explicitly.

Then a thread-safe extension function will be used, i.e., everything will work correctly.

Under the hood…

Here is another similar situation, also based on one of the modules of our project.

In the code, we check for the presence of “3” and “8” in the set of hashSet strings.

Two ways to check for the presence of 3 and 8 in a HashSet

HashSet is a Java class that we use in Kotlin. It is known that “contains” calls the getNode element search method and returns true or false very quickly, regardless of the number of elements in the set (the complexity of the algorithm, in this case, is O(1)).

The screenshot shows two code versions that implement the functionality we need. In the first case, we hardcode the strings we are looking for in the set. In the second (under the comment), we pass the fields of the numbers variable to the “contains” method.

Practice shows that the second option is four times slower because it calls an extension function that works differently. This happens because the variables in the Numbers data class have a different type — “String?” (i.e., they are nullable). From the point of view of Kotlin, we pass an object of a different type to “contains.” Therefore the search itself is carried out differently — the complexity of the algorithm increases, and the speed decreases. The fact that the execution slowed down only four times is our luck since the hashSet was small.

In a typical system, no one would have noticed. But in our case, this fix alone helped to increase performance by 10% (the service was already working quickly — we managed to clean up other obvious points).

By the way, the operator “!!” could have fixed it, although we ended up acting differently on the project.

Abowementioned does not mean you need to give up extension functions, but you should carefully look at what is happening inside. It’s not for nothing that IDEA highlights extension functions with color.

Concluding the topic of extension functions, we would like to share one more amusing example.

As we have found out in practice, toString() for a null object can throw an unexpected result — literally “null.”

toString() for a null object yields the string “null” of 4 characters

Look for StringBuilder even where it is not supposed to be

Kotlin uses a particular StringBuilder class (it came from Java) to construct strings. It helps to build strings faster in several situations without creating many intermediate new objects. But sometimes, StringBuilder appears where it is not expected. Here is another example from our project (as in the previous examples, we rewrote the code specifically for the article).

At first glance, the screenshot is a harmless method:

It would seem that we are adding two strings through concatenation, but Kotlin uses StringBuilder. It slows down the work when it comes to a highly loaded service.

Kotlin initializes StringBuilder with a constant-defined capacity. If this capacity is not enough, memory allocation begins. In our case, allocating new memory just slowed down the entire service. At the same time, there was no StringBuilder in the code explicitly.

The second screenshot shows the same code but decompiled in Java.

What’s under the hood…

This is where a new StringBuilder is created with a default capacity of 16 characters. Since the string “Current timestamp is” does not fit into these 16 characters, memory is re-allocated. The result is double work, which is clearly visible under load.

Since the Kotlin 1.4.20, string concatenation uses “invokedynamic.” Read more about that. However, in our project, the problem was fixed differently.

Check twice after the Kotlin version update

In dev, stage, and test environments, we use the lowest logging level — trace.

But in production, we turn off the logger so that performance does not degrade and the hard drive does not overflow (the service writes hundreds of GB of logs per day). As highlighted in the screenshot, we added a class to create the logging blacklist.

This is how we restrict logging.

In production, the logger was declared differently — through private:

private companion object: KLogging ()

As we thought then, this does not affect anything since the logger is used only inside the class.

At first, everything worked as it should. But then we updated Kotlin and accidentally noticed that the logger output had changed. He added “$ Companion” at the end of the class name, thereby falling blacklist.

What it looks like after updating the language version.

What it looks like after updating the language version.

In fact, we were on the edge of a crash. So far in Kotlin, the private access modifier affects how the class is named in the log.

We are sure that we have not come across all the features of the language. The most interesting is yet to come! Have you ever encountered something similar?

PS. Subscribe to our social networks: Twitter, Telegram, FB to learn about our publications and Maxilect news.

--

--

Maxilect

We are building IT-solutions for the Adtech and Fintech industries. Our clients are SMBs across the Globe (including USA, EU, Australia).