연산자 오버로딩(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. 두 객체가 모두 널일때
}
'코틀린' 카테고리의 다른 글
코틀린 : 2장 테스트에 도전해 보세요 (0) | 2025.01.17 |
---|---|
코틀린: 연산자 오버로딩(3) - compareTo() (0) | 2025.01.17 |
코틀린: 연산자 오버로딩(1) - 복합 대입 연산자 (0) | 2025.01.15 |
코틀린: 연산자 in과 범위(updated) (0) | 2025.01.06 |
코틀린: 식과 문장 (0) | 2025.01.05 |