Int Test에서 일어나는 동등성과 동일성

Hyemdooly·2023년 2월 20일
1

동등성? 동일성?

동등성 : 내용이 같으면 동등성 (value, isEqualTo, ==)

동일성 : 할당된 메모리 주소도 같으면 동일성 (reference, isSameAs, ===)

코드를 보자

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class IntTest {
    @Test
    fun test1() {
        val actual: Int = 1
        val expected: Int = 1
        assertThat(actual).isEqualTo(expected) // pass
        assertThat(actual).isSameAs(expected) // pass
    }

    @Test
    fun test2() {
        val actual: Int = 1000
        val expected: Int = 1000
        assertThat(actual).isEqualTo(expected) // pass
        assertThat(actual).isNotSameAs(expected) // pass
    }
// ...나머지 코드는 이 두 코드를 먼저 보고나서 보자

test2에서는 isSameAs()가 통과하지 않는 것은 확인할 수 있다. 왜일까? Java로 디컴파일해보자

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

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

Java에서 isNotSameAs/isSameAs는 인자로 Integer를 받는다는 것을 확인할 수 있다. 여기서 더 파고 들어가보자

/* Integer.java */
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) // 여기!!!
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

/* IntegerCache */
private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer[] cache;
	static Integer[] archivedCache;

	static {
		// high value may be configured by property
		int h = 127;
		//
		// 생략...
		//
		high = h;

		// Load IntegerCache.archivedCache from archive, if possible
		VM.initializeFromArchive(IntegerCache.class);
		int size = (high - low) + 1;

		// Use the archived cache if it exists and is large enough
		if (archivedCache == null || size > archivedCache.length) {
				Integer[] c = new Integer[size];
				int j = low;
				for(int k = 0; k < c.length; k++)
					c[k] = new Integer(j++);
				archivedCache = c;
		}
		cache = archivedCache;
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}

	private IntegerCache() {}
}

IntegerCache가 존재한다! 코드가 길어 조금 생략했는데, low인 -128부터 high인 127까지 Integer 객체를 미리 생성해둔다는 것을 확인할 수 있다. -128~127 사이의 값은 IntegerCach에서 이미 만들어둔 객체를 사용하고 그 이외 값은 새 Integer 객체를 반환한다.

물론 옵션을 주면 127 이상으로 범위를 조절할 수 있다. 이는 IntegerCache 클래스 위 주석에서 확인할 수 있다.

  • The cache is initialized on first usage. The size of the cachemay be controlled by the {@code XX:AutoBoxCacheMax=} option.
@Test
fun test1() {
		val actual: Int = 1
		val expected: Int = 1
		assertThat(actual).isEqualTo(expected) // pass
		assertThat(actual).isSameAs(expected) // pass
}

@Test
fun test2() {
		val actual: Int = 1000
		val expected: Int = 1000
		assertThat(actual).isEqualTo(expected) // pass
		assertThat(actual).isNotSameAs(expected) // pass
}

그렇다면 다시 코드를 보자! test1에서 actual과 expected는 1, 즉 -128~127 사이 숫자이므로 IntegerCache에서 캐싱해둔 객체를 사용한다. 두 숫자가 모두 1이니까 같은 객체이므로 테스트는 Pass된다.

하지만 test2에서 actual과 expected는 1000, -128~127 범위 이외의 숫자이므로 새로운 객체를 생성한다. 그렇다면, 두 객체는 서로 다른 객체이므로 isSameAs로 두면 Fail되었을 것이다.

assertThat에서는 Interger.valueOf()를 안한다고? int로 받고 있지만 내부에서 Integer로 변환된다.

// ...
    @Test
    fun test3() {
        val actual: Int = 1000
        val expected: Int = 1000
        assertThat(actual == expected).isTrue
        assertThat(actual === expected).isTrue // 주소값 비교
    }
// ...

그렇다면 test3은? actual과 expected는 int의 ==, ===연산을 하고난 후에 isTrue인지 검사하기 때문에 Pass된다.

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

    @Test
    fun test5() {
        val actual: Int? = 1000
        val expected: Int? = 1000
        assertThat(actual == expected).isTrue
        assertThat(actual === expected).isFalse
    }
}

test5에서 동일성 비교를 할 때 isFalse가 참이다. 왜일까? 또 디컴파일한 코드를 보자

@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
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.isFalse();
}

Kotlin의 Nullable를 처리하기 위해 내부에서 Integer인 모습을 확인할 수 있다.

위 설명과 같이, test4에서 1은 IntegerCache에서 캐싱한 값을 사용하고 test5에서 1000은 안되어있을테니 새로운 객체를 만들 것이다. 따라서 동일성 비교에서는 False이다.

0개의 댓글