[Kotlin - JVM] Integer Cache

4rk·2023년 10월 26일
0

Kotlin에서는 Int 범위를 표현 하는 IntInteger라는 두가지의 자료형이 있다.

자바에서 버전 9부터 Integer 의 생성자는 현재 Deprecated가 된 상태지만 하위호환을 중요하게 여기는 Kotlin은 아직도 사용중이다.

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }    

@Deprecated(since="9", forRemoval = true)
    public Integer(int value) {
        this.value = value;
    }

Integerint하나를 wrapping 하고 있다. 그리고 이를 통해서 비교 연산을 하게 된다면 object로 처리 될 것이다. 추가로 캐싱은 -128 ~ 127까지 되고 나머지는 매번 새로운 객체를 생성하여 반환하고 있다. 추가로 캐싱 범위를 수정할 수 있다.

결과 미리 보기 :

스크린샷 2023-02-17 오후 10.00.05.png

Test1

int 동등성

이제 다음 아래 코드를 동등성 을 확인해보면 한 눈에 봤을 때에 actual와 expected를 IntInt의 관계로 비교 한다고 알 수 있다.

// Kotlin Version
@Test
fun test1() {
   val actual: Int = 1
   val expected: Int = 1
   assertThat(actual).isEqualTo(expected)
}

// Decompile Version
@Test
public final void test1() {
   int actual = 1;
   int expected = 1;
   Assertions.assertThat(actual).isEqualTo(expected);
}

// Assertion.assertThat().isEqualTo()
public SELF isEqualTo(int expected) {
    integers.assertEqual(info, actual, expected);
    return myself;
  }

문제 없이 똑같이 int 타입으로 들어가게 되고 비교 또한 intint 형태로 비교 하고 있다.

따라서 문제 없이 참이 나올 것이다.

Integer 동일성

이제 문제의 동일성을 확인 해보자


// Kotlin Version
@Test
fun test1() {
    val actual: Int = 1
    val expected: Int = 1
    assertThat(actual).isSameAs(expected)
}

// Decompile Version
@Test
public final void test1() {
   int actual = 1;
   int expected = 1;
   Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}

// Assertion.assetThat().isSameAs()
@Override
public SELF isSameAs(Object expected) {
   objects.assertSame(info, actual, expected);
   return myself;
}

확인해보면 actual 은 int로 들어가고 expected 는 Integer.valueOf(expected)를 통해서 Integer로 들어가게 된다. 이를 이상하게 여겨 왜 expected 만 Integer가 되는지 궁금하여 찾아보니 int 가 인자로 들어오고 매개 변수가 Integer 로 자동 변환 된다고 한다. 따라서 모두 Integer로 처리 하기 된다는 것이다. 과정은 아래와 같고 자세히는 알필요 없다.


// In the case of the assertThat method, the int value is autoboxed into an Integer object when it is passed to the constructor of IntegerAssert.
  public static AbstractIntegerAssert<?> assertThat(int actual) {
    return AssertionsForClassTypes.assertThat(actual);
  }
=>
  public static AbstractIntegerAssert<?> assertThat(int actual) {
    return new IntegerAssert(actual);
  }
=>
public class IntegerAssert extends AbstractIntegerAssert<IntegerAssert> {
=>
  public IntegerAssert(Integer actual) {
    super(actual, IntegerAssert.class);
  }
}

전체적인 테스트 코드

@Test
public final void test1() {
   int actual = 1;
   int expected = 1;
   Assertions.assertThat(actual).isEqualTo(expected);
   Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}

첫번째 결과는 모두 참이 나오게 되는데 이유는 -128 ~ 127은 IntegerCache로 존재하고 오브젝트의 주소를 반환하여 사용된다.

Test 2

// Kotlin Version
@Test
fun test2() {
    val actual: Int = 1000
    val expected: Int = 1000
    assertThat(actual).isEqualTo(expected)
    assertThat(actual).isSameAs(expected)
}

// Decompile Version
@Test
public final void test2() {
   int actual = 1000;
   int expected = 1000;
   Assertions.assertThat(actual).isEqualTo(expected);
   Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}

Test1을 통해서 Test2가 왜 안되는지 이제 바로 알 수 있을 것이다. int에서 Integer로 변환 되게 되지만 -128~127 범위에 안들어가기 때문에 새로운 객체가 생성되어 반환 된다. 따라서 오브젝트끼리의 비교로 변하기 때문에 거짓이 나오는 것이다.

Test 3

Decompile을 하게 되면 항상 참이 되기에 최적화 되서 나오게 되지만 최적화 전으로 돌리게 되면 위와 같은 코드를 얻을 수 있다.

assertThat 을 들어가기 전에 비교를 하고 Boolean을 전달하고 있기 때문에 JVM의 영향으로 Integer로 변환 되지 않고 Kotlin에서 결과를 처리하게 된다.

@Test
fun test3() {
    val actual: Int = 1000
    val expected: Int = 1000
    assertThat(actual == expected).isTrue
    assertThat(actual === expected).isTrue
}
@Test
public final void test3() {
	  int actual = true;
	  int expected = true;
	  AbstractBooleanAssert var10000 = Assertions.assertThat(true);
	  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
	  var10000.isTrue();
	  var10000 = Assertions.assertThat(true);
	  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
	  var10000.isTrue();
}

