본문 바로가기

코틀린

Object expressions and declarations (객체 표현식과 선언식)

출처
코틀린의 object와 companion object를 공부하기 위해서 공식문서의 글을 보고 해석 및 의역후 정리한 내용이다. 만약 공식문서를 참조하면서 공부한다면 매우 요긴하게 읽을 수 있을 것이다.

Object expressions and declarations

Sometimes you need to create an object that is a slight modification of some class, without explicitly declaring a new subclass for it. Kotlin can handle this with object expressions and object declarations.
가끔씩 어떤 클래스를 조금만 변형시켜서 객체를 만들어야 하는 상황이 있다. 새로운 서브클레스를 선언하지 않으면서 말이다.
코틀린에서는 이를 표현식과 선언식을 통해서 가능하게 해준다.

Object expressions

객체 표현식
객체 표현식은 익명객체를 만들어주고, 이는 class 예약어를 사용해서 class를 정확하게 선언해주지 않는다.
이렇게 만들어진 클래스(익명객체)는 한번 사용하는데 좋다. scratch나 이미 존재하던 클래스의 상속에서, 구현체에서 이를 사용할 수 있다.
익명클래스의 객체를 '익명 객체' 라고도 부르는데 이는 표현식으로 정의되지 이름으로 정의되는게 아니기 때문이다.
내 생각 : 자바에서도 있는 익명 객체가 언급되고 있다. 자바를 배웠을때도 익명객체는 이름없는 클래스라고 불렸고 어차피 클래스안에서 한번만 사용되는 경우 쓸때 유용하다고 했었는데, object 표현식은 자바의 익명객체와 그 사용목적이 유사한 것 같다.

Creating anonymous objects from scratch

Object 표현식은 object 키워드로 시작한다.
명확한 supertype 이 필요하지 않은 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"
}
내 생각 : obejct 예약어를 사용해서 객체를 만드는 방법에 관해서 설명해주고 있다.
그리고 object 표현식은 Any 클래스(코틀린의 최상위 부모클래스)를 상속하기 때문에 toString 메소드를 오버라이드 해야할 필요가 있다.

Inheriting anonymous objects from supertypes

(익명객체를 sueprtype 에서 상속하기)
어떤 type을 상속하는 익명 클래스의 객체를 만들기 위해서는, object 와 colon (:)뒤에 특정 타입을 명시해야 한다. 그러면 상속(구현)하고 있는 클래스의 멤버들을 구현하면 된다.
window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }
    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
내 생각 : 부모 객체를 상속(구현)받고 자식클래스의 익명객체를 구현하는 방법에 관해서 설명하고 있다.
위의 스니펫에서는 부모 객체(supertype)가 MouseAdapter 이고 아마 MouseAdapter는 인터페이스이고 추상메소드로 mouseCliked 와 mouseEntered 를 갖고 있어서 직접 구현해야 하는 것으로 보인다.
 
supertype이 생성자를 갖고 있다면, 알맞은 매개변수를 해당 생성자에 넘겨준다.
다양한 supertype들이 colon 뒤에 쉼표를 기준으로 해서 특정될 수 있다.
open class A(x: Int) {
    public open val y: Int = x
}
interface B { /*...*/ }
val ab: A = object : A(1), B {
    override val y = 15
}
내 생각 : 위와 같이 설정하면 ab 객체는 A 클래스를 상속받고 B를 구현하는 것도 가능하다.
object로 만든 객체를 상속, 구현하는 방법에 관해서 서술한 것이다.

Using anonymous objects as return and value types

return 과 value type으로 익명 객체를 사용하는 방법
 
익명객체가 inline 선언(함수나 속성)이 아닌 지역 private으로 사용되는 경우, 함수나 속성으로 모든 멤버가 접근이 가능하다.
class C {
    private fun getObject() = object {
        val x: String = "x"
    }
    fun printX() {
        println(getObject().x)
    }
}
내 생각 : private 으로 선언된 object 객체에 접근하는게 가능하다. 위에 코드처럼.
 
만약 이 함수나 속성값이 public 또는 private inline 이라면, 타입은 다음과 같다.
 
  • Any 타입이 된다. 익명객체의 supertype이 명시되어 있지않다면 말이다.
  • 익명 객체의 선언된 상위 유형(해당 유형이 정확히 하나만 있는 경우)
  • 선언된  supertype이 두 개 이상인 경우 명시적으로 선언된 유형
 
