-
4-4. 클래스와 객체 다루기 : 객체Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 10. 1. 22:24
4장
4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티4-4. 클래스와 객체 다루기 : 객체
4-4. 객체
1) 객체(object) 선언
코틀린은 어떤 클래스에 인스턴스가 오직 하나만 존재하게 보장하는 싱글턴 패턴을 내장하고 있다.
코틀린에서는 클래스와 비슷한 방법으로 싱글턴을 선언한다.
코틀린에서의 선언 방식은 class 대신 object 키워드를 사용한다.
object Application{ val name = "My App" override fun toString() = name fun exit(){ } }
위 객체 선언은 클래스를 정의하는 동시에 클래스의 인스턴스를 정의하는 것 이다.
fun describe(app: Application) = app.name // Application은 타입 fun main() { println(Application) // Application은 값 }
일반적으로 객체의 인스턴스는 하나뿐이므로 인스턴스만 가리켜도 어떤 타입을 쓰는지 알 수 있다.
따라서 객체를 타입으로 사용해도 상관없다.
객체 정의는 thread-safe 하다. 컴파일러는 실행되는 여러 스레드에서 싱글턴에 접근하더라도
오직 한 인스턴스만 공유되고 초기화 코드도 단 한번만 실행되도록 보장한다.
초기화는 싱글턴 클래스가 실제 로딩되는 시간까지 지연되며
보통 프로그램이 객체 인스턴스에 처음 접근할 때 초기화가 이뤄진다.
클래스와 마찬가지로 객체 선언도 멤버 함수와 프로퍼티를 포함할 수 있고, 초기화 블록도 포함할 수 있다.
하지만 객체 인스턴스는 항상 암시적으로 만들어 지기 때문에 객체의 경우 생성자 호출이 의미가 없다.
그래서 객체에는 주생성자나 부생성자가 없다.
객체의 본문에 들어있는 클래스에는 inner가 붙을 수 없다.
inner를 쓸 수 있는 내부 클래스의 인스턴스는 항상 바깥쪽 클래스의 인스턴스와 연관되는데,
객체 선언은 항상 인스턴스가 하나뿐이므로 inner가 불필요해진다.
그래서 객체 안에 정의된 클래스에 대해서는 inner를 사용할 수 없다.
객체의 멤버를 임포트해서 간단한 이름만 참조해 사용할 수 있다.
import Application.exit //다른 파일로 정의 // import Application.* // Error fun main() { println(Application.name) // 전체 이름을 사용 exit() }
객체의 모든 멤버가 필요할 때 import 문으로 참조할 수는 없다.
이런 제약이 있는 이유는 객체 정의 안에는 다른 클래스 정의와 같이 toString()이나 equals()같은
공통 메서드 정의가 들어있기 때문이다. 따라서 위와같이 * 를 사용한 임포트를 사용하면
공통 메서드까지 임포트돼 문제가 생길수 있으므로 * 를 사용해 임포트할 수 없다.
클래스와 마찬가지로 객체도 다른 클래스 안에 내포될 수 있고, 다른 객체 안에도 내포될 수 있다.
이렇게 내포된 객체 선언도 싱글턴이며 전체 애플리케이션에서 인스턴스가 단 하나만 생긴다.
2) 동반 객체(companion object)
내포된 클래스(nested class)와 마찬가지로 내포 객체(nested object)도 인스턴스가 생기면 자신을 둘러싼 클래스의
비공개 멤버에 접근할 수 있다. 이 특성은 팩토리 디자인 패턴을 쉽게 구현하는 경우 유용하게 활용 가능하다.
생성자를 사용하면 어떤 사전 검사 결과에 따라 널을 반환하거나 다른 타입의 객체를 반환할 수 없을 때
생성자는 항상 자신이 정의된 클래스의 객체를 반환하거나 예외를 던질 수만 있기 때문에
생성자를 사용하고 싶지 않을 수 있다.
이를 해결하는 방법은 생성자를 비공개로 지정해 클래스 외부에서 사용할 수 없게 한 다음
내포된 객체에 팩토리 메서드 역할을 하는 함수를 정의하고
그 함수 안에서 필요에 따라 객체의 생성자를 호출하는 것 이다.
class Application private constructor(val name: String){ object Factory{ fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } } fun main() { val args = arrayOf("John", "banana", "car") // val app = Application(name) // 직접 생성자를 호출하도록 허용 x val app = Application.Factory.create(args) ?: return println("App started ${app.name}") // App started John }
위 경우 별도로 import Application.Factory.create로 팩토리 메서드를 임포트하지 않으면
매번 내포된 객체의 이름을 지정해야 한다.
코틀린에서 Factory 메서드를 동반 객체(companion object)로 정의하면 이를 해결할 수 있다.
companion object는 companion 이라는 키워드를 덧붙인 내포된 객체(nested object)이다.
이 객체는 다른 내포된 객체와 동일하게 작동하지만 한가지의 예외가 있다.
companion object의 멤버에 접근할 때는 companion object의 이름을 사용하지 않고
companion object가 들어있는 외부 클래스의 이름을 사용할 수 있다.
이를 이용해 위 코드를 다음과 같이 작성할 수 있다.
class Application private constructor(val name: String){ companion object Factory{ fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } } fun main() { val args = arrayOf("a", "b", "c") // val app = Application(name) // 직접 생성자를 호출하도록 허용 x val app = Application.create(args) ?: return println("App started ${app.name}") }
companion object의 경우 정의에서 이름을 생략할 수도 있으며, 이 방식을 더 권장한다고 한다.
이 경우 기본 이름을 Companion으로 가정한다.
class Application private constructor(val name: String){ companion object{ // Factory를 생략 fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } }
companion object의 멤버를 임포트하고 싶을 때는 객체 이름을 명시해야 한다.
import Application.Companion.create // import Application.create // Error
클래스에 companion object는 하나만 있어야 한다.
class Application private constructor(val name: String){ companion object Factory // companion object Utils // Error : Only one companion object is allowed per class }
companion 변경자를 최상위 객체 앞에 붙이거나 다른 객체에 내포된 객체 앞에 붙이는 것은 불가능하다.
최상위 객체의 경우 동반 객체를 연결할 클래스 정의가 없고,
객체의 내포된 객체는 companion 변경자를 붙이는 것이 불필요한 중복이기 때문이다.
자바의 static 초기화 블록처럼 동반 객체 안에서도 init 블록을 사용할 수 있다.
3) 객체 식(object expression)
코틀린은 명시적인 선언 없이 객체를 바로 생성할 수 있는 식을 제공한다.
객체 식(object expression)은 자바의 익명 클래스(anonymous class)와 유사하다.
fun main() { fun midPoint(xRange: IntRange, yRange: IntRange) = object{ val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } val midPoint = midPoint(1..20, -20..-1) println("${midPoint.x}, ${midPoint.y}") // 10, -10 }
object expression은 이름이 없는 객체 정의처럼 보인다. 그리고 object expression도 식이므로 위 예제처럼
object expression이 만들어내는 값을 변수에 대입할 수 있다.
객체는 클래스나 object expression과 다르게 함수 안에 정의는 불가능하다.
fun printMiddle(xRange: IntRange, yRange: IntRange) { object midPoint{ // error // Named object 'midPoint' is a singleton and cannot be local. Try to use anonymous object instead val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } }
코틀린 설계자들이 이렇게 결정한 이유는 객체 선언이 싱글턴을 표현하지만,
지역 객체들은 자신을 둘러싼 바깥 함수가 호출될 때 마다 매번 다시 생성되야 하기 때문이라고 한다.
fun main() { fun midPoint(xRange: IntRange, yRange: IntRange) = object{ val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } val midPoint = midPoint(1..20, -20..-1) println("${midPoint.x}, ${midPoint.y}") // 10, -10 }
midPoint() 함수에 대한 반환 타입을 명시하지 않았는데, 이 함수가 반환하는 반환 타입은 무엇일까?
답은 object expression 안에 정의된 모든 멤버가 들어있는 클래스를 표현하는 익명 객체 타입이며(anonymous object type),
이런 타입은 단 하나만 존재한다.(멤버가 완전히 동일한 두 object expression이 있어도 둘의 타입은 다르다)
코틀린 언어에서 위 타입을 표현할 방법은 없다.
이 타입은 단지 코틀린 컴파일러가 object expression의 타입을 표현하기 위해사용하는 내부 표현이다.
object expression이 만들어나는 객체도 다른 클래스 인스턴스와 마찬가지로 사용할 수 있다.
fun main() { val o = object{ val x = readLine()!!.toInt() val y = readLine()!!.toInt() } println("${o.x}, ${o.y}") // o 안의 x, y에 접근이 가능함 }
하지만 익명 객체 타입은 지역 선언이나 비공개 선언에만 전달될 수 있다.
예를들어 midPoint() 함수를 최상위 함수로 정의하면 객체 멤버에 접근이 불가능하다.
fun midPoint(xRange: IntRange, yRange: IntRange) = object{ val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } fun main() { val midPoint = midPoint(1..5, 2..6) println("${midPoint.x}, ${midPoint.y}") // error // Unresolved reference: x // Unresolved reference: y }
위 코드에서 midPoint() 함수의 타입은 object expression에 해당하는 익명 객체 타입이 아니라,
object expression에 저장된 상위 타입이 된다.
하지만 위 식에서는 상위 타입을 명시하지 않았으므로 Any를 상위 타입으로 가정한다.
그래서 midPoint.x 참조에서 x를 찾을 수 없는 것 이다.
지역 함수나 클래스와 마찬가지로, object expression도 자신을 둘러싼 코드 영역의 변수를 포획할 수 있다.
이렇게 표현한 가변 변수를 객체 본문에서 변경할 수 있다.
fun main() { var x = 20 println(x) // 20 val o = object { fun change(){ x *= 5 } } o.change() println(x) // 100 }
지연 초기화되는 객체 선언과 달리 object expression이 만들어내는 객체는
객체 인스턴스가 생성된 직후 바로 초기화된다.
fun main() { var x = 20 val o = object { val a = x++ } println(x) // 21 println("${o.a}") // 20 println(x) // 21 }
위 코드를 보면 o 정의에서 객체가 생성되는 시점에 이 객체의 a 프로퍼티가 초기화되므로
o.a에 접근하기 전에 x를 표시했음에도 불구하고 x의 값으로 21이 표시된다
다음 단원은 고급 함수와 함수형 프로그래밍 활용하기이다.
고차 함수와, 람다, 익명 함수 등을 배운다고 하는데 분량이 꽤 많다..다음 정리는 시간이 꽤 많이 걸릴 것 같아서 열심히 해야겠다
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
5-2. 고급 함수와 함수형 프로그래밍 활용하기 : 확장 (0) 2022.10.08 5-1. 고급 함수와 함수형 프로그래밍 활용하기 : 코틀린을 활용한 함수형 프로그래밍 (0) 2022.10.08 4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티 (2) 2022.10.01 4-2. 클래스와 객체 다루기 : 널 가능성 (1) 2022.10.01 4-1. 클래스와 객체 다루기 : 클래스 정의하기 (2) 2022.09.29