kotlincachingboxingkotest

In Kotlin kotest, boxing of number giving false for number less than 128


The Kotlin Docs contains the following information.

All nullable references to a are actually the same object because of the memory optimization that JVM applies to Integers between -128 and 127. It doesn't apply to the b references, so they are different objects.

I wrote the main function and test code to check the above. At this time, each result was different.

I test this using kotest.

class Ch7Test: StringSpec({

    "nullable Int between -128 and 127 should be caching" {
        val boxedA: Int? = 100
        val anotherBoxedA: Int? = 100
        
        assertSoftly {
            (boxedA === anotherBoxedA) shouldBe true
        }
    }
})

The test result was failed.

expected:<true> but was:<false>
Expected :true
Actual   :false

But when I test in main function, the result was different from the test results.

fun main() {
    val boxedA: Int? = 100
    val anotherBoxedA: Int? = 100

    println("debug: ${boxedA === anotherBoxedA}") // output was "debug: true"
}

Why is it the same logic, but the results are different?


Solution

  • What's actually happening is a bit obscured by the DSL syntax, but essentially when you do "foo bar bar" { ... } in a StringSpec, you are calling String.invoke declared in StringSpecRootScope.

    operator fun String.invoke(test: suspend StringSpecScope.() -> Unit)
    

    The important part here is that the lambda you pass is actually a suspend lambda. In other words, the boxing and tests happen in a coroutine.

    And apparently, boxing in coroutines is different. Instead of calling the Java boxing methods like Integer.valueOf, one of the methods from this file is called. For Ints, it is:

    internal fun boxInt(primitive: Int): java.lang.Integer = java.lang.Integer(primitive)
    

    As you can see, this straight up creates a new Integer object (new Integer(primitive) in Java). If it had used Integer.valueOf, then the integer cache would have been used.

    Try decompiling a file like

    suspend fun main() {
        val x: Int? = 100
    }
    

    and see which method is called for the boxing.

    According to the commit that added this file,

    These methods are very thin wrappers around primitive wrapper classes constructors.

    They are used by coroutines code which returns primitives and this way HotSpot is able to throw the allocations away completely.

    #KT-26591 Fixed

    So this is actually an optimisation specifically for the HotSpot JVM.

    That said, I'm not familiar with the JVM internals to explain exactly how explicitly allocating an Integer actually makes the JVM optimise away the allocations.