Inline classes aim to reduce the overhead due to boxing of primitives. Inline classes are experimental since kotlin 1.3.
You can use them without warning with the following configuration added to the kotlin-maven-plugin, for instance in maven:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>1.4.21</version>
...
<configuration>
<args>
<arg>-Xinline-classes</arg>
</args>
</configuration>
...
</plugin>
The inline keyword means the compiler will replace wherever possible the wrapper class by its enclosing primitive.
Let's have a look at this piece of code:
fun main() {
val age = Age(18)
val weight = Weight(87)
println("Hello I am ${age.value}, my weight is ${weight.value} !")
}
class Age(val value: Int)
inline class Weight(val value: Int)
Using intellij idea we can see the byte code generated by the main method by putting our cursor in the main function and searching the action ByteCode viewer
, we obtain:
LINENUMBER 2 L0
NEW Age
DUP
BIPUSH 18
INVOKESPECIAL Age.<init> (I)V
ASTORE 0
L1
LINENUMBER 3 L1
BIPUSH 87
INVOKESTATIC Weight.constructor-impl (I)I
ISTORE 1
L2
LINENUMBER 4 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Hello I am "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL Age.getValue ()I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
LDC ", my weight is "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
LDC " !"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 2
ICONST_0
ISTORE 3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 2
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L3
LINENUMBER 5 L3
RETURN
L4
LOCALVARIABLE weight I L2 L4 1
LOCALVARIABLE age LAge; L1 L4 0
MAXSTACK = 3
MAXLOCALS = 4
// access flags 0x1009
public static synthetic main([Ljava/lang/String;)V
INVOKESTATIC MainKt.main ()V
RETURN
MAXSTACK = 0
MAXLOCALS = 1
}
Not very sexy π± right? Even without being a byte code expert let's decrypt the structure first:
We can recognize the different parts of our main
function. From LINENUMBER 2
to LINENUMBER 4
we can see the declaration of age and weight variable.
From LINENUMBER 4
to LINENUMBER 5
we can see the invocation of the println
function and the use of the java/lang/StringBuilder
which is how a template string from Kotlin in translated into bytecode.
If we look again at the declaration of variables:
LINENUMBER 2 L0
NEW Age
DUP
BIPUSH 18
INVOKESPECIAL Age.<init> (I)V
ASTORE 0
L1
LINENUMBER 3 L1
BIPUSH 87
INVOKESTATIC Weight.constructor-impl (I)I
ISTORE 1
L2
We notice the two variables declaration are similar:
- first: a value is pushed on the stack, with the
BIPUSH
18 in the case of the age and 87 in the case of the weight command, - second: the constructor is invoked with INVOKESTATIC for the inline class weight and INVOKESPECIAL for the class Age.
- third: a step of storage into the variable pool of the thread is done with
ISTORE 0
for weight variable andASTORE 1
for Age variable.
Let's focus on what makes the difference now:
We use ASTORE
in the case of the Age variable and ISTORE
in the case of the weight variable. If we go on this reference
of bytecode instructions for java, we can see that commands prefixed by A
deals with references to object whereas I
deals with integer.
So ASTORE 0
command designates the storage of an object reference at index 0
of the local variables of the thread, that's how the reference to the Age object is stored.
On the other hand, our Weight object doesn't exist in the byte code, it is directly stored at index 1 with the type integer, that is why we have ISTORE 1
instruction.
The StringBuilder calls confirms this replacement of the reference with a primitive:
LDC ", my weight is "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
LDC " !"
The call to the integer primitive value stored at index 1 is made with ILOAD 1
, so no object reference is used there either.
Another interesting thing to do is to replace the weight class with an integer, and see that the generated bytecode is very close from the one shown above. The main difference we will see are the static calls to the constructor of Weight INVOKESTATIC Weight.constructor-impl (I)I
will not be in the bytecode anymore.
Conclusion
With the inline
keyword, the kotlin compiler replaces in the bytecode the object reference with the primitive type being boxed.
The point of this is to reduce memory consumption while keeping the advantage of value classes for readability and domain design.
In some cases, though, the compiler doesn't replace the inline class with the primitive type, for instance if more than one property is boxed.
Why looking through the bytecode? Just out of curiosity :), I could also have used the decompiler plugin from intellij to decompile the bytecode into java code to see that the inlined class is indeed replaced, I let you try ;)
Sources:
For more details about inline classes: the kotlin documentation https://kotlinlang.org/docs/reference/inline-classes.html
To deep dive more into how java byte code is written, this article is useful : http://arhipov.blogspot.com/2011/01/java-bytecode-fundamentals.html
Wikipedia reference of bytecode instructions : https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
Top comments (0)