1. Overview
String interpolation in Kotlin allows us to easily concatenate constant strings and variables to build another string elegantly.
In this article, we’re going to see how this interpolation works under the hood by taking a look at the generated bytecode. Also, we’ll take a look at possible future optimizations for the current implementation.
2. String Interpolation
Let’s start with a simple and familiar example:
class Person(val firstName: String, val lastName: String, val age: Int) {
override fun toString(): String {
return "$firstName $lastName is $age years old"
}
}
As shown above, we’re using string interpolation to implement the toString() method. In order to see how the Kotlin compiler implements this feature, we should first compile this class via kotlinc:
>> kotlinc interpolation.kt
Then we can use the javap tool to take a peek at the generated bytecode:
>> javap -c -p com.baeldung.interpolation.Person
// truncated
public java.lang.String toString();
Code:
0: new #9 // class StringBuilder
3: dup
4: invokespecial #13 // Method StringBuilder."<init>":()V
7: aload_0
8: getfield #17 // Field firstName:LString;
11: invokevirtual #21 // Method StringBuilder.append:(LString;)LStringBuilder;
14: bipush 32
16: invokevirtual #24 // Method StringBuilder.append:(C)LStringBuilder;
19: aload_0
20: getfield #27 // Field lastName:LString;
23: invokevirtual #21 // Method StringBuilder.append:(LString;)LStringBuilder;
26: ldc #29 // String is
28: invokevirtual #21 // Method StringBuilder.append:(LString;)LStringBuilder;
31: aload_0
32: getfield #33 // Field age:I
35: invokevirtual #36 // Method StringBuilder.append:(I)LStringBuilder;
38: ldc #38 // String years old
40: invokevirtual #21 // Method StringBuilder.append:(LString;)LStringBuilder;
43: invokevirtual #40 // Method StringBuilder.toString:()LString;
46: areturn
This set of bytecode instructions creates a StringBuilder instance and appends each part of the string using the append() method. Basically, this bytecode is equivalent to the following Java code:
new StringBuilder()
.append(firstName)
.append(' ') // ascii code 32 or space
.append(lastName)
.append(" is ")
.append(age)
.append(" years old")
.toString()
So, the string interpolation feature is using the StringBuilder class under the hood.
2.1. Pros and Cons
On the bright side, the StringBuilder implementation is quite simple and easy to understand for many Java and Kotlin developers.
However, as the number of template variables increases, the bytecode becomes longer. This will, in turn, affect the JVM startup time since there are more bytecodes to process and verify.
Moreover, the concatenation strategy is fixed at compile-time. So, if a newer compiler version uses a more efficient approach, we should recompile our old code to take advantage of that efficient implementation.
Let’s see how Java 9 and Kotlin 1.4.20 fix these issues.
3. Invoke Dynamic
Invoke Dynamic (also known as Indy) was part of JSR 292 intended to enhance the JVM support for dynamically typed languages. As of Java 9 and as part of JEP 280, the string concatenation in Java is using invokedynamic.
The primary motivation behind this is to have a more dynamic implementation. That is, it’s possible to change the concatenation strategy without changing the bytecode. This way, clients can benefit from a new optimized strategy even without recompilation. The generated bytecode also gets smaller, contributing to a faster startup time.
As of Kotlin 1.4.20, the Kotlin compiler can take advantage of indy for string concatenation. To that end, we have to do two things:
- Use 9 or higher as the JVM targets because string concatenation with indy is available only on Java 9+
- Use the -Xstring-concat compiler flag to enable invokedynamic string concatenation
So, if we recompile the same code with these flags:
>> kotlinc -jvm-target 9 -Xstring-concat=indy-with-constants interpolation.kt
Then the bytecode would be:
>> javap -c -p com.baeldung.interpolation.Person
public java.lang.String toString();
Code:
0: aload_0
1: getfield #11 // Field firstName:LString;
4: aload_0
5: getfield #14 // Field lastName:LString;
8: aload_0
9: getfield #18 // Field age:I
12: invokedynamic #30, 0 // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;I)LString;
17: areturn
As shown above, the bytecode would be the same, regardless of the number of template variables — much simpler, more reliable, and less verbose!
Please note that this will only work on Java 9+ and Kotlin 1.4.20+ with 9+ JVM target. Also, there’s a plan to make indy the default implementation in Kotlin 1.5.
4. Conclusion
In this quick article, we saw two different implementations for string interpolation in Kotlin: one with StringBuilder and another with invokedynamic.
As usual, all the examples are available over on GitHub.