본문 바로가기

Kotlin

Kotlin - Object 표현과 정의

간혹 어떤 클래스를 살짝 변경해서 사용하고 싶은데 사용범위가 넓지 않아서 새로운 타입을 정의하기는 부담스러운 경우들이 있다. Kotlin은 object표현식과 object 정의를 통해 이러한 상황들을 지원한다.

Object 표현 (Object expressions)

Object 표현은 명시적으로 class로 정의하지 않은 익명 클래스의 객체를 생성한다. 익명클래스는 일회용으로 사용하는 경우 적합하며, 기존 클래스를 상속하거나 인터페이스를 구현 할수도 있다. 익명 클래스의 인스턴스는 익명 객체로도 부르기도 한다.

기본 익명 객체 생성하기

Object 표현은 object 키워드로 시작한다.

상위타입이 없다면, 멤버들을 object 이후에 나오는 괄호({ }) 안에 작성 하는 것으로 생성 할 수 있다.

val helloWorld = object {
    val hello = "Hello"
    val world = "World"
    // object expressions extend Any, so `override` is required on `toString()`
    override fun toString() = "$hello $world"
}

print(helloWorld)
// Hello World

 

상위타입을 상속받는 익명 객체 생성하기

상위타입을 상속받는 객체를 생성할때는 object 키워드 뒤에 콜론(:) 을 붙이고 상속하거나 구현할 클래스를 작성하면 된다.

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

만약 상위 클래스에 생성자가 존재 한다면 적합한 파라미터를 전달해야 한다. 복수개의 상위 타입이 존재한다면 콤마로 구분해서 상위타입들을 작성 하면 된다.

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

 

익명 객체를 반환 및 값 타입으로 사용

익명 객체가 로컬 또는 private 타입이지만 인라인 선언(함수, 속성)이 아닌 경우 모든 멤버는 이 함수 또는 속성을 통해 익명 객체의 멤버에 접근 할 수 있다.

class AnonymousObjectAsReturn {
    private fun getObject() = object {
        val x: String = "X of AnonymousObjectAsReturn"
    }
    fun printX() {
        println(getObject().x) //anonymous object 의 x 속성에 접근
    }
}

함수나 속성이 public 이거나 private inline 인 경우 이들의 실제 타입은 아래 3 개 중 하나가 된다.

  • 익명 객체가 supertype을 가지지 않는다면 익명 객체는 Any 타입으로 처리된다.
  • 익명 객체가 상속받는 정확한 타입이 하나만 존재하는 경우 supertype으로 처리 된다.
  • 익명 객체가 복수개의 supertype을 가지고 정확히 어떤 타입인지 명시 된경우 명시된 타입으로 처리된다.

이상의 모든 겨웅에 대해 익명 객체의 멤버로 접근이 불가능 해 진다. 

interface SuperOfAnonymousA {
    fun funFromA() {}
}

interface SuperOfAnonymousB {

}

class AnonymousC {
    // The return type is Any; x is not accessible
    fun getObject() = object {
        val x: String = "x"
    }
//    fun printlnX1() {
//        println(getObject().x)
//    }


    // The return type is A; x is not accessible
    fun getObjectA() = object: SuperOfAnonymousA {
        override fun funFromA() {}
        val x: String = "x"
    }
//    fun printlnX2() {
//        println(getObjectA().x)
//    }


    // The return type is B; funFromA() and x are not accessible
    fun getObjectB(): SuperOfAnonymousB = object: SuperOfAnonymousA, SuperOfAnonymousB { // explicit return type is required
        override fun funFromA() {}
        val x: String = "x"
    }
//    fun printlnX3() {
//        println(getObjectB().x)
//    }

}

 

익명 객체에서 변수에 접근하기

object 표현식 코드는 표현식 외부에 있는 변수에 접근 가능하다

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++ // countClicks 함수에 선언 됨
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++ // countClicks 함수에 선언 됨
        }
    })
    // ...
}

 

Object 선언

Kotlin 에서는 Singleton을 더 쉽게 선언 할 수 있다.

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

위와 같은 선언 방식을 object 선언 이라고 부르고, object 키워드 이후에 이름을 작성한다. 변수 선언과 마찬가지로 object 선언은 표현식이 아니다. 그리고 할당 문에 오른쪽 편에 작성 할 수 없다.

object 선언의 초기화는 쓰레드 세이프 하고 처음 접근(access)할때 수행된다.

해당 object에 접근하고 싶으면 object의 이름을 직접 작성하면 된다.

DataProviderManager.registerDataProvider(...)

상위 타입(supertype)을 갖는 object 선언도 가능하다.

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }

    override fun mouseEntered(e: MouseEvent) { ... }
}

 

Data objects

Kotlin에서 순수 object 선언을 프린트 할때, object 이름과 object의 해시값 으로 표현된다.

object MyObject

fun main() {
    println(MyObject) // MyObject@1f32e575
}

data class 같이 object 에 data 수정자를 사용할 수 있다. 이를 통해 컴파일러는 해당 object에 몇가지 관련 기능들을 자동으로 생성한다.

  • toString() - data 오브젝트의 이름을 반환한다.
  • equals() / hashCode()
data object MyDataObject {
    val x: Int = 3
}

fun main() {
    println(MyDataObject) // MyDataObject
}

data object의 equals 함수는 data object를 가진 모든 타입이 동일한 것으로 간주된다. 보통의 경우 단일 인스턴스로 사용되지만, 간혹 같은 타입의 새로운 객체가 생성 되는 경우가 있는데 (platform reflection 이나 JVM 동기화 라이브러리를 사용하는 경우 등) 이런 경우 다른 인스턴스 이지만 같은(equal) 객체로 처리한다.

hashCode() 함수 역시 equals()와 마찬가지로 어떤 경우라도 동일한 hash code를 반환한다.

data object 와 data class의 차이점

data object와 data class는 비슷한점도 있고 서로 같이 사용되는 경우도 있다. 아래와 같은 일부 함수들은 data object에서 지원하지 않는다.

  • copy() - data object는 singleton 이기 때문에 copy 함수는 지원하지 않는다.
  • componentN() - data object는 data property를 가질 수 없기 때문에 당연히 지원하지 않는다.

Companion objects

class 내부에 object를 선언하는 경우 companion 키워드를 같이 사용할 수 있다. companion object의 멤버들은 상위 class의 이름을 붙여서 호출 할 수 있다.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

fun main() {
	val instance = MyClass.create()
}

companion object의 이름은 생략 가능하고, 이름이 생략 된 경우 이름 대신 Companion 을 사용 할 수 있다.

class MyClass {
    companion object { }
}

val x = MyClass.Companion

클래스 이름을 직접 사용하면, 클래스 내부에 있는 companion object의 참조값으로 사용된다.

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

companion object의 멤버들이 static 멤버들처럼 보이지만 실제로 실제 객체의 인스턴스 멤버들이다. @JvmStatic 어노테이션을 붙이면 진짜 static 멤버로 컴파일 된다.

object 표현식과 선언의 차이

  • Object 표현식은 사용될때 즉시 실행(초기화)된다.
  • Object 선언은 처음 접근 할때 초기화 된다.
  • companion object는 class가 로딩될때 초기화 된다. (Java의 static 초기화와 비슷)

'Kotlin' 카테고리의 다른 글

Kotlin - 타입 별칭 (Type aliases)  (0) 2024.03.29
Kotlin - Delegated properties  (0) 2024.03.28
Kotlin - Deligation  (0) 2024.03.23
Kotlin - 인라인 값 클래스 (inline value classes)  (0) 2024.03.05
Kotlin - Enum 클래스  (1) 2024.02.17