이 모든 경우에서, 익명 객체에 선언된 모든 멤버에는 접근하는게 불가능하다. 오버라이드된 멤버들은 function이나 property에 선언된 경우 접근 가능하다.
interface A {
    fun funFromA() {}
}
interface B
class C {
    // The return type is Any; x is not accessible
    fun getObject() = object {
        val x: String = "x"
    }
    // The return type is A; x is not accessible
    fun getObjectA() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }
    // The return type is B; funFromA() and x are not accessible
    fun getObjectB(): B = object: A, B { // explicit return type is required
        override fun funFromA() {}
        val x: String = "x"
    }
}
내 생각 : 그냥 object로 만들어진 객체의 접근제어자가 앞에 private이 안붙어 있으면 객체의 멤버변수에 접근하는게 불가능 하다는 말 아닌가?
interface A {
    fun funFromA() {}
}
interface B

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

    private fun getObject2() = object{
        val x: String = "x"
    }

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

    private fun getObjectA2() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }

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

    private fun getObjectB2(): B = object: A, B {
        override fun funFromA() {}
        val x: String = "x"
    }
    
    fun temp(){
        getObject2().x
        getObjectA2().x
        //getObjectB2().x 컴파일에러
    }
}
궁금해서 한번 테스트해봤는데 접근제어자에 따라서 object의 멤버변수 x 접근 여부가 다르다.

Accessing variables from anonymous objects

익명객체에서 변수에 접근하는 방법
object 표현식의 코드는 scope 범위에서 변수에 액세스할 수 있습니다:
fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }
        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ...
}
내 생각 : object 에서 그냥 멤버변수 접근해서 사용하는 방법을 가르쳐주는 거인듯.
이런 세세한 것까지는 굳이 머리속에 외워야 할 필요는 없다고 생각한다. 입시공부도 아니고 말이다.
그리고 여기까지 공부한 것을 잠시 정리하면 object 키워드를 사용해서 객체를 설정하는게 가능하다. 여태까지 말한게 객체를 처음부터 다 만들어서 쓰지는 않더라도 중간중간 내용을 수정해서 사용하는 방법에 관해서 얘기한 것이다.

Object declarations

객체 선언식
싱글톤 패턴은 다양한 상황에서 유용하며 코틀린에서는 이를 사용하기 편하게 제공해준다.
객체 선언식은 싱글톤으로 객체를 만드는데 사용한다.
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }
    val allDataProviders: Collection<DataProvider>
        get() = // ...
}
이를 obect 선언식이라고 부르고, object 키워드를 이름으로 한다.
변수를 선언하듯이, obejct 선언문은 표현식이 아니고, it cannot be used on the right-hand side of an assignment statement. (변수를 할당하는데 right-hand side로 쓸 수 없다고 하는데 이게 무슨소린지 이해가 안된다.)
object 선언식 초기화는 thread-safe하고 처음 접근 됐을때 코드가 실행된다.
객체에 접근하기 위해서는, 해당 이름을 바로 접근해라.
DataProviderManager.registerDataProvider(...)
각 객체는 sueprtype 들을 가질 수 있다.
object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseEntered(e: MouseEvent) { ... }
}
내 생각 : 이제 object 를 선언식으로 썼을때 어떤 방식으로 사용되는지 설명해주고 있다.
싱글톤 패턴을 만들때 objecet 선언식을 통해서 만드는게 가능하고, 이는 thread-safe하고 코드를 실행하는 시점에 초기화 된다.
object 선언식은 지역으로 선언될 수 없지만(즉, 함수 안에 직접 중첩할 수 없다.), 다른 객체 선언식이나 inner클래스가 아닌 경우에 중첩될 수 있다.

Data objects

plain 객체를 코틀린에서 로그로 찍으려고 하면, 문자열 값은 해당 값의 이름과 해쉬값을 포함하고 있다.
object MyObject
fun main() {
    println(MyObject) // MyObject@1f32e575
}
data 클래스처럼 , object 선언식data 로 설정할 수 있다. 이렇게 설정하면 컴파일러는 객체에 대한 몇가지 함수를 생성해준다.
 
  • toString() returns the name of the data object
  • equals()/hashCode() pair
 
커스텀한 equls나 hashcode 메소드 구현체를 data object를 위해서 제공해줄수는 없다.
 
data object의 toString 함수는 object의 이름을 반환해준다.
data object MyDataObject {
    val x: Int = 3
}
fun main() {
    println(MyDataObject) // MyDataObject
}
data object에 대한 equals() 함수는 data object의 유형을 가진 모든 object가 동일한 것으로 간주되도록 합니다.
대부분의 상황에서, 런타임시 data object에 대한 단하나의 객체만을 가진다. (이후, data object 는 싱글톤을 선언한다.) 그러나, 같은 타입의 다른 객체가 런타임시에 생성되면(for example, by using platform reflection with java.lang.reflect or a JVM serialization library that uses this API under the hood), 이는 객체가 동일한 것으로 취급하도록 해준다.
 
내 생각 : 싱글톤으로 만들어진 객체는 단 하나의 인스턴스만 가지므로 새로 선언하더라도 같은 것으로 취급한다는 말을 여기서 해주는 것 같다.
 
data objects 를 구조적으로(== 연산자를 사용해) 비교하고 객체(=== 연산)를 비교하지 말아야 한다는 사실을 명심하자. 이렇게 하면 런타임에 데이터 객체의 인스턴스가 두 개 이상 존재할 때 발생할 수 있는 함정을 피할 수 있다.
 
