[Kotlin] 클래스, 객체, 인터페이스(2)

sw·2022년 2월 7일
0
post-custom-banner

1. 내부 클래스와 중첩 클래스: 기본적으로 중첩 클래스!!

  • 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 외부 클래스 인스턴스에 대한 접근 권한이 없다. ( Java의 정적 중첩 클래스와 대응한다 )
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안에 정의된 클래스 AJavaKotlin
중첩 클래스(외부 클래스에 대한 참조를 저장 X)static class Aclass A
내부 클래스(외부 클래스에 대한 참조를 저장 O)class Ainner class A

내부 클래스 Inner 안에서 외부 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.

class Outer{
    var num = 10
    inner class Inner{
        fun getOuterNumber(): Int = this@Outer.num
    }
}



2. Sealed 클래스: 클래스 계층 정의 시 계층 확장 제한

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의 자식 클래스에 누가 있는지 알고 있다.



3. 주 생성자와 초기화 블록

  • 주 생성자
    생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의한다. 클래스 옆에 괄호 표기로 정의된 것을 말하며 클래스에 최대 1개만 존재한다.
  • 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() {}



4. 부 생성자 : 상위 클래스를 다른 방식으로 초기화

  • 클래스에 여러 개의 부 생성자를 가질 수 있다. 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) { ... }
}



5. 인터페이스에 선언된 프로퍼티 구현

  • Kotlin에서는 인터페이스에 추상 프로퍼티 선언을 할 수 있다. 하지만 인터페이스는 뒷받침 하는 필드를 가질 수 없기 때문에 초기화가 불가능하다. 대신 인터페이스를 구현하는 클래스에서 getter나 setter를 구현해야 한다.
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)
}
// 객체 초기화할 때 데이터를 뒷받침하는 필드에 저장하고 그 값을 불러온다.

  • 인터페이스에 getter와 setter가 있는 프로퍼티를 선언할 수 있다. 물론 상태를 저장할 수는 없다.
interface User{
    val email: String
    val nickname: String
        get() = email.substringBefore('@')	// 매번 결과를 계산해서 리턴
}

// email은 반드시 구현해야 하지만 
// nickname은 구현하지 않으면 인터페이스에서 정의된 default getter를 사용한다.



6. getter와 setter에서 뒷받침 하는 필드에 접근

  • 프로퍼티에는 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
// 주의! 무한 재귀에 빠진다.

  • get(), set()의 가시성 변경
class LengthCounter {
    var counter: Int = 0
      private set				// 이 클래스 밖에서 counter의 값을 변경할 수 없다.
    
    fun addWord(word: String) {
        counter += word.length
    }
}
profile
끄적끄적
post-custom-banner

0개의 댓글