Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

코틀린에서는 산술 연산자와 마찬가지로 원시 타입 값뿐 아니라 모든 객체에 대해 비교 연산을 수행할 수 있다.
- 자바에서는 객체 비교 시
equals
나compareTo
를 호출해야 함 - 코틀린에서는
==
비교 연산자를 오버로딩하여 사용 가능
이번에는 비교 연산자를 지원하는 convention(관례) 를 살펴보자.
동등성 연산자: equals
코틀린은 ==
연산자 호출을 equals
메서드 호출로 컴파일한다.
이는 다른 산술 연산자 오버로딩처럼 convention 을 적용한 것이다.
!=
연산자를 사용하는 식도 equals
호출로 컴파일된다. (비교 결과를 뒤집은 값을 결과 값으로)

==
와 !=
는 내부에서 인자가 null 인지 검사하므로 다른 연산과 달리 nullable 값에도 적용할 수 있다.
a == b
→ a 가 null 이 아니면a.equals(b)
를 호출.- a 가 null 이면 b 도 null 일 때만 true 리턴
equals
메서드 구현하기
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (other === this) return true // 최적화: 파라미터가 this 와 같은 객체인지 확인
if (other !is Point) return false // 파라미터 타입 검사
return other.x == x && other.y == y // Point 로 smart cast 해서 x 와 y 프로퍼티에 접근
}
}
assert(Point(10, 20) == Point(10, 20))
assert(Point(10, 20) != Point(5, 5))
assert(null == Point(1, 2))
- 식별자 비교 연산자(
===
)를 사용해equals
의 파라미터가 수신 객체와 같은지 살펴본다. - 식별자 비교 연산자는 자바
==
연산자와 같다. equals
를 구현할 때는===
를 사용해서 자기 자신과의 비교를 최적화하는 경우가 많다.===
는 오버로딩할 수 없다.
이미 Any
의 equals
에는 operator
가 붙어 있다.
public open class Any {
public open operator fun equals(other: Any?): Boolean
// ...
}
Any
에 이미 정의된 메서드를 override 하는 것이므로 따로 operator 를 붙이지 않아도 된다.
또한 Any 에서 상속받은 equals
가 확장 함수보다 우선순위가 높기 때문에 equals
를 확장 함수로 정의할 수 없다.
!=
호출은 equals
메서드 호출로 바뀌며, 컴파일러는 equals
의 리턴 값을 반전시켜서 돌려준다.
우리가 따로 !=
을 처리하기 위해 해야 할 일은 없다.
순서 연산자: compareTo
자바에서 비교를 수행하는 클래스는 Comparable
인터페이스를 구현해야 한다.
자바는 >
혹은 <
연산자로는 원시 타입의 값만 비교할 수 있다.
다른 모든 타입은 element1.compareTo(element2)
처럼 명시적으로 써야 한다.
Comparable
에 들어있는compareTo
메서드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내준다. (이 메서드와 관련된 설명)
java닫기
public interface Comparable<T> { public int compareTo(T o); }
코틀린도 똑같은 Comparable
인터페이스를 지원한다.
코틀린은 Comparable
인터페이스 안에 있는 compareTo
메서드를 호출하는 convention 을 제공한다.
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
따라서 비교 연산자 <
,>
,<=
,>=
는 compareTo
호출로 컴파일된다.
Comparable
인터페이스의 compareTo
에 operator
가 이미 붙어 있다.

