Search

Object expressions and declarations

Kotlin은 객체 표현식과 객체 선언으로 명시적으로 새로운 하위 클래스를 선언하지 않고도 어떤 클래스를 약간 수정한 객체를 만들 수 있게 해준다.

객체 표현식

객체 표현식은 익명 클래스의 인스턴스를 생성하는 방법이다.
익명 클래스는 명시적으로 선언되지 않은 클래스이며 주로 일회성으로 사용된다.
처음부터 정의하거나 기존 클래스에서 상속하거나 인터페이스를 구현할 수 있다.

익명 객체 생성

클래스가 없거나 상속받지 않는 단순한 객체를 생성할 때 사용한다.
object 키워드로 시작하며 중괄호 안에 객체의 멤버를 정의한다.
val helloWorld = object { val hello = "Hello" val world = "World" override fun toString() = "$hello $world" } println(helloWorld) // 출력: Hello World
Kotlin
복사

익명 객체 상속과 구현

익명 객체를 상속받거나 인터페이스를 구현할 수 있다.
상속할 타입이나 인터페이스는 object 키워드 뒤에 콜론을 사용하여 지정한다.
window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { /* 클릭 이벤트 처리 */ } override fun mouseEntered(e: MouseEvent) { /* 마우스 진입 이벤트 처리 */ } })
Kotlin
복사
상속할 타입에 생성자가 있는 경우 적절한 생성자 매개변수를 전달한다.
여러 타입을 콜론 뒤에 쉼표로 구분하여 상속할 수 있다.
open class A(x: Int) { public open val y: Int = x } interface B { /* ... */ } val ab: A = object : A(1), B { override val y = 15 }
Kotlin
복사

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

익명 객체가 로컬 또는 프라이빗 타입으로 사용되지만 인라인 함수 또는 인라인 프로퍼티로 사용되지 않는 경우 이 함수 또는 프로퍼티를 통해 모든 멤버에 접근할 수 있다.
class C { private fun getObject() = object { val x: String = "x" } fun printX() { println(getObject().x) } }
Kotlin
복사
이 함수나 프로퍼티가 public 또는 private 인라인으로 선언되었다면 실제 반환 타입은 다음과 같이 처리된다.
익명 객체에 명시적인 상위 타입이 없다면 반환 타입은 Any가 된다.
익명 객체에 단 하나의 상위 타입이 명시되었다면 그 상위타입이 반환 타입이 된다.
익명 객체에 여러 개의 상위 타입이 명시되었다면 명시적으로 선언된 반환 타입이 실제 반환 타입이 된다.
이 모든 경우에 익명 객체에 추가된 멤버는 접근할 수 없고 오버라이드된 멤버는 함수나 프로퍼티의 실제 타입에서 선언된 경우 접근할 수 있다.
interface A { fun funFromA() {} } interface B class C { // 반환 타입이 Any이므로 x에 접근 불가 fun getObject() = object { val x: String = "x" } // 반환 타입이 A이므로 x에 접근 불가 fun getObjectA() = object: A { override fun funFromA() {} val x: String = "x" } // 반환 타입이 B이므로 funFromA()와 x에 접근 불가 fun getObjectB(): B = object: A, B { // 명시적 반환 타입 필요 override fun funFromA() {} val x: String = "x" } }
Kotlin
복사

익명 객체에서 변수에 엑세스하기

객체 표현식의 코드는 자신을 둘러싼 범위에서 변수에 접근할 수 있다.
익명 객체 내부에서 외부 스코프의 변수를 사용하면 그 변수의 값은 익명 객체가 생성된 시점에 캡쳐된다.
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++ } }) // ... }
Kotlin
복사

객체 선언

