서른일곱 번째, 동등성입니다.
Kotlin에는 두 가지 유형의 동등성이 있습니다.
- 구조적 동등성 ( == ) - equals() 함수를 사용한 검사
- 참조적 동등성( === ) - 두 개의 참조가 같은 객체를 가리키고 있는지 검사
동일성 vs 동등성
보통 두 대상이 같은지 비교하는 데는 참조적으로 같은 동일성과 구조적으로 같은 동등성을 얘기합니다. 이와 관련해서 동일성 쪽에는 identity를 동등성 쪽에는 equality라는 단어를 사용하는 경우가 국내 및 영어권에 많이 있습니다. 하지만, 보통 equality 한데 이게 어떤 식으로 equality 하냐고 정의하는 경우도 많습니다.
예를 들어 Java 언어 명세에 보면 == 연산자를 단순히 equality operator라고 있습니다. 이 걸 우리말로 동일성과 동등성으로 구분하는 상황 하에서 동등성 연산자라고 얘기하면 뜻이 와전돼 버립니다. JavaScript 같은 경우도 ECMA-262 3rd 명세에 보면 == 는 equality, ===는 strict equality라고 표현하고 있습니다(당연히, Kotlin 언어 명세도 equality 하나로 얘기하고 있습니다).
영한 사전의 경우 eqaulty는 평등성으로 나오는 경우가 많습니다. 즉, 동등성을 얘기합니다. 하지만, 형용사 equal은 사전에서도 동일하다는 뜻이 우선시 되고 우리도 일상에서 equal을 동일하다는 뜻으로 많이 사용합니다. 그래서, 동일성, 동등성 관련해서 이 문서처럼 equality 하나의 단어만 사용하고 구조적으로 equality 하냐 참조적으로 equality 하냐라고 언급하는 경우에는 '동일성'으로 표현(번역) 하는 것이 더 자연스럽다고 생각됩니다. 구조적 동일성, 참조적 동일성.
다만, 이미 너무 많은 곳에서 동일성과 동등성을 구분해서 쓰는 상황에서 equality를 동등성으로 번역하고 있기 때문에 (혼란을 피하기 위해) 여기서도 동등성으로 표현합니다.
구조적 동등성 (Structural equality)
구조적 동등성은 두 개의 객체가 같은 내용이나 구조를 가지고 있는지 확인합니다. 구조적 동일성은 == 연산으로 검사됩니다. 이에 대한 부정형은 != 입니다. 관례적으로 a = b 같은 표현식은 다음과 같이 변환됩니다.
a?.equals(b) ?: (b === null)
a가 널이 아니면 equals(Any?) 함수를 호출합니다. a가 널이면 b가 참조적으로 null과 같은지 검사합니다.
fun main() {
var a = "hello"
var b = "hello"
var c = null
var d = null
var e = d
println(a == b)
// true
println(a == c)
// false
println(c == e)
// true
}
null과 명시적으로 비교할 때는 위쪽에 변환되는 코드처럼 특별히 최적화 되는 부분이 없습니다. a == b는 자동으로 a === b로 변환됩니다.
Kotlin에서 equals() 함수는 Any로부터 모든 클래스에 상속됩니다. 기본적으로 equals는 참조적 동등성으로 구현됩니다. 하지만, Kotlin에서는 맞춤화된 동등성 로직을 equals() 함수를 오버라이딩하여 제공할 수 있습니다. 이런 방법으로 구조적 동등성을 구현할 수 있습니다.
값 클래스(value classes)와 데이터 클래스는 자동으로 equals가 오버라이딩 되는 두 개의 특별한 타입입니다. 이로 인해 기본적으로 구조적 동등성 구현합니다.
데이터 클래스의 경우에는 부모 클래스에 equals가 final로 지정돼 고정돼 있습니다
데이터 클래스가 아닌 일반 클래스는 기본적으로 equals를 오버라이딩 하지 않습니다. 대신에, (앞서 언급한 것처럼) 참조적 동등성을 비교하는 구현을 Any로부터 상속받습니다. 구조적 동등성을 구현하기 위해서는 equals() 함수를 오버라이딩하는 맞춤화된 로직이 필요합니다.
맞춤화된 로직을 구현할 때는 equals(other: Any?): Boolean 함수를 오버라이딩합니다.
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
// 구조적 동등성을 위한 프로퍼티 비교
return this.x == other.x && this.y == other.y
}
}
equals()를 오버라이딩 할 때는 hashCode() 함수도 오버라이딩 해야 합니다. 그렇게 해야 동등성과 해싱 사이에 일관성을 유지할 수 있고, 이 함수들이 제대로 동작할 것을 보장할 수 있습니다(equals()에 의해 같다는 두 객체는 같은 해시코드를 가져야 합니다. 즉, hashCode() 실행 결과가 같아야 합니다. 상세 내용은 여기에서 hashCode와 equals 부분을 참고하세요).
equals(other: Foo) 같이 equals와 이름만 같고 시그니처가 다른 함수는 ==나 != 연산자의 동등성 검사에 영향을 미치지 않습니다.
구조적 동등성은 Comparable<...> 인터페이스로 정의되는 비교와는 아무런 관련이 없습니다. 단지, equals(Any?) 구현만이 연산자 행동에 영향을 미칩니다.
참조적 동등성
참조적 동등성은 두 객체가 같은 인스턴스인지 결정하기 위해 둘의 메모리 주소를 확인합니다.
참조적 동등성은 === 연산으로 검사됩니다. 이에 대한 부정형은 !== 입니다. a === b 는 a와 b가 같은 객체를 가리키고 있는 경우 true로 평가(evaluation) 됩니다.
fun main() {
var a = "Hello"
var b = a
var c = "world"
var d = "world"
println(a === b)
// true
println(a === c)
// false
println(c === d)
// true
}
실행 시간에 원시 타입으로 표현되는 값들에 대해서 === 동등성 검사는 == 검사와 동일합니다.
※ Kotlin/JS에서 참조적 동등성은 다르게 구현됩니다. 보다 자세한 정보는 Kotlin/JS 문서를 참고하시기 바랍니다.
부동 소수점 숫자의 동등성
동등성 검사의 피연산자(operand)가 (null 인지 상관 없이) Float나 Double이라고 정적으로 알려진 경우에는 부동 소수점 연산에 대한 IEEE 754 표준을 따라서 검사합니다.
피연산자가 부동 소수점 숫자로서 정적 타입이 아닌 경우에는 동등성 비교가 다릅니다. 이런 경우에는 구조적 동등성이 구현됩니다. 그래서, 이런 경우에는 IEEE 표준과 다릅니다. 다음과 같은 경우입니다.
- NaN은 그 자체와 같습니다.
- NaN은 POSITIVE_INFINITY를 포함한 그 어떤 것보다 큽니다.
- -0.0은 0.0과 같지 않습니다.
보다 자세한 내용은 부동 소수점 비교 부분을 보시기 바랍니다.
* 이 내용은 부동 소수점 비교를 보는 것이 좋습니다. 여기에 있는 얘기만으로는 이해가 어렵습니다.
배열 동등성
두 개의 배열이 같은 항목(element)들을 같은 순서로 가지고 있는지 비교할 때는 contentEquals()을 사용합니다.
보다 자세한 내용은 배열 비교에서 확인할 수 있습니다.
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 코틀린 - 39. Annotations (0) | 2024.03.16 |
---|---|
공식 문서로 배우는 코틀린 - 38. This expressions (0) | 2024.03.16 |
공식 문서로 배우는 코틀린 - 36. Null safety (0) | 2024.03.15 |
공식 문서로 배우는 코틀린 - 35. Using builders with builder type inference (0) | 2024.03.15 |
공식 문서로 배우는 코틀린 - 34. Type-safe builders (2) | 2024.03.15 |