interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State)
}
// Java
public class Button implements View{
@Override
public State getCurrentState(){
return new ButtonState();
}
@Override
public void restoreState(State state){
...
}
public class ButtonState implements State{
...
}
}
Java의 경우 Button을 직렬화하면 java.io.NotSerializableException: Button
이라는 오류가 발생한다. Java에서는 다른 클래스 안에 정의한 클래스는 자동으로 inner class
가 된다. 그래서 ButtonState클래스는 외부 Button 클래스에 대한 참조를 묵시적으로 포함한다. 그 참조로 인해 Button State를 직렬화할 수 없다. 이 문제를 해결하려면 ButtonState를 static class
로 선언해야 한다.
// Kotlin
class Button: View{
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State){
...
}
class ButtonState: State{
...
}
}
코틀린의 경우 중첩 클래스에 아무런 변경자도 붙지 않으면 자바의 static class
와 같다. 만일 외부 클래스에 대한 참조를 포함하게 만들고 싶으면 inner
변경자를 붙여서 inner class
를 만든다.
클래스 B안에 정의된 클래스 A | Java | Kotlin |
---|---|---|
중첩 클래스(외부 클래스에 대한 참조를 저장 X) | static class A | class A |
내부 클래스(외부 클래스에 대한 참조를 저장 O) | class A | inner class A |
내부 클래스 Inner 안에서 외부 클래스 Outer의 참조에 접근하려면 this@Outer
라고 써야 한다.
class Outer{
var num = 10
inner class Inner{
fun getOuterNumber(): Int = this@Outer.num
}
}
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr): Expr
fun eval(e: Expr): Int =
when(e){
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else ->
throw IllegalArgumentException("Unknown expression")
}
항상 디폴트 분기를 추가하는게 편하지는 않다. 또 실수로 새로운 클래스 처리를 잊어버리면 디폴트 분기가 선택되기 때문에 심각한 버그가 발생할 수 있다.
이를 위해 sealed class
를 사용한다. 상위 클래스에 sealed
변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. 또 sealed class
는 항상 열려있기 때문에 open
변경자를 붙일 필요가 없다.
sealed class Expr {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when (e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
}
// 컴파일러가 sealed class의 자식 클래스에 누가 있는지 알고 있다.
constructor
주 생성자나 부 생성자 정의를 시작할 때 사용한다.
init
초기화 블록을 시작한다. 초기화 블록에는 클래스가 인스턴스화
될 때 실행되는 초기화 코드가 들어가며 주 생성자와 함께 사용된다. 한 클래스 안에 여러 초기화 블록을 선언할 수 있다. 초기화 블록은 주 생성자 직후에 실행되며 부 생성자보다는 먼저 실행된다.
class User constructor(_nickname: String){ // 파라미터가 1개인 주 생성자
val nickname: String // 프로퍼티
init { // 초기화 블록
nickname =_nickname
}
}
nickname 프로퍼티를 초기화하는 코드를 프로퍼티 선언과 동시에 할 수 있고, 주 생성자 앞에 별다른 annotation
이나 visibility modifer
가 없으면 constructor
를 생략 할 수 있다.
주 생성자의 파라미터는 프로퍼티의 초기화 식이나 초기화 블록 안에서만 참조할 수 있다.
class User(_nickname: String){
val nickname = _nickname
}
아래와 같이 더 간결하게 쓸 수 있다.
class User(val nickname: String)
open class User(val nickname: String) {...}
class TwitterUser(nickname: String) : User(nickname) {...}
디폴트 생성자
를 만든다.open class Button
class RadiButton: Button()
// Button 생성자는 아무 인자도 받지 않지만
// 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.
private
로 만들면 된다.class SecretObject private constructor() {}
클래스에 여러 개의 부 생성자를 가질 수 있다. constructor
키워드를 사용하며 주 생성자와 달리 생략할 수 없다.
주 생성자가 정의된 경우, 부 생성자에서 호출하는 생성자를 따라가면 반드시 주 생성자를 호출해줘야 한다. 클래스의 다른 생성자를 클래스 내부에서 호출할 때는 :this()
를 이용한다.
class Person(val name: String) {
var age: Int = 0
var weight: Int = -1
constructor(_name: String, _age: Int) : this(_name) {
age = _age
}
constructor(_name: String, _age: Int, _weight: Int) : this(_name, _age) {
weight = _weight
}
}
인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여러 개 만들지 말자.
대신 파라미터에 default값을 줌으로써 해결할 수 있다.부 생성자가 필요한 주된 이유는 Java와의 상호운용성 때문이다. 이외에도 클래스 인스턴스를 생성할 때 파라미터 목록이 다른 생성 방법이 여러 개인 경우 사용한다.
open class View { // 주 생성자 없이 부 생성자만 2개
constructor(ctx: Context) {...}
constructor(ctx: Context, attr: AttributeSet) {...}
}
class MyButton : View {
constructor(ctx: Context) : super(ctx) {...}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {...}
}
// super 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출한다.
class MyButton: View{
constructor(ctx: Context) : this(ctx, MY_STYLE) { ... }
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) { ... }
}
interface User {
val nickname: String
}
class PrivateUser(override val nickname: String) : User
// 주 생성자 안에 프로퍼티를 직접 선언
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore('@')
}
// 뒷받침하는 필드에 값을 저장하지 않고 매번 이메일 주소에서 nickname을 계산한다.
class FacebookUser(val accountId: Int) : User {
override val nickname = getFacebookName(accountId)
}
// 객체 초기화할 때 데이터를 뒷받침하는 필드에 저장하고 그 값을 불러온다.
interface User{
val email: String
val nickname: String
get() = email.substringBefore('@') // 매번 결과를 계산해서 리턴
}
// email은 반드시 구현해야 하지만
// nickname은 구현하지 않으면 인터페이스에서 정의된 default getter를 사용한다.
프로퍼티에는 getter / setter와 같은 함수가 내장되어 있고, 프로퍼티가 가진 값은 field
에 저장된다. 프로퍼티 외부에서는 get()이나 set()을 호출하지만 get(), set() 내부에서는 field
를 통해 프로퍼티가 가지고 있는 값에 접근한다.
( field는 get(), set()에서만 사용 가능하다. )
field
를 통해서 뒷받침하는 필드에 접근할 수 있다. getter는 field
값을 읽을 수만 있고 setter는 field
값을 읽거나 쓸 수 있다.
class User(val name: String) {
var address: String = "unspecified"
set(value) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent())
field = value
}
}
>>> val user = User("Alice")
>>> user.address = "Seoul"
Address was changed for Alice:
"unspecified" -> "Seoul".
class Person {
var name: String = "F"
var age: Int=0
get() = age
}
>>> Person().age
// 주의! 무한 재귀에 빠진다.
class LengthCounter {
var counter: Int = 0
private set // 이 클래스 밖에서 counter의 값을 변경할 수 없다.
fun addWord(word: String) {
counter += word.length
}
}