본문으로 바로가기
반응형

이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

자바의 클래스는 toString, equals, hashCode를 반드시 오버라이드 해야 합니다.

코틀린에서는 이를 자동으로 컴파일러에서 해줍니다. (따로 만드는 수고스러움을 덜수 있습니다~)


4.3.1 클래스의 기본 method 구현

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

fun main(args: Array) {
    val client1 = Client("Alice", 342562)
    println(client1)
}

자바는 기본적으로 Client의 toString()을 구현해 줍니다.

다만 print 했을때 Client@d97g8cs 란 쓸모없는 값을 리턴해 줍니다.

따라서 위 코트처럼 override하여 유요향 정보를 출력하도록 toString()을 재정의 할 수 있습니다.


class Client(val name: String, val postalCode: Int)

fun main(args: Array) {
    val client1 = Client("Alice", 342562)
    val client2 = Client("Alice", 342562)
    println(client1 == client2)
}


코틀린에서 == 는 자바에서 equals와 같습니다.

따라서 두개의 결과로 true가 나와야 하지만 false가 나옵니다.

true로 반환값을 만들기 위해서는 equals()도 override해야 합니다.

class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client)
            return false
        return name == other.name &&
               postalCode == other.postalCode
    }
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

fun main(args: Array) {
    val processed = hashSetOf(Client("Alice", 342562))
    println(processed.contains(Client("Alice", 342562)))
}

각 property를 비교하도록 equals를 수정했습니다.

따라서 이제 client1 == client2는 true가 나옵니다. (equals를 override 했으니, == 연산시 override된 함수를 사용합니다.)

단!! main 함수를 실행시키면 hasSet에 해당 객체가 없다면 false를 반환합니다.

이는 hashcode가 없기 때문입니다. 따라서 equals를 override한다면 hashcode도 반드시 override 해야 합니다.

이는 "JVM의 언어에서는 equals()가 true인 객체는 반드시 hashcode() 값도 동일해야 한다"는 제약이 있기 때문입니다.


hashSet은 객체를 찾을때 먼저 hashcode를 이용하여 찾습니다. 그리고 나서 hashcode가 같은게 여러개 존재 한다면 그때서야 값을 비교합니다.

위 코드에서는 두 객체의 hashcode가 다르니, contains에서 false가 떨어집니다.

override fun hashCode() : Int = name.hashCode() * 31 + postalCode

따라서 위와같이 hashCode()도 override해야만 비교시 정상 동작합니다.


4.3.2 data class

앞절에서 여러 얘기를 한 이유는 이런것들을 한번에 제공하는 data class를 언급하기 위함 입니다.
data class는 아래와 같이 같단히 정의할 수 있습니다.
data class Client(val name: String, val postalCode: Int)

fun main(args: Array) {
    val bob = Client("Bob", 973293)
    println(bob.copy(postalCode = 382555))
}


참 쉽죠잉.

class 앞에 data 키워드만 붙이면 됩니다.

이런게 data로 선언된 Client는 클래스는 컴파일시에 아래작업이 추가적으로 수행됩니다.

  • 인스턴스간 비교를 위한 equals() 자동생성
  • Hash 기반 container에서 키로 사용할 수 있도록 hashCode() 자동 생성
  • property 순서대로 값을 반환해 주는 toString() 자동생성

이때 위엔 언급된 함수들은 주 생성자(main constructor)에 주어진 값들을 기반으로 만들어집니다.

(보조 생성자나 기타 선언된 property들은 조합요소가 아닙니다.)


data 클래스이 이외에도 copy()를 제공합니다.

이는 data 클래스를 불변클래스로 만들면서 일부 값이 변경되어야 할때 기존 값들을 복사하여 새 객체를 만들어주는 역할을 합니다.


4.3.3 class delegation: by

클래스의 기능을 일부를 재구현 하는 클래스를 만들때 by를 사용하면 편리하게 만들 수 있습니다.
코틀린은 기본적으로 class의 상속을 제한합니다. (final class가 기본값임)
따라서 상속을 원하는경우 명시적으로 open class로 만들어야 한다고 앞선 포스팅에서 언급했습니다.
또한 이 이유가 fragile base problem이라는 얘기도 했었습니다.

따라서 상속할수 없는 객체에 특정 기능을 추가하거나, 변경하려고 할때 decorate pattern을 사용합니다.

데코레이터 패턴은 기반이 되는 클래스를 property로 가지고, 변경이나, 추가를 원하는 기능을 재정의 합니다.
단! 기반이 되는 클래스의 기본 기능들은 요청을 전달하기 위해서 한번 wrapping 하는 번거로운 작업을 해야 합니다.

by는 신규추가 또는 변경되는 api 이외의 값은 제외하고 나머지 api들을 기반 객체로 연결하는 작업을 컴파일러가 해줍니다.
class CountingSet(val innerSet: MutableCollection = HashSet()) : MutableCollection by innerSet {
    var objectsAdded = 0

    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    override fun addAll(c: Collection): Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

fun main(args: Array) {
    val cset = CountingSet()
    cset.addAll(listOf(1, 1, 2))
    println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}

위 CountingSet 함수는 MutableCollection을 상속받으나, innerSet에 해당 구현을 위임 합니다.

따라서 추가작업이 필요하여 override되어 구현된 add()나 addAll() 이외의 다른 API들은 기본 HashSet() class의 API를 사용합니다.


반응형