Test 4

이제 Nullable로 변환 하게 되면 Java에서 int 라는 자료형은 null 을 가질 수 없기 때문에 Integer로 변환 하여 진행 하게 된다. 하지만 내부적인 IntegerCache로 인해서 또 -128 ~ 127까지 같은 오브젝트들을 반환 할 것이다.

@Test
fun test4() {
    val actual: Int? = 1
    val expected: Int? = 1
    assertThat(actual == expected).isTrue
    assertThat(actual === expected).isTrue
}

@Test
public final void test4() {
  Integer actual = 1;
  Integer expected = 1;
  AbstractBooleanAssert var10000 = Assertions.assertThat(Intrinsics.areEqual(actual, expected));
  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
  var10000.isTrue();
  var10000 = Assertions.assertThat(actual == expected);
  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
  var10000.isTrue();
}

Test 5

Test5도 nullable을 표현 하기 위해서 Integer로 들어가게 되고 IntegerCache 범위 밖의 값을 요청하기에 서로 다른 객체가 반환 된다는 것이다.


// Kotlin Version
@Test
fun test5() {
    val actual: Int? = 1000
    val expected: Int? = 1000
    assertThat(actual == expected).isTrue
    assertThat(actual === expected).isTrue
}
// Decompile Version
@Test
public final void test5() {
  Integer actual = 1000;
  Integer expected = 1000;
  AbstractBooleanAssert var10000 = Assertions.assertThat(Intrinsics.areEqual(actual, expected));
  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
  var10000.isTrue();
  var10000 = Assertions.assertThat(actual == expected);
  Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
  var10000.isTrue();
}

여담

Java에 대해서 공부를 해본 적이 없기 때문에 오브젝트끼리의 비교는 어떻게 처리하는지 궁금하여 bytecode로 뜯어보았더니 다음과 같은 결과가 나왔다.

obj1 == obj2 와 같이 두 객체를 비교할 때는 다음과 같은 bytecode를 사용하게 되는데 주소를 비교하여 값을 반환한다.

aload 1       ; load reference to obj onto the stack
aload 2       ; load reference to obj2 onto the stack
if_acmpne 8   ; jump to instruction at index 8 if obj1 != obj2
iconst_1      ; push int value 1 onto the stack (true)
goto 9        ; jump to instruction at index 9
8: iconst_0   ; push int value 0 onto the stack (false)
9:            ; continue with the next instruction
=>
if_acmpne : succeeds if and only if value1 ≠ value2

또 여담 with kotest

우연히 알게된 kotest에서는 어떻게 동작할지 궁금해서 찾아봤다.

주의 아쉽게도 Decompile이 안되서 bytecode로 해석하게 되서 보기 어려울 수 있다.

결과부터 말하자면 아래의 코드들은 모두 전멸했다.

package domain

import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeSameInstanceAs

class IntegerTest2 : ShouldSpec({
    should("test1") {
        val actual: Int = 1
        val expected: Int = actual
        actual shouldBe expected
        actual shouldBeSameInstanceAs expected
    }

    should("test2") {
        val actual: Int = 1000
        val expected: Int = 1000
        actual shouldBe expected
        actual shouldBeSameInstanceAs expected
    }

    should("test3") {
        val actual: Int = 1000
        val expected: Int = 1000
        actual shouldBe expected

        actual shouldBeSameInstanceAs expected
    }

    should("test4") {
        val actual: Int? = 1
        val expected: Int? = 1
        actual shouldBe expected
        actual shouldBeSameInstanceAs expected
    }

    should("test5") {
        val actual: Int? = 1000
        val expected: Int? = 1000
        actual shouldBe expected
        actual shouldBeSameInstanceAs expected
    }
})

스크린샷 2023-02-17 오후 10.20.41.png

이제 이유를 찾아보기 위해 아래와의 코드를 보면 모두 Boxing.boxInt로 Wrapping 하는 것을 볼 수 있다.

L3
    LINENUMBER 9 L3
    ICONST_1
    ISTORE 2
   L4
    LINENUMBER 10 L4
    ILOAD 2
    ISTORE 3
   L5
    LINENUMBER 11 L5
    ILOAD 2
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    ILOAD 3
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    INVOKESTATIC io/kotest/matchers/ShouldKt.shouldBe (Ljava/lang/Object;Ljava/lang/Object;)V
   L6
    LINENUMBER 12 L6
    ILOAD 2
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    ILOAD 3
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    INVOKESTATIC io/kotest/matchers/types/MatchersKt.shouldBeSameInstanceAs (Ljava/lang/Object;Ljava/lang/Object;)V

그래서 해당 코드도 찾아 보았다. IntegerCache따위 없이 상남자 답게 모두 Wrapping해서 내보내고 있다.

모두 통과하지 못하는 이유를 알게 되었다.

// io.kotest:kotest-assertions-shared-jvm:5.2.3
// kotlin/coroutines/jvm/internal/Boxing.boxInt
package kotlin.coroutines.jvm.internal

internal object Boxing {
    fun boxInt(i: Int): Integer = i
}

유용한 사이트 : https://youtrack.jetbrains.com boxInt

profile
4rk의 프로그래밍 스터디

0개의 댓글