Idealim
article thumbnail

/* 본 게시물은 참고자료의 내용을 토대로 작성되었습니다. */

/* 본 글은 개인적으로 공부한 내용을 정리한 글이므로 오류가 있을 수 있습니다. */

참고 자료

[쾌락 코딩] 코틀린 동등성 연산 : https://wooooooak.github.io/kotlin/2019/02/24/kotiln_%EB%8F%99%EB%93%B1%EC%84%B1%EC%97%B0%EC%82%B0/

[NATION OF 6KIKI] ==, compareTo(), equals() 서로간의 차이점에 대해 알아보자 : https://6kkki.tistory.com/9
[김성일] Comparable / Comparator 인터페이스 차이점 : https://dev-daddy.tistory.com/23


객체에서 숫자, 문자열 비교를 하려면 어떻게 해야할까? 객체를 비교하기 위해서는 동등성의 개념과 Comparable 인터페이스의 이해가 필요하다. 우선, 코틀린의 동등성 연산에 대해 알아보자.


코틀린 동등성 연산(== / ===) 

코틀린에서의 == 는 자바와는 조금 다른 부분이 있다. 원시 타입 두개를 비교할 때는 == 연산자는 동일(값을 비교)하지만, 참조 타입을 비교할 때는 다르게 동작한다. 자바와 코틀린 코드의 예시를 보면서 이해해보자.

자바

String a = "hello world!" // 주소값 : 1
String b = "hello world!" // 주소값 : 2

System.out.println(a==b) // false
System.out.println(a.equals(b)) // true

코틀린

val a: String = "hello world!"
val b: String = "hello world!"

println(a == b) // true
println(a === b) // true

kotlin bytecode decompile

코틀린에서 == 는 내부적으로 equals 를 호출한다. 따라서 참조 타입인 두개의 String을 == 연산으로 비교하면 주소값이 아닌 값(동등성)비교를 한다.

 

그렇다면 주소값을 비교하고 싶을 때는 어떻게 해야할까?

=== 연산자를 이용하면 된다. 참조 비교를 할 때는 === 연산자를 사용한다. 즉, 자바의 주소 값 비교인 == 와 코틀린의 === 가 동일한 역할을 한다.

 

여기서 잠깐!
그러면 String 객체 a,b 에 대하여 a === b 는 false 가 되어야하지 않나요? 라고 생각할 수 있다. 하지만 Stirng 객체는 String pool 이라는 힙의 영역이 따로 존재하여 값을 String pool에 저장한다. 만약 이미 String pool 에 같은 값이 존재한다면 같은 주소를 참조하는 방식이다. 그래서 a === b 값이 true를 반환하게 된다. 즉, String class 에는 equals 를 객체의 equals 를 override(재정의) 한 것이다. String 자료형에 대한 자세한 내용은 이 글을 확인하자.
class A

val c = A()
val d = A()

println(c == d) // false
println(c === d) // false

* 객체에서 equals 는 주소 비교 와 같다. 객체의 값을 비교하려면 equals 를 override 해야 한다.

 

다음 코드를 이해하면 코틀린의 동등성은 완벽히 이해했다고 보면 된다. (코드 출처)

class UserA(var id: String)

var user1 = UserA("0")
var user2 = UserA("0")

if (user1 == user2) >> false
if (user1 === user2) >> false

class UserB(var id: String) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        other as UserB
        if (id != other.id) return false
        return true
    }
    override fun hashCode(): Int {
        return id.hashCode()
    }
}
var user3 = UserB("1")
var user4 = UserB("1")
if (user3 == user4) >> true
if (user3 === user4) >> false

*equals를 override 하기 위해서는 hashCode 도 override 해야됨.


Comparable 인터페이스

객체에서 원하는 자료형으로 비교하기 위해, Comparable 인터페이스를 사용할 수 있다. Comparable 인터페이스는 같은 자료형의 다른 객체 하나를 인자로 받아와 비교하는 compareTo 함수를 사용한다. a.comparable(b) 는 a가 b보다 작을 때는 0보다 작은 수, a와 b가 같으면 0, a가 b보다 크면 0보다 큰 수를 반환한다. Comparable 인터페이스는 다음과 같이 구현되어 있다.

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

