Pitfalls in Kotlin
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.
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.
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.
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).
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.
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:
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.
For the code to work, you need to register ConcurrentHashMap explicitly:
Then a thread-safe extension function will be used, i.e., everything will work correctly.
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.
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.”
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.
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.
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.
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?