•
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
복사