동등성

동등성 (Equality)

Kotlin에는 두 가지 종류의 동등성이 있어요.

  • 구조적 동등성(Structural equality, ==) - equals() 함수를 검사해요
  • 참조 동등성(Referential equality, ===) - 두 참조가 같은 객체를 가리키는지 검사해요

출처: Kotlin 공식 문서

본문

구조적 동등성

구조적 동등성은 두 객체가 같은 내용이나 구조를 가졌는지 확인해요. 구조적 동등성은 == 연산과 그 부정인 !=로 검사합니다. 관례상 a == b 같은 표현식은 이렇게 변환돼요.

a?.equals(b) ?: (b === null)

anull이 아니면 equals(Any?) 함수를 호출해요. 그렇지 않으면(anull이면) bnull과 참조 동등한지 검사합니다.

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 == null은 자동으로 a === null로 변환되거든요.

Kotlin에서 equals() 함수는 모든 클래스가 Any 클래스로부터 상속받아요. 기본적으로 equals() 함수는 참조 동등성을 구현합니다. 하지만 Kotlin 클래스는 equals() 함수를 오버라이드해 커스텀 동등성 로직을 제공할 수 있고, 이렇게 해서 구조적 동등성을 구현할 수 있어요.

값 클래스(value class)와 데이터 클래스(data class)는 equals() 함수를 자동으로 오버라이드하는 두 가지 특정 Kotlin 타입이에요. 그래서 기본적으로 구조적 동등성을 구현합니다.

다만 데이터 클래스의 경우 부모 클래스에서 equals() 함수가 final로 표시되어 있으면 그 동작은 바뀌지 않아요.

이와 달리 데이터가 아닌 클래스(data 수정자로 선언되지 않은 클래스)는 기본적으로 equals() 함수를 오버라이드하지 않아요. 대신 Any 클래스에서 상속받은 참조 동등성 동작을 구현합니다. 구조적 동등성을 구현하려면 non-data 클래스는 equals() 함수를 오버라이드하는 커스텀 동등성 로직이 필요해요.

커스텀 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

        // Compares properties for structural equality
        return this.x == other.x && this.y == other.y
    }
}

equals() 함수를 오버라이드할 때는 동등성과 해싱 사이의 일관성을 유지하고 이 함수들의 올바른 동작을 보장하도록 hashCode() 함수도 함께 오버라이드해야 해요.

같은 이름이지만 다른 시그니처를 가진 함수(예: equals(other: Foo))는 ==!= 연산자의 동등성 검사에 영향을 주지 않아요.

구조적 동등성은 Comparable<...> 인터페이스가 정의하는 비교와는 관련이 없어요. 그러니 커스텀 equals(Any?) 구현만 연산자의 동작에 영향을 줄 수 있습니다.

참조 동등성

참조 동등성은 두 객체의 메모리 주소를 확인해서 같은 인스턴스인지 판단해요.

참조 동등성은 === 연산과 그 부정인 !==로 검사합니다. a === bab가 같은 객체를 가리킬 때에만 true로 평가돼요.

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

}

런타임에 프리미티브 타입으로 표현되는 값(예: Int)의 경우, === 동등성 검사는 == 검사와 동등해요.

참조 동등성은 Kotlin/JS에서 다르게 구현돼요. 동등성에 대한 자세한 내용은 Kotlin/JS 문서를 참고하세요.

부동소수점 숫자 동등성

동등성 검사의 피연산자가 정적으로 Float 또는 Double(nullable이든 아니든)로 알려져 있다면, 그 검사는 IEEE 754 부동소수점 산술 표준을 따릅니다.

부동소수점 숫자로 정적 타입이 지정되지 않은 피연산자에서는 동작이 달라져요. 이런 경우 구조적 동등성이 구현됩니다. 결과적으로 부동소수점 숫자로 정적 타입이 지정되지 않은 피연산자를 사용한 검사는 IEEE 표준과 다를 수 있어요. 이 시나리오에서:

  • NaN은 자기 자신과 같아요
  • NaN은 다른 모든 요소( POSITIVE_INFINITY 포함)보다 커요
  • -0.00.0과 같지 않아요

자세한 내용은 Floating-point numbers comparison 문서를 참고하세요.

배열 동등성

두 배열이 같은 순서로 같은 요소를 가지는지 비교하려면 contentEquals()를 사용해요.

자세한 내용은 Compare arrays 문서를 참고하세요.

더 알아보기 (Learn more)