If you’re starting out with Kotlin, my personal advice is to pay more attention to compiled bytecode or decompiled Java code so you’ll find more detail. Just studying the syntax can cause you to miss details that can be a source of performance problems or bugs.

Here are some of the problems I’ve encountered in using it, and if you can get some inspiration, I’ll write about them as I go along. This article is based on Java 8 + Kotlin 1.5.21

1. String stitching

In Java we usually concatenate strings using StringBuilder, concat, or +. String templates and Plus are also available in Kotlin

A simple example:

val a = "Hello"
val b = "World"

val c = "$a $b"
val d = "$a $b!"
val e = a.plus("").plus(b)
val f = a.plus("").plus(b).plus("!")
Copy the code

And then clickTools – > Kotlin – > Show Kotlin Bytecode -> DecompileYou can decompile kotlin’s compiled bytecode to see the Java version of the code.The only difference in the sample code is that one more is concatenated at the end!The resulting conversion code is slightly different, however+The number concatenation method is eventually used in Java as wellStringBuilderString concatenation, so it’s equivalent. This is just to show the difference.

But the problem is usageplusYou can see the penultimate line in the figure, which is directly in appenda + " ". Feeling a little off, let’s go straight to the bytecode:You can see that two have been createdStringBuilder, that is, every time plus, create one. That is, the internal implementation of Plus passes the left and right arguments to the method and then usesStringBuilderJoining together. The equivalence relation is:plus(plus(a, " "), b). So it seems to make sense why decompilation shows up that way.

"$a $b"Mode bytecode as shown below:So I don’t have to tell you more about how to concatenate characters in Kotlin. As in Java, concatenating characters in loops is recommendedStringBuilderIf a string template is used, it is not created each time through the loopStringBuilder?

2.lazy

The lazy function is to initialize the property the first time it is used for lazy loading.

private val name: String by lazy { "weilu" }
Copy the code

Lazy has three initialization modes:The default mode isLazyThreadSafetyMode.SYNCHRONIZED, which ensures that only one thread can initialize the instance. Let’s look at the implementation code:Source code used@Volatile,synchronizedDouble checked locks are implemented to ensure thread safety. But there is also a significant performance overhead. We can specify lazy if we only use it in a single threadLazyThreadSafetyMode.NONETo avoid such problems.Optimized use method:

private val name: String by lazy(LazyThreadSafetyMode.NONE) { "weilu" }
Copy the code

3.companion object

If you need to write static properties or methods in a Kotlin class, you need to create them using a Companion Object. Here are a few ways to write it:

class CompanionTest {

    companion object {
        val TEST_1 = "TEST_1"
        const val TEST_2 = "TEST_2"

        private val TEST_3 = "TEST_3"
        private const val TEST_4 = "TEST_4"
        
        fun test(a) {
            println(TEST_1)
            println(TEST_2)
            println(TEST_3)
            println(TEST_4)
        }
    }
    val test5 = "TEST_5"
    private val test6 = "TEST_6"
}
Copy the code

Let’s take a look at the generated code:

As you can see, the getTEST_1 method is generated without the const modifier. Then call TEST_1 when, in fact is called CompanionTest.Com panion getTEST_1 (), such code to tell the truth a bit cumbersome.

How to read static properties directly like Java does, like TEST_2, with a const modifier, so that the variable is compiled inline and no extra methods are generated.

Test5, test6, test5, test6

4.inline

Inline is a modifier for a method that causes the method to compile inline. What is inline? Simply put, it is like copying a method implementation code in.

For example, we have a method add:

fun add(a: Int, b: Int): Int {
    return a + b;
}
Copy the code

If used directly, the decompiled code is as follows:

UtilsKt.add(1.4);
Copy the code

If you add an inline modifier, the decompilated code looks like this:

byte a$iv = 1;
int b$iv = 4;
int var10000 = a$iv + b$iv;
Copy the code

In general, we do not need to add inline, otherwise the method will be “copied” every time it is called, which would generate too much code and increase in size. So AS also gives us a warning:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
Copy the code

The performance impact of inlining is minimal, and inlining is best for functions with function type parameters.

This makes it clear that inlining is suitable for methods that pass functions as arguments. If you look at some of kotlin’s source code, you’ll see that some of the higher-order functions let, map, and run do this.

To see why, we can take an example:

	private fun testFunction(i: Int, call: (Int) - >String) {
        call.invoke(i)
    }

    fun test(a) {
        testFunction(9) {
            it.toString()
        }
    }
Copy the code

The compiler: Function1Is the generic interface provided by Kotlin, 1 means there is one parameter. So using lambda expressions is essentially creating one at a timeFunctionXObject.

Then look at the bytecode:

INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;) Ljava/lang/Object; (itf)Copy the code

There is automatic boxing using integer.valueof. Then for function1.invoke (Object) : Object, enter the convention box. If there is a return value, it will be unpacked.

Both of these are performance costs of using lambda expressions directly. There are two ways to avoid this:

1. You can assign a lambda expression to a variable and then reference the variable each time, avoiding both the double creation of function objects and the double boxing and unboxing overhead.

2. Inline functions can avoid the overhead of creating function objects and boxing and unboxing higher-order functions, but the inline function body should not be too large.

Below is fortestFunctionThe decompilated code for the inline method:

5. Gson parsing

The main problem is the use of Gson and Data class. Even if your variable is declared non-nullable (excluding the underlying type) and has a default value, if the field in json is null, the non-nullable variable will be assigned null after parsing. The application will crash when you use this field.

Specific problems and solutions can see the following several blogs, write very clear and detailed, here is not to say.

  • Gson and Kotlin collided with an unsafe operation
  • A few thoughts on parsing data classes using Gson
  • Summary of Kotlin Json library problems (Gson and Moshi’s pit)

These are some of the things I’ve learned and encountered while developing with Kotlin, and have reworked a lot of previous code… Sharing is also to help you step on a few holes.

There are many performance costs like those mentioned in this article that I can’t list here. So we need to pay more attention to the compiled code in learning and using.

I remember reading a cartoon called “Steel alchemy”, the core of which is to say: to obtain something, need to exchange at the same price. For example, many of the open source frameworks we use are very simple and flexible, but at the cost of encapsulation optimization by authors. Kotlin’s simplicity doesn’t come without a cost, including a lot of default behavior in the form of a performance penalty. If we master these details and play to our strengths and avoid our weaknesses, we may achieve a “win-win” situation.

Too much, ha ha. If this post helped you, give it a thumbs up!

6. Reference & Recommended reading

  • Kotlin performance optimization