Comparable을 상속받는 클래스를 한 번 만들어보자.

class User(var id: Int, var name: String) : Comparable<User> {
	// id로 비교
    override fun compareTo(other: User): Int =
        when {
            this.id > other.id -> 1
            this.id < other.id -> -1
            else -> 0
        }
}

fun main() {
    val user1 = User(1, "민수")
    val user2 = User(2, "영희")

    println(user1 > user2) // false
    println(user1 < user2) // true
    println(user1 == user2) // false
    println(user1.compareTo(user2)) // -1

}

Comparable 를 상속받아서 compareTo 를 override 한 경우 객체간의 >, < 비교를 지원한다.

String 자료형에서 재정의되있는 compareTo 는 주소값에 상관없이 값만을 비교하며 같은 때는 0, 다를 땐 같은 인덱스에서 다른 문자가 처음 나온 문자들의 차이의 값(앞파벳 순으로 기준 값이 큰 경우 양수, 작으면 음수)을 return한다.

그 외에도 Boolean, Byte, Int, Duration 등 Comparable 을 상속받기 때문에 연산자를 통해 비교가 가능하다. (관련 링크)


Comparator 인터페이스

Comparator 는 Comparable 와 마찬가지로 비교(정렬) 규칙을 설정하기 위해 사용하는 목적은 동일하다. 둘의 차이점은 기본적으로 인터페이스가 영향을 미치는 범위와 그에 따른 사용목적이 다르다는 점이다. Comparable 의 예시 코드를 확인하면 Comparable 인터페이스는 객체 스스로에게 상속하여 한 가지 기본 정렬 규칙을 설정한다. 위 예시처럼 id 로 비교하는 방법 말고 이름으로 비교하는 방법을 추가할 수는 없을까? 이 때 Comparator를 사용하면 된다. Comparator는 정렬 규칙을 정한 클래스로, 기본 정렬 규칙과 다르게 정렬 순서를 지정하고 싶을 때 사용한다. 코드를 통해 알아보자. 

object SortByName : Comparator<User>{
    override fun compare(user1: User, user2: User): Int {
        // 오름차순
        return user1.name.compareTo(user2.name)
        // 내림차순
        //return user2.name.compareTo(user1.name)
    }
}

fun main(){
    val user1 = User(1, "영희")
    val user2 = User(2, "민수")
    val user3 = User(3, "가가")

    arrayListOf<User>(user1, user2, user3).apply{
        println("sort by age")
        this.sort()
        this.forEach{
            println("${it.id} ${it.name}")
        }
    }

    arrayListOf<User>(user1, user2, user3).apply {
        println("sort by name")
        this.sortWith(SortByName)
        this.forEach {
            println("${it.id} ${it.name}")
        }
    }
}

*sortWith 의 파라미터로 Comparator 객체를 넘겨줘야한다. 

*compare() 는 compareTo() 와 다르게 파라미터로 기준 객체와 비교 객체가 필요하다. 

첫 번째는 arrayList 에 정의되어 있는 Comparable 를 이용해 정렬되었다. 두 번째는 Comparator를 이용해 오름차순의 이름으로 정렬되었다.


정리

Kotlin 에서 equals, == 는 내부적으로 동일하며 주소값에 상관없이 값만을 가지고 비교하며 같을 땐 true, 다를 땐 false를 return 한다. (*단, Object.equals() 는 주소 값 비교)

 

compareTo는 주소값에 상관없이 값만을 가지고 비교하며 같을 땐 0을, 다를 땐 같은 인덱스에서 다른 문자가 처음 나온 문자들의 차이의 값을 return 한다.

 

Comparable 는 이를 상속받아 구현한 객체 스스로에게 부여하는 한 가지 기본 정렬 규칙을 설정하는 목적으로 사용한다.

 

Comparator 는 상속받은 클래스는 정렬 규칙 그 자체를 의미하며, 기본 정렬 규칙과 다른 정렬순서를 지정하고 싶을 때 사용한다.

 

반응형
profile

Idealim

@Idealim

읽어주셔서 감사합니다. 잘못된 내용이 있으면 언제든 댓글로 피드백 부탁드립니다.