(코틀린에서 === 연산자는 객체의 주소지를 비교하는 연산자다.)
import java.lang.reflect.Constructor
data object MySingleton
fun main() {
    val evilTwin = createInstanceViaReflection()
    println(MySingleton) // MySingleton
    println(evilTwin) // MySingleton
    // Even when a library forcefully creates a second instance of MySingleton, its `equals` method returns true:
    println(MySingleton == evilTwin) // true
    // Do not compare data objects via ===.
    println(MySingleton === evilTwin) // false
}
fun createInstanceViaReflection(): MySingleton {
    // Kotlin reflection does not permit the instantiation of data objects.
    // This creates a new MySingleton instance "by force" (i.e. Java platform reflection)
    // Don't do this yourself!
    return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}
생성된 hashCode() 함수는 equals() 함수와 일치하는 동작을 갖는데, 이 때문에 data object는 런타임시 항상 hashcode() 값이 같다.
 
내 생각 : 솔직히 이 섹션까지 읽었을때 무엇을 말하고 싶은 것인지 잘 모르겠다.
 

Differences between data objects and data classes

data objects 와 data class 의 차이점
 
data object와 data class 는 둘이 함께 쓰이고 비슷한데, data object에는 생성되지 않는 함수가 몇개 있다.
 
-copy 함수가 생성되지 않는다. 왜냐하면 data object로 선언하면 싱글톤 객체로 사용하는 의도이기 때문에, copy() 함수가 생성되지 않는다.
 
-componentN() 함수가 생성되지 않는다. data class 와는 다르게, data object는 어떤 멤버 변수도 갖지 않는다.
객체의 속성 없이 이러한 객체를 파괴하려고 시도하는 것은 의미가 없으므로 componentN() 함수가 생성되지 않습니다.
 

Using data objects with sealed hierarchies

sealed 상속과 함께 data object 들을 사용하기
 
sealed 상속에서 data object는 특히 유용한데, sealed classes 나 sealed interfaces 처럼, 객체와 함께 정의한 모든 데이터 클래스와 대칭성을 유지할 수 있습니다.
 
잠깐상식
sealed 클래스는 추상클래스이다. 그리고 자신을 상속할 수 있는 클래스를 따로 설정할 수 있다.
아래와 같이 말이다.

sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult
fun printReadResult(r: ReadResult) {
    when(r) {
        is Number -> println("Num(${r.number}")
        is Text -> println("Txt(${r.text}")
        is EndOfFile -> println("EOF")
    }
}
fun main() {
    printReadResult(EndOfFile) // EOF
}
내 생각 : 솔직히 무엇을 말하고 싶은 것인지 잘 모르겠다. 아마 내가 sealed 에 관해서 많이 안살펴봐서 이해가 안되는 것일수도 있고 필요한 시점이 오면 다시 한번 면밀히 보자.

Companion objects

클래스 안에서 객체를 선언하는 방법은 companion 키워드를 사용하는 것이다.
자바에서 static과 동일한 용도로 많이 쓰인다.
class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
companion object로 생성된 객체는 간단하게 클래스명을 사용해서 접근하는게 가능하다.
내 생각 : companion object 키워드를 사용하면 무조건 싱글톤으로 객체를 만드는 것만 생각했는데 내가 잘못 생각했다. 객체를 설정하는 역할도 할 수 있다.
companion object의 이름은 생략이 가능하며 Companion 은 아래와 같은 방법으로 쓰인다.
class MyClass {
    companion object { }
}
val x = MyClass.Companion
클래스 멤버는 해당 companion 객체의 private 멤버에 액세스할 수 있습니다.
The name of a class used by itself (not as a qualifier to another name) acts as a reference to the companion object of the class (whether named or not):
다른 이름에 대한 한정자가 아닌 그 자체로 사용되는 클래스의 이름은 클래스의 동반 객체(이름이 지정되었는지 여부에 관계없이)에 대한 참조로 작동합니다:
(이 문장은 도저히 이해가 안된다..)
class MyClass1 {
    companion object Named { }
}
val x = MyClass1
class MyClass2 {
    companion object { }
}
val y = MyClass2
companion object의 멤버변수는 다른 언어에서는 static처럼 사용되지만, 런타임에도 여전히 실제 객체의 인스턴스 멤버이며, 예를 들어 인터페이스를 구현할 수 있습니다:
interface Factory<T> {
    fun create(): T
}
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}
val f: Factory<MyClass> = MyClass
JVM에서 @JvmStatic 주석을 사용하면 컴패니언 객체의 멤버를 실제 static 메서드 및 필드로 생성할 수 있습니다. 자세한 내용은 Java interoperability 섹션을 참조.

Semantic difference between object expressions and declarations

객체의 표현식과 선언식에는 의미차이가 몇가지 존재한다.
-객체 표현식은 실행과 초기화 사용된 곳에서 즉시 일어난다.
-객체 선언식은 객체에 처음 접근했을때 lazily(늦은 초기화)로 초기화 된다.
-companion object는 Java 정적 이니셜라이저의 의미와 일치하는 해당 클래스가 로드(resolve)될 때 초기화됩니다.
정리하기
-object 와 companion object를 사용해서 객체를 선언할 수 있다.
-object는 표현식과 선언식으로 나뉘어 진다.