코틀린은 주 생성자(클래스를 초기화할 때 사용, 클래스 본문 밖에서 정의)와 부 생성자(클래스 본문 안에서 정의)를 구분한다
또한 코틀린에서는 초가화 블록을 통해 초기화 로직을 추가할 수 있다.
주 생성자와 초기화 블록
보통 클래스의 모든 선언은 중괄호({}) 사이에 들어간다.
클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 부른다
주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 목적에 쓰인다.
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
- constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다.
- init 키워드는 초기화 블록을 시작한다.
- 초기화 블록에는 클래스의 객체가 만들어질 때(인스턴스화) 실행될 초기화 코드가 들어간다.
- 초기화 블록은 주 생성자와 함께 사용된다.
- 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다.
- 생성자 파라미터 _nickname에서 _은 프로퍼티와 생성자 파라미터를 구분해 준다.(this 키워드를 사용해서 모호함을 없애도 된다)
- 이 예제에서는 nickname 프로퍼티를 초기화하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있어서 초기화 코드를 초기화 블록에 넣을 필요가 없다. 또한 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면 construct를 생략해도 된다.
class User(_nickname: String) { //파라미터가 하나뿐인 주 생성자
val nickname = _nickname //프로퍼티를 주 생성자의 파라미터로 초기화
}
- 프로퍼티를 초기화하는 식이나 초기화 블록 안에서만 주 생성자의 파라미터를 참조할 수 있다.
- 주 생성자로 파라미터로 프로퍼티를 초기화한다면 그 주 생성자 파라미터 이름 앞에 val를 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
class User(val nickname:String) //val은 파라미터에 상응하는 프로퍼티가 생성된다는 의미
디폴트 값도 정의가 가능하다.
class User(val nickname:String, val isSubscribed: Boolean = true) //생성자 파라미터에 대한 디폴트값 제공
인스턴스 생성하기
fun main(args: Array<String>) {
val hyun = User("현석")
println(hyun.isSubscribed)
val gye = User("계영", false)
println(gye.isSubscribed)
val hey = User("혜원", isSubscribed = false) //생성자 인자 중 일부에 대해 이름 지정 가능
println(hey.isSubscribed)
}
Tip : 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다. 그렇게 자동으로 만들어진 파라미터가 없는 생성자는 디폴트 값을 사용해 클래스를 초기화한다.
의존관계 주입 프레임워크 등 자바 라이브러리 중에는 파라미터가 없는 생성자를 통해 객체를 생성해야만 하는 라이브러리 사용이 가능한 경우가 있는데 이는 이를 쉽게 통합하도록 도와준다.
기반 클래스 생성자 호출하기
open class User(val nickname: String)
class TwitterUser(nickname: String) : User(nickname)
- 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
open class Button //인자가 없는 디폴트 생성자가 만들어진다
class RadioButton: Button() //Button클래스를 상속한 하위 클래스는 반드시 Button을 생성자를 호출해야한다
- Button 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button클래스의 생성자를 호출해야 한다.
- 이 규칙으로 인해 기반 클래스의 이름 뒤에는 꼭 빈 괄호가 들어간다.
- 반면, 인터페이스는 생성자가 없기 때문에 어떤 클래스가 인터페이스를 구현하는 경우 그 클래스의 상위 클래스 목록에 있는 인터페이스 이름 뒤에는 아무 괄호도 없다.
- 클래스 정의에 있는 상위 클래스, 인터페이스 목록에서 이름 뒤에 괄호가 붙었는지 살펴보면 쉽게 클래스/인터페이스 구분 가능
클래스를 외부에서 인스턴스화 시키지 못하도록 막기
class Secretive private constructor(){}
- private 키워드를 주 생성자 앞에 붙인다.
- Secretive 클래스 안에 주 생성자 밖에 없고, 그 생성자가 private이기 때문에 인스턴스화 할 수 없다.
상위 클래스를 다른 방식으로 초기화
코틀린에서는 생성자가 여럿 있는 경우가 자바보다 훨씬 적다 -> 디폴트 파라미터 값과, 이름 붙인 인자 문법을 사용해 해결할 수 있기 때문
그럼에도 생성자가 여럿 필요한 경우가 있는데, 일반적인 상황은 프레임워크 클래스를 확장해야 하는데 여러 가지 방법으로 인스턴스를 초기화할 수 있게 다양한 생성자를 지원해야 하는 경우이다.
open class View{
constructor(ctx: Context){
//Code
}
constructor(ctx: Context, attr: AttributeSet){
//Code
}
}
- 이 클래스는 주 생성자를 선언하지 않고 부 생성자만 2가지 선언한다.
- 부 생성자는 construct 키워드로 시작한다
- 필요에 따라 부생 성자를 많이 선언해도 된다.
class MyButton: View{
constructor(ctx: Context): super(ctx){
//Code
}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr){
//Code
}
}
- View 클래스를 확장하면서 똑같이 부 생성자를 정의할 수 있다.
class MyButton: View{
constructor(ctx: Context): this(ctx, MY_STYLE){} //이 클래스의 생성을 다른 생성자에게 위임
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr){
//Code
}
}
- this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.
- 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.
- 부 생성자가 필요한 주된 이유는 자바 상호운용성이다.
- 단, 이것이 모든 이유는 아니고, 클래스 인스턴스를 생성하는 방법이 여러 가지일 경우 부 생성자를 둘 수밖에 없다
인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
interface User{
val nickname: String
}
- User 인터페이스를 구현하는 클래스가 nickname값을 얻을 수 있는 방법 제공
- 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다.
- 사실 인터페이스는 아무 상태도 포함할 수 없으므로 상태를 저장할 필요가 있다면, 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티를 만들어야 한다
class PrivateUser(override val nickname: String): User
class SubscribingUser(val email:String): User{
override val nickname: String
get() = email.substringBefore('@')
}
class FacebookUser(val accountId:Int): User{
override val nickname = getFacebookName(accountId)
}
fun main(args: Array<String>) {
println(PrivateUser("test@kotlinlang.org").nickname)
println(SubscribingUser("test@kotlinlang.org").nickname)
}
- PrivateUser는 주생성자 안에 프로퍼티를 직접 선언한다.
- 이 프로퍼티는 User의 추상 프로퍼티를 구현하고 있으므로 override를 표시해야 한다
- SubscribingUser는 커스텀 게터로 nickname 프로퍼티를 설정한다.
- FacebookUser에서는 초기화 식으로 nickname 값을 초기화한다.
- 페이스북 사용자 ID를 받아 그 사용자 이름을 반환해주는 getFacebookName 함수(있다고 가정)를 호출하여 초기화
- SubscribingUser는 게터를 호출할 때마다 커스텀 게터를 호출하고, FacebookUser는 초기화 시 계산한 데이터를 저장했다 불러오는 방식을 활용한다.
인터페이스의 게터 세터가 있는 프로퍼티 선언
interface User{
val email: String
val nickname: String
get() = email.substringBefore('@')
}
- 추상 프로퍼티인 email과 커스텀 게터가 있는 nickname 프로퍼티가 함께 들어있다.
- 하위 클래스는 추상 프로퍼티인 email을 반드시 오버라이드 해야 한다.
- 반면 nickname은 오버라이드 하지 않고 상속할 수 있다.
게터와 세터에서 뒷받침하는 필드에 접근
어떤 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만들어 보자
값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.
class User(val name: String){
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value"""".trimIndent()) //뒷바침 하는 필드 값 읽기
field = value //뒷받침하는 필드 값 변경하기
}
}
fun main(args: Array<String>) {
val user = User("Alice")
user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
}
- 코틀린에서는 값을 바꿀 때는 user.address = "new Value"처럼 필드 설정 구문을 사용한다.
- 내부적으로 address을 세터를 호출한다.
- 현재 로직에서는 커스텀 세터를 정의해서 로직 수행
- 접근자의 본문에서는 field라는 특별한 서식자를 통해 뒷받침하는 필드에 접근할 수 있다.
- 게터에서는 field값을 읽을 수만 있고, 세터에서는 field값을 읽거나 쓸 수 있다
- 클래스는 프로퍼티를 사용하는 쪽에서 프로퍼티를 읽는 방법이나 쓰는 방법은 뒷받침하는 필드의 유무와는 관계가 없다
- 컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터나 세터를 정의하건 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다
- 다만, field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않는다
접근자의 가시성 변경
get, set 앞엣 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String){
counter += word.length
}
}
fun main(args: Array<String>) {
val lengthCounter = LengthCounter()
lengthCounter.addWord("Hi!")
println(lengthCounter.counter)
}
- 자신에게 추가된 모든 단어의 길이를 합산한다.
- 전체 길이를 공개하는 프로퍼티는 클라이언트에게 제공하는 API로 public으로 외부에 공개하고, 외부 코드에서 단어 길이의 합을 마음대로 바꾸지 못하게 내부에서만 길이를 바꾸고 싶다
- 기본 가시성을 가진 게터를 컴파일러가 생성하게 내버려 두고, 세터의 가시성의 private로 지정한다
'KotlinInAction > 클래스, 객체, 인터페이스' 카테고리의 다른 글
| Object 키워드 : 클래스 선언과 인스턴스 생성 (0) | 2022.07.31 |
|---|---|
| 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임 (0) | 2022.07.31 |
| 클래스 계층 정의 (0) | 2022.07.30 |