026. 비교 연산자 오버로딩

비교 연산자 오버로딩

코틀린은 원시타입뿐만 아니라 모든 객체에 대해 비교 연산자를 사용할 수 있다.

이번엔 이 비교연산자를 오버로딩해보자.

동등성 연산자 equals의 오버로딩

코틀린에서 == 연산자는 동등성을 비교함을 의미한다.

컴파일러는 == 연산자를 equals 함수로 변환해서 처리하는데, 이는 == 연산자의 오버로딩은 equals 함수의 오버로딩이라는 것을 추측할 수 있게 해준다.

== 연산자와 정반대 역할을 하는 != 연산자도 동일하게 equals를 호출하지만 마지막에 반대의 결과를 돌려주는 방식으로 동작한다.

또한 다른 연산자의 오버로딩때와는 다르게 ==!= 연산자를 함께 사용할 수 있다.

== 연산자를 a == b형태로 호출하게되면 먼저 a가 null인지 검증한 후, null아 아니면 a.equals(b)를 호출하는 형태이다.

참고 a == b -> a?.equals(b) ?: (b == null)

산술 연산자 오버로딩에서 활용한 예제인 좌표 객체 Point 클래스를 다시 가져와보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point(val x: Int, val y: Int) {
override fun equals(obj: Any?): Boolean {
if (obj === this) return true
if (obj !is Point) return false
return obj.x == x && obj.y == y
}
}

/* 출력 결과 */
>>> println(Point(10, 20) == Point(10, 20))
true
>>> println(Point(10, 20) != Point(5, 5))
true
>>> println(null == Point(1, 2))
false

위의 예제에서 override 키워드가 붙어있음을 확인할 수 있는데, 이는 equals함수가 Any클래스에 기존 정의된 함수이기 때문이다.

상속 받은 함수는 확장 함수보다 우선하기 때문에 equals는 확장 함수로 정의할 수 없다.

또한 Any클래스내에 정의된 equals 함수에 이미 operator 키워드가 붙어 있어 생략할 수 있다.

아래는 Any.kt 클래스의 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package kotlin

/**
* The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
*/
public open class Any {

public open operator fun equals(other: Any?): Boolean

public open fun hashCode(): Int

public open fun toString(): String
}

참고 === 연산자를 사용하여 동일성 검증을 하는 방법이 있으나 이는 오버로딩이 불가능한 연산자이다.
Equality 문서

순서 연산자 compareTo의 오버로딩

Java에서는 객체간의 (주로 최대/최소값 찾이나 정렬를 위해 사용하는)비교를 위한 인터페이스를 구현할 수 있다.

참고 Comparable 인터페이스다. Oracle Comparable 문서

코틀린도 Java와 동일하게 Comparable 인터페이스를 제공하고 있다.

따라서 <, >, <= >=는 모두 compareTo 함수로 컴파일 되어 Int를 반환한다.

참고 a >= b -> a.compareTo(b) >= 0

Person 클래스를 통해 어떻게 compareTo를 구현할 수 있는 지 확인해보자.

Person 클래스들 간의 비교 규칙은 lastName에 저장된 성을 먼저 비교한 뒤,

성이 같으면 firstName에 저장된 이름을 비교하는 방식으로 진행된다.

1
2
3
4
5
6
7
8
9
10
11
class Person(val firstName: String, val lastName: String) :  Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName, Person::firstName)
}
}

/* 출력 결과 */
>>> val p1 = Person("Alice", "Smith")
>>> val p2 = Person("Bob", "Johnson")
>>> println(p1 < p2)
false

보통 Java에서는 사용자가 compareTo 메소드를 전부 구현해야하지만 코틀린의 표준 라이브러리에서 compareValuesBy라는 함수를 제공해주므로 이를 활용해 손쉽게 구현할 수 있다.

compareValuesBy 함수는 비교를 위한 같은 타입의 두 객체를 받고, selectors를 통해 비교 우선순위를 넘겨받아 비교 결과를 콜백으로 반환해준다.

아래 실제 코틀린 코드를 첨부해두었으니 참고하면 이해가 빠를 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Comparisons.kt
public fun <T> compareValuesBy(a: T, b: T, vararg selectors: (T) -> Comparable<*>?): Int {
require(selectors.size > 0)
return compareValuesByImpl(a, b, selectors)
}

private fun <T> compareValuesByImpl(a: T, b: T, selectors: Array<out (T) -> Comparable<*>?>): Int {
for (fn in selectors) {
val v1 = fn(a)
val v2 = fn(b)
val diff = compareValues(v1, v2)
if (diff != 0) return diff
}
return 0
}

만약 Java로 참조중인 객체를 비교할 때에도 해당 객체가 Comparable 인터페이스를 구현한 상태라면 아래와 같이 손쉽게 비교할 수 있다.

즉, 별도의 확장 작업 없이 동작시킬 수 있다.

1
2
>>> println("abc" < "bac")
true