동등성 : 내용이 같으면 동등성 (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이다.