compareTo
는 Int
를 리턴한다.
compareTo
메서드를 Person
에 구현하기
class Person(val firstName: String, val lastName: String) : Comparable<Person> {
override fun compareTo(other: Person): Int =
compareValuesBy(this, other, Person::lastName, Person::firstName) // 인자로 받은 함수를 차레로 호출하면서 값 비교
}
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
assertFalse { p1 < p2 }
여기서 정의한 Person
객체의 Comparable
인터페이스는 코틀린 뿐 아니라 자바 쪽에서도 사용할 수 있다.
compareValuesBy
함수는 두 객체와 여러 비교 함수를 인자로 받는다.
첫 번째 비교 함수에 두 객체를 넘겨서 두 객체가 같지 않다는 결과가 나오면 값을 즉시 리턴하고, 두 객체가 같다는 결과가 나오면, 두 번째 비교 함수를 통해 두 객체를 비교하는 식으로 모두 비교한다.
각 비교함수는 람다나 프로퍼티/메서드 참조일 수 있다.
필드를 직접 비교하면 코드는 더 복잡해지지만, 비교 속도는 훨씬 더 빨라진다.
처음에는 성능에 신경 쓰지 말고 이해하기 쉽고, 간결하게 코딩하자.
나중에 성능이 문제가 되면 성능을 개선하자.
Comparable
인터페이스를 구현하는 모든 자바 클래스를 코틀린에서는 간결한 연산자 구문으로 비교할 수 있다.
즉, 비교 연산자를 자바 클래스에 대해 사용하기 위해 특별히 확장 메서드를 만들 필요는 없다.
compareTo 규약을 지켜라
compareTo
는 아래와 같이 동작해야 한다.
- 비대칭적 동작
a >= b
이고,b >= a
라면,a == b
여야 한다.
비교와 동등성 비교에 어떠한 관계가 있어야하며, 서로 일관성이 있어야 한다.
- 연속적 동작
a >= b
이고b >= c
라면,a >= c
여야 한다.
마찬가지로a > b
이고,b > c
라면a > c
여야 한다.
이러한 동작을 하지 못하면, 원소 정렬이 무한 loop 에 빠질 수 있다.
- 코넥스적 동작(connex relation)
- 두 원소는 어떤 확실한 관계를 가져야 한다.
즉,a >= b
또는b >= a
중, 적어도 하나는 항상 true 여야 한다.
두 원소 사이에 관계가 없으면, 퀵 정렬과 삽입 정렬 등의 정렬 알고리즘을 사용할 수 없다.
대신 위상 정렬(topological sort) 와 같은 정렬 알고리즘만 사용할 수 있다.
- 두 원소는 어떤 확실한 관계를 가져야 한다.
compareTo 를 따로 정의하지 않아도 되는 경우
코틀리에서는 일반적으로 어떤 프로퍼티 하나를 기반으로 순서를 지정하는 것으로 충분하다.
sortedBy
: surname
프로퍼티를 기반으로 정렬하는 예
data class User(val name: String, val surname: String)
val names = listOf(User("Alice", "Smith"), User("Bob", "Johnson"))
val sorted = names.sortedBy { it.surname }
assert(sorted == listOf(User("Bob", "Johnson"), User("Alice", "Smith")))
그렇다면 여러 프로퍼티를 기반으로 정렬해야 한다면 어떻게 할까?
그럴 때는 sortedWith
함수를 사용하면 된다.
compareBy
를 활용해서 comparator(비교기)를 만들어서 사용한다.
sortedWith
: surname
으로 정렬하고, 같다면 name 까지 비교해서 정렬하는 예
val names = listOf(User("Alice", "Smith"), User("Bob", "Smith"))
val sorted = names.sortedWith(compareBy({ it.surname }, { it.name }))
assert(sorted == listOf(User("Alice", "Smith"), User("Bob", "Smith")))
위와 달리 User
가 Comparable<User>
를 구현하는 형태로 만들 수도 있다.
특정 프로퍼티를 기반으로 정렬하게 하면 된다.
예를 들어 문자열은 알파벳과 숫자 등의 순서가 있다.
그래서 내부적으로 Comparable<String>
을 구현하고 있다.
일반적으로 텍스트는 알파벳과 숫자 순서로 정렬해야 하는 경우가 많아서 유용하다.
하지만 단점도 있다.
예를 들어 직관적이지 않은 부등호 기호로 두 문자열을 작성하는 코드는 이해하는 데 약간 시간이 걸린다.
그래서 Effective kotlin 에서는 아래처럼 하지 말라고 권고한다.
// 이렇게 하지 말라고 권고함
println("Kotlin" > "Java") // true
측정 단위, 날짜, 시간 등의 객체는 자연스러운 순서를 갖는다.
만약 객체가 자연스러운 순서인지 확실하지 않다면 Comparable 을 바로 구현하지 말고 comparator(비교기) 를 사용하자.
이를 자주 사용한다면, 클래스에 동반 객체로 만들어 두는 것이 좋다.
companion object 를 사용하여 comparator 만들어두기
data class User(val name: String, val surname: String) {
// ...
companion object {
val DISPLAY_ORDER = compareBy(User::surname, User::name)
}
}
val names = listOf(User("Alice", "Smith"), User("Bob", "Smith"))
val sorted = names.sortedWith(User.DISPLAY_ORDER)
assert(sorted == listOf(User("Alice", "Smith"), User("Bob", "Smith")))
compareTo 구현하기
compareTo
를 구현할 때 유용한 최상위 함수가 있다.
compareValues
: 두 값을 단순하게 비교하기만 하는 경우
class User1(val name: String, val surname: String) : Comparable<User> {
override fun compareTo(other: User): Int = compareValues(surname, other.surname)
}
compareValuesBy
: 더 많은 값을 비교하거나 selector(선택기)를 활용해서 비교하고 싶은 경우
class User2(val name: String, val surname: String) : Comparable<User2> {
override fun compareTo(other: User2): Int = compareValuesBy(this, other, { it.surname }, { it.name })
}
이 compareValuesBy
함수는 위에서도 사용했다.
compareValuesBy
함수는 사실 내부적으로 compareValues
를 for 문 을 돌면서 여러 번 실행하는 식으로 구현되어 있다.
compareTo
함수는 아래와 같은 값을 리턴해야 한다.
- 0: 수신 객체(receiver)와
other
가 같은 경우 - 양수: 수신 객체가
other
보다 큰 경우 - 음수: 수신 객체가
other
보다 작은 경우
이를 구현한 뒤에는 이 함수가 비대칭적 동작, 연속적 동작, 코넥스적 동작을 하는지 확인하자!
'Kotlin' 카테고리의 다른 글
[Kotlin] 구조 분해 선언과 component 함수 (0) | 2024.01.24 |
---|---|
[Kotlin] 컬렉션과 범위에 대해 쓸 수 있는 convention(관례) (1) | 2024.01.24 |
[Kotlin] 산술 연산자 오버로딩 (1) | 2024.01.23 |
[Kotlin] 객체 타입의 배열 & 원시 타입의 배열 (0) | 2024.01.22 |
[Kotlin] 코틀린,자바 컬렉션과 nullability, 변경 가능성 (0) | 2024.01.22 |