클래스 초기화: 주 생성자와 초기화 블록
클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자(primary constructor)라고 한다.
class User(val nickname: String)
이를 명시적인 선언으로 풀어보면
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다. init 키워드는 초기화 블록을 실행한다.
초기화 블록은
- 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
- 주 생성자와 함께 사용된다.
- 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록을 사용한다.
- 필요하다면 클래스 안에 여러 초기화 블록을 선언할 수 있다.
위 예제는 nickname 프로퍼티를 초기화하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있다.
class User constructor(_nickname: String) {
val nickname = _nickname
}
주 생성자 파라미터 이름 앞에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
class User(val nickname: String)
함수 파라미터와 마찬가지로 디폴트 값을 정의할 수 있다.
class User(
val nickname: String,
val isSubscribed: Boolean = true
)
클래스의 인스턴스를 만드려면 생성자를 직접 호출하면 된다.
>>> val hyun = User("현석")
>>> println(hyun.isSubscribed)
true
>>> val gye = User("계영", false)
>>> println(gye.isSubscribed)
false
>>> val hey = User("혜원", isSubscribed = false)
>>> println(hey.isSubscribed)
false
기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출할 필요가 있다. 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
open class User(val nickname: String)
class TwitterUser(nickname: String) : User(nickname)
별도의 생성자를 정의하지 않을 수 있다. 이 경우 컴파일러가 자동으로 아무런 인자가 없는 디폴트 생성자를 만든다.
open class Button
생성자가 아무 인자를 받지 않더라도 하위 클래스는 반드시 생성자를 호출해야 한다.
class RadioButton: Button()
이러한 규칙으로 인해 기반 클래스의 이름 뒤에는 꼭 괄호가 들어간다. 반면 인터페이스는 생성자가 없기 때문에 괄호를 사용하지 않는다. 따라서 괄호의 유무로 클래스인지 인터페이스인지 쉽게 판별할 수 있다.
어떤 클래스를 외부에서 인스턴스화하지 못하게 하려면 모든 생성자를 private로 만들면 된다.
class Secretive private constructor() {}
위 클래스는 유일한 주 생성자가 비공개이므로 외부에서 인스턴스화할 수 없다.
부 생성자: 상위 클래스를 다른 방식으로 초기화
일반적으로 코틀린은 생성자가 여럿 있는 경우가 자바보다 훨씬 적다. 자바와 달리 디폴트 파라미터 값과 이름 붙은 인자 문법을 제공하기 때문이다.
open class View {
constructor(ctx: Context) {}
constructor(ctx: Context, attr: AttributeSet) {}
}
이 클래스는 주 생성자를 선언하지 않고(클래스 헤더의 클래스 이름 뒤에 괄호가 없다) 부 생성자 2개를 선언한다. 부 생성자는 constructor
키워드로 시작하고 얼마든지 생성할 수 있다.
생성자에서 super
를 이용해 상위 생성자에게 객체 생성을 위임할 수 있다.
class MyButton: View {
constructor(ctx: Context): super(ctx) {}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}
생성자에서 this()
를 이용해 클래스 자신의 다른 생성자를 호출할 수 있다.
class MyButton: View {
constructor(ctx: Context): super(ctx, MY_STYLE) {}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.
인터페이스에 선언된 프로퍼티 구현
코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
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) //프로퍼티 초기화 식
}
커스텀 게터와 프로퍼티 초기화 식은 비슷하면서도 차이점이 존재한다. 프로퍼티 초기화 식은 객체 초기화 시 한번만 실행되며 커스텀 게터는 호출될 때마다 실행된다.
인터페이스는 게터와 세터가 있는 프로퍼티를 선언할 수 있다. 물론 뒷받침하는 필드를 참조할 수 없다.
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
게터와 세터에서 뒷받침하는 필드에 접근
프로퍼티의 저장된 값의 변경 이력을 로그에 남기려는 경우를 생각해보자.
class User(val name: String) {<br> var address: String = "unspecified"<br> set(value: String) {<br> <em>println</em>("""<br> Address was changed for $name:<br> "$<strong>field</strong>" -> "$value".""".<em>trimIndent</em>()) //뒷받침하는 값 필드 읽기<br> <strong>field </strong>= value //뒷받침하는 값 필드 변경하기<br> }<br>}
코틀린에서 프로퍼티의 값을 바꿀 때는 user.address = "new value"
를 사용한다. 이 구문은 내부적으로 address
의 세터를 호출한다. 이 세터를 커스텀하여 추가 로직을 실행할 수 있다.
접근자의 본문에서는 field
라는 식별자를 이용해 뒷받침하는 필드에 접근할 수 있다. 게터는 field
값을 읽을 수만 있고 세터는 읽고 쓸 수 있다.
접근자의 가시성 변경
접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 하지만 원한다면 가시성 변경자를 추가해서 가시성을 변경할 수 있다.
class LengthCounter {
var counter: Int = 0
private set //이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다
fun addWord(word: String) {
counter += word.length
}
}