1. Overview

Sometimes we might want to wrap some value inside an object in order to provide a useful abstraction – for example, a counter. However, this can lead to additional runtime work because the wrapper object requires additional memory on the heap.

If the wrapped type is primitive, it can greatly affect performance since primitives are usually optimized while wrappers are not.

To address this problem, Kotlin provides a solution called inline classes. Inline classes are a specific type of class based on values rather than identities. They don’t possess their own distinct identity and can only store values.

2. What Are Inline Classes?

As opposed to regular (non-inlined) wrappers, inline classes benefit from improved performance. This happens because the data is inlined where it’s used, and object instantiation is skipped in the compiled code.

Let’s see an example of an inline class called Circle with a property of type Double representing the radius:

@JvmInline
value class Circle(private val radius: Double) {
    val circumference: Double
      get() = radius * 2.0 * 3.14
}

We can now use the above class in our code as follows:

val circle = Circle(5.0)
val circumference = circle.circumference

Compiling the above Kotlin code to Java bytecode and then decompiling it back to readable code essentially shows us how an inline class is treated by the JVM at runtime. Let’s decompile with the CFR decompiler:

public final class Circle
{
    private final double radius;
    
    /* toString, hashCode and equals implementations */
    
    public static double constructor-impl(final double radius) {
        return radius;
    }
    
    public static final double getCircumference-impl(double arg0) {
        return arg0 * 2.0 * 3.14;
    }
}

And the usage of our inlined class:

double circle = Circle.constructor-impl(5.0);
double circumference = Circle.getCircumference-impl(circle);

We can see that our Circle class is not instantiated in the compiled code – the constructor-impl() method is a simple static method that returns the passed argument. This is then stored in memory as a double primitive type in the call site.

Similarly, the getCircumference-impl() static method calculates the circumference of the passed argument.

2.1. Usage Example

Now we know what inline classes are, let’s discuss their usage.

An inline class must have a single property initialized in the primary constructor. This single value represents the instance at runtime.

Therefore, in order to have a correct definition, we can use a single line of code:

@JvmInline
value class InlineDoubleWrapper(val value: Double)

We defined InlineDoubleWrapper as a simple wrapper over a Double object and applied the value keyword to it. As we are writing code for JVM, value classes must be annotated with @JvmInline annotation. Finally, we can now use this class in our code with no additional changes:

@Test
fun whenInclineClassIsUsed_ThenPropertyIsReadCorrectly() {
    val pi = InlineDoubleWrapper(3.14)
    assertEquals(3.14, pi.value)
}

3. Class Members

Up until now, we used inline classes just like simple wrappers. But they offer more than that. They also allow us to define properties and functions just like regular classes.

Let’s extend our example to define a property representing the diameter, and a function to return the area of the circle:

@JvmInline
value class Circle(private val radius: Double) {
    val diameterOfCircle: Double
        get() = 2.0 * radius
    
    fun areaOfCircle = 3.14 * radius * radius
    
    init {
        require(radius > 0) { "Circle radius must be positive" }
    }
}

We’ll now create a test for our diameterOfCircle property. It instantiates our Circle inline class and then accesses the property:

@Test
fun givenRadius_ThenDiameterIsCorrectlyCalculated() {
    val radius = Circle(5.0)
    assertEquals(10.0, radius.diameterOfCircle)
}

And we can test the areaOfCircle function:

@Test
fun givenRadius_ThenAreaIsCorrectlyCalculated() {
    val radius = Circle(5.0)
    assertEquals(78.5, radius.areaOfCircle())
}

Instantiating inline classes can also invoke init blocks. This is useful for validating the wrapped property’s value:

@Test
fun givenNegativeRadius_ThenThrowsIllegalArgumentException() {
    assertThrows<IllegalArgumentException> {
        Circle(-5.0)
    }
}

However, there are some limitations on what we can and can’t define inside our inline classes. While properties, functions, and init blocks are allowed, inner classes and backing fields are not.

4. Inheritance

Inline classes can inherit only from interfaces. They cannot be subclassed and so inline classes are also effectively final.

4.1. Inheritance Example

Given an interface Drawable with a method draw(), we’ll implement this method in our Circle class:

interface Drawable {
    fun draw()
}

@JvmInline
value class Circle(private val radius: Double) : Drawable {
    val diameterOfCircle get() = 2 * radius
    fun areaOfCircle() = 3.14 * radius * radius

    init {
        require(radius > 0) { "Circle radius must be a positive number" }
    }

    override fun draw() {
        println("Draw my circle")
    }
}

4.2. Inheritance Caveat

While using inheritance for inline classes, it’s really important to understand its limitations to use them to their full potential.

Inline classes are represented as the wrapped type only if they are statically used as their actual type. This means that an instance of an object of such class is only inlined if used in a method where the method argument type is of inline class type.

Let’s imagine we have 3 methods that accept the interface inherited by the Circle class as an argument:

fun useAsDrawable(drawableInline: Drawable) { }
fun useAsNullableDrawable(drawableNullableInline: Drawable?) { }
fun <T> useAsGeneric(genericInline: T) { }

Invoking each of these methods bypasses the inlining of our object, and so the performance is the same as a regular class.

5. Conclusion

Inline classes provide a solution for efficiently wrapping data types without incurring the overhead of heap allocations. By leveraging inline classes, we can optimize performance by avoiding unnecessary object instantiation.

It’s also crucial to understand the limitations of inheritance when working with inline classes to maximize their benefits.

As always, all of the example code can be found over on GitHub.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!