Kotlin inline class in JUnit tests

1k Views Asked by At

I am trying to understand concept of inline classes - they are a simple object wrapper of single property that is being inlined during runtime. That means, that the actual initialization of the class is not happening at runtime

I was trying to write simple test which directly will show my above explanation during JUnit test as below:

companion object {
   private const val NAME = "JACK"
}

inline class NameInlineClass(val value: String)

@Test
fun unwrapping() {
    val nameInlineClass = NameInlineClass(NAME)
    val name = nameInlineClass
    assertEquals(name, NAME)
}

This test fails unfortunately which leads me to the question why during assertEquals() the actual unwrapped String value is not being compared, but the actual inline class (which should be unwrapped during runtime)?

1

There are 1 best solutions below

0
On

What you probably wanted to do was val name = nameInlineClass.value, but I'll try to explain the error.

See Representation from docs (includes code sample):

In generated code, the Kotlin compiler keeps a wrapper for each inline class. Inline class instances can be represented at runtime either as wrappers or as the underlying type. This is similar to how Int can be represented either as a primitive int or as the wrapper Integer.

That means as long as you don't reference the wrapping object or its type explicitly, value will not be boxed. We can check it by inspecting bytecode (decompiled back to Java for readability):

// kotlin source
fun unwrapping() {
    val nameInlineClass = NameInlineClass(NAME)
    val name = nameInlineClass  // this line gets dropped by compiler by the way
    assertEquals(name, NAME)
}

// java representation of bytecode
public final void unwrapping() {
   String nameInlineClass = NameInlineClass.constructor-impl("JACK");
   Assert.assertEquals(NameInlineClass.box-impl(nameInlineClass), "JACK");
}

I won't paste entire generated NameInlineClass body, but constructor-impl is static method that only checks for null of value, and box-impl creates the wrapper object.

You can see nameInlineClass is indeed a String - that means inline works and no extra object was allocated.

Only when you reference nameInlineClass instead of nameInlineClass.value compiler determines that this object needs representation and "boxes" the value with wrapper NameInlineClass class.