본문 바로가기

코틀린

코틀린: 연산자 오버로딩(2) - equals()

연산자 오버로딩(operator overloading)에 관한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 2.2 연산자 오버로딩을 참고하기 바랍니다. 연산자 오버로딩에서 무심코 지나치면 안되는 내용 중 복합 대입연산자부터 먼저 알아보았습니다. 이번에는 equals()에 대해 알아보겠습니다. equals()는 연산자 "==" 또는 "!="를 사용했을 때 호출되는 함수입니다. 

연산자 오버로딩의 일반적 구문은 "operator fun plusAssign() ..." 이지만, equals()는 특이하게 "override fun equals() ..." 를 사용합니다. 다음에 설명할 compareTo()도 "override fun compareTo() ... "를 사용합니다. 

■  Any 클래스에서 정의한 연산자 오버로딩 메소드: equals()
연산자 오버로딩을 위해 equals()를 정의할 때, equals()의 형식인자 타입은 Any 입니다. 클래스 Any는 널(null)이 될 수 없는 타입 중 최상위 클래스입니다. 어떤 타입을 비교할지 모르기 때문에 형식인자 타입을 최상위 타입으로 선언한 것입니다. 널이 될 수 있는 타입의 최상위 클래스는 Any? 입니다.  클래스 Any는 equals()를 멤버 함수로 정의합니다.

public open class Any {
    public open operator fun equals(other: Any?): Boolean
    public open fun hashCode(): Int
    public open fun toString(): String
}

간단히 테스트해 보겠습니다. Any 타입 객체는 속성이 없습니다. "a == b"는 두 개의 Any 타입 객체 a, b가 같은 메모리 공간을 참조하고 있는지 비교합니다. equals()는 Boolean 값을 반환하기 때문에 비교 결과는 true 또는 false입니다.

fun main() {
    val a = Any()    // a의 타입은 Any
    val b = a        // b의 타입도 Any
    println(a == b)  // true: 같은 객체를 참조하고 있습니까? 
    println(a != b)  // false: 다른 객체를 참조하고 있습니까?
}

 

■  사용자 정의 클래스에서 equals() 구현
Any에서 상속받은 equals()가 확장 함수보다 우선 순위가 높기 때문에, equals()는 확장 함수로 정의할 수 없습니다. equals()를 확장 함수로 정의하면 에러가 발생합니다. equals()는 반드시 클래스의 멤버 함수로 정의해야 합니다.

사용자 정의 클래스 Person에서 연산자(==) 오버로딩을 위해 equals()를 정의하려면 키워드 override를 붙여야 합니다. equals()는 최상위 클래스 Any에서 정의된 메소드이기 때문입니다. 클래스 Any에서는 키워드 operator를 사용해 equals()를 연산자 오버로딩 메소드로 정의했습니다. 따라서, 클래스 Person에서 equals()를 재정의할 때 override operator fun equals(...) 와 같이 키워드 operator를 붙이지 않아도 됩니다(키워드 operator를 포함해도 에러는 아닙니다). 

equals() 블록에서 가장 먼저 if (this === other) ... 를 사용해 2개의 객체가 같은 메모리 주소를 참조하는지 조사합니다. "==="는 참조 동일(referential equality) 연산자, "=="는 구조 동일(structural equality) 연산자입니다. 이어서 비교 대상 객체 other가 수신 객체(this)와 같은 타입인지 비교합니다. 마지막으로 객체의 속성이 같은지 비교합니다.

class Person(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Person) return false
        return this.name == other.name && this.age == other.age
    }
}

fun main() {
    val hong1 = Person("Hong", 22)
    val hong2 = Person("Hong", 22)
    val kim = Person("Kim", 24)

    println(hong1 == hong2)  // true: equals() 메소드 호출
    println(hong1 != kim)    // true: equals() 메소드 호출
    println(hong1 === hong2) // false: hong1과 hong2는 서로 다른 객체
}

 

■  hashCode() 함수 추가
equals()를 구현할 때는 hashCode()도 함께 구현할 것을 권고하고 있습니다.

class Person(val name: String, val age: Int) {
    override operator fun equals(other: Any?): Boolean { ... }
    override fun hashCode(): Int {
        return name.hashCode() * 31 + age
    }
}

 

■  돌발 퀴즈 1: equals()를 구현하지 않고 연산자 ==를 사용하면?
equals()를 정의하지 않고 연산자 "=="를 사용해 비교하면 어떻게 될까요? equals()를 구현했을 때와 비교 결과가 정반대입니다. 왜 그럴까요? 코틀린은 Any에서 정의한 equals() 메소드의 기본 구현을 적용하기 때문입니다. 기본 구현이란 바로 두 개의 객체가 같은 곳을 참조하는지 (===)를 조사합니다. 따라서, "=="를 사용하거나 "==="를 사용해도 결과는 같습니다.

class Person(val name: String, val age: Int)

fun main() {
    val hong1 = Person("Hong", 22)
    val hong2 = Person("Hong", 22)

    println(hong1 == hong2)   // false
    println(hong1 === hong2)  // false
}

 

■  돌발 퀴즈 2: 비교 대상이 널(null)이면 비교 결과는 true일까요 false 일까요?
 a == b는 내부적으로 a?.equals(b) ?: (b == null) 과 같이 처리됩니다. 간단하게 요약하면, equals()로 비교할 객체 파라미터로 널 객체를 전달할 수 있지만, 두 객체 중 어느 하나만 널(null)이면, 비교 자체가 의미가 없기 때문에 결과는 항상 false 입니다. 재미있는 점은 두 객체가 모두 널이면 비교 결과는 true 입니다. 

class Person(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean { ... }
    override fun hashCode(): Int { ... }
}

fun main() {
    val hong1 = Person("Hong", 22)
    val hong2 = null
    val hong3 = null

    println(hong3 == hong1)  // false. 수신 객체(hong3)가 널.
    println(hong1 == hong2)  // false. 수신 객체(hong1)는 널이 아니지만, 비교 객체 hong2가 널.
    println(hong2 == hong3)  // true. 두 객체가 모두 널일때
}