Kotlin을 사용하면 싱글톤을 쉽게 선언할 수 있다.
object 키워드 뒤에 이름을 붙여서 사용하며 표현식이 아니기 때문에 객체를 변수에 할당할 수 없다.
객체 선언은 thread-safe 하며 처음 접근될 때 초기화된다.
object DataProviderManager { fun registerDataProvider(provider: DataProvider) { // ... } val allDataProviders: Collection<DataProvider> get() = // ... }
Kotlin
복사
객체를 참조하려면 객체 이름을 직접 사용한다.
DataProviderManager.registerDataProvider(...)
Kotlin
복사
객체 선언은 슈퍼타입을 가질 수 있다.
object DefaultListener : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { ... } override fun mouseEntered(e: MouseEvent) { ... } }
Kotlin
복사
객체 선언은 로컬이 될 수 없다. 즉, 함수 내에 직접 중첩시킬 수 없다.
fun someFunction() { // 함수 내부에서 객체 선언을 시도 (오류 발생) object LocalObject { val name = "Local" } }
Kotlin
복사
하지만 다른 객체 선언이나 inner 가 아닌 클래스 안에 중첩시킬 수는 있다.
class OuterClass { object InnerObject { val name = "InnerObject" object NestedObject { val name = "NestedObject" } } fun printNames() { println(InnerObject.name) // InnerObject println(InnerObject.NestedObject.name) // NestedObject } } fun main() { val outer = OuterClass() outer.printNames() }
Kotlin
복사

데이터 객체

Kotlin에서 일반 객체 선언을 출력하면 객체의 이름과 해시가 포함된 문자열이 반환된다.
object MyObject fun main() { println(MyObject) // MyObject@1f32e575 }
Kotlin
복사
data class와 마찬가지로 object 선언에도 data 키워드를 사용할 수 있다.
data class 처럼 toString(), equals(), hashCode() 의 함수를 생성하지만 커스텀 equals()나 hashCode()의 구현을 제공할 수 없다.
data object의 toString()은 해시가 포함되지 않은 객체의 이름만을 출력한다.
data object MyDataObject { val x: Int = 3 } fun main() { println(MyDataObject) // MyDataObject }
Kotlin
복사
data object의 equals()는 해당 data object 타입의 모든 객체가 동일하다고 간주되도록 보장한다.
대부분의 경우 data object는 싱글톤이므로 런타임에서 단일 인스턴스만 존재한다.
리플렉션 기능을 사용해 런타임에서 동일한 타입의 또 다른 객체를 생성하더라도 data object는 동일한 객체로 간주된다.
data object를 비교할 때는 반드시 구조적 비교(== 연산자)를 사용해야 하며 참조 비교(=== 연산자)는 피해야 한다.
import java.lang.reflect.Constructor data object MySingleton fun main() { val evilTwin = createInstanceViaReflection() println(MySingleton) // MySingleton println(evilTwin) // MySingleton // 라이브러리가 강제로 MySingleton의 두 번째 인스턴스를 생성해도, equals 메서드는 true를 반환합니다: println(MySingleton == evilTwin) // true // data object는 ===로 비교하지 마세요. println(MySingleton === evilTwin) // false } fun createInstanceViaReflection(): MySingleton { // Kotlin 리플렉션은 data object의 인스턴스화를 허용하지 않습니다. // 아래 코드는 "강제로" 새로운 MySingleton 인스턴스를 생성합니다 (즉, Java 플랫폼 리플렉션 사용). // 스스로 이런 코드를 작성하지 마세요! return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance() }
Kotlin
복사
data object 와 data class 의 차이점
copy() 함수가 없다.
data class는 객체의 복사본을 생성할 수 있는 copy() 함수를 자동으로 제공한다.
data object는 싱글톤이므로 copy() 함수가 생성되지 않는다.
componentN() 함수가 없다.
data class는 선언된 프로퍼티들을 개별적으로 반환할 수 있는 componentN() 함수를 자동으로 제공한다.
data object는 데이터 프로퍼티를 갖지 않으므로 deconstruction이 의미가 없다.
봉인된 계층 구조에서의 data object 사용
data object는 sealed 클래스나 인터페이스와 같은 sealed 계층에 유용하다.
data class와 함께 정의된 객체에서도 대칭성을 유지할 수 있다.
sealed interface ReadResult data class Number(val number: Int) : ReadResult data class Text(val text: String) : ReadResult data object EndOfFile : ReadResult fun main() { println(Number(7)) // Number(number=7) println(EndOfFile) // EndOfFile }
Kotlin
복사
EndOfFile을 data object로 선언하면 toString() 함수를 자동으로 제공한다.

동반 객체

클래스 내부에 선언된 객체로 Java의 static 멤버와 유사하고 companion 키워드를 사용한다.
class MyClass { companion object Factory { fun create(): MyClass = MyClass() } }
Kotlin
복사
동반 객체의 멤버는 클래스 이름을 통해 쉽게 호출될 수 있다.
val instance = MyClass.create()
Kotlin
복사
동반 객체의 이름은 생략될 수 있으며 이름을 생략하면 기본적으로 Companion이라는 이름을 사용한다.
class MyClass { companion object { } } val x = MyClass.Companion
Kotlin
복사
클래스 이름 자체를 동반 객체에 대한 참조로 사용할 수 있다.
동반 객체가 이름을 가지든 가지지 않든 상관없이 가능하다.
class MyClass1 { companion object Named { } } val x = MyClass1 // MyClass1이 Named 동반 객체를 참조 class MyClass2 { companion object { } } val y = MyClass2 // MyClass2가 Companion 동반 객체를 참조
Kotlin
복사
동반 객체는 static 멤버처럼 보이지만 실제 객체의 인스턴스 멤버이다.
동반 객체는 클래스 내부의 객체로서 동작하며 인터페이스를 구현하거나 다른 객체처럼 행동할 수 있다.
interface Factory<T> { fun create(): T } class MyClass { companion object : Factory<MyClass> { override fun create(): MyClass = MyClass() } } val f: Factory<MyClass> = MyClass // MyClass가 Factory 인터페이스를 구현한 동반 객체로 참조됨
Kotlin
복사
JVM에서 @JvmStatic 어노테이션을 사용하면 정적 메서드나 필드로 생성할 수 있다.
class MyClass { companion object { @JvmStatic fun staticMethod() { println("This is a static method") } } } MyClass.staticMethod() // JVM에서는 진짜 static 메서드로 동작함
Kotlin
복사

객체 표현식과 선언 사이의 의미적 차이

객체 표현식은 사용되는 곳에서 즉시 실행(및 초기화)된다.
val myObject = object { val x = 10 fun sayHello() = println("Hello") } myObject.sayHello() // "Hello" 출력
Kotlin
복사
객체 선언은 처음 접근할 때 지연 초기화된다.
object MySingleton { val x = 10 fun sayHello() = println("Hello from Singleton") } MySingleton.sayHello() // 처음 호출될 때 객체가 초기화됨
Kotlin
복사
동반 객체는 해당 클래스가 처음으로 참조되거나 로드될 때 초기화된다.
class MyClass { companion object { init { println("Companion object initialized") } } } val instance = MyClass() // 이 시점에서 Companion object가 초기화됨
Kotlin
복사