본문 바로가기

코틀린

[Kotlin In Action] 4장. 클래스, 객체, 인터페이스

Kotlin in Action 책에서 각 챕터의 Summary를 보며 부족한 키워드만 후라닥 정리하는 글.
안드정이 코틀린 인 액션 책 스터디하면서 정리한 자료도 참고할 예정 땡큐땡큐 

클래스 계층의 정의

인터페이스

인터페이스나 상위 클래스의 매서드, 프로퍼티 재 정의시 override 수식어를 사용한다.

class Button : Clickable {
  override fun click() = println("I was clicked!")
}

메서드 본문을 메서드 시그니처 뒤에 추가함으로 디폴트 메서드를 구현할 수 있다.

interface Clickable {
    fun click()
    fun showOff() = println("defailt")
}

open, final, abstract

코틀린의 클래스와 클래스의 멤버는 기본적으로 final이다. 
상속을 허용할 클래스, 오버라이딩을 허용할 메서드와 프로퍼티 앞에 open을 붙여야한다. 

추상 클래스는 abstract로 선언하며, 추상 멤버는 open 변경자가 없어도 항상 open이다.

가시성 변경자

수식어 클래스 멤버 최상위 선언
public 모든 곳에서 접근 가능 모든 곳에서 접근 가능
internal 같은 모듈 안에서만 접근 가능 같은 모듈 안에서만 접근 가능
protected 같은 클래스 및 하위 클래스 안에서만 접근 가능 최상위 선언에 적용 불가
private 같은 클래스 안에서만 접근 가능 같은 파일 안에서만 접근 가능

확장함수의 가시성은 클래스의 가시성보다 낮거나 같아야 한다. 

중첩 클래스

변경자가 붙지 않는다면 자바의 static 중첩 클래스와 같다. 
외부 클래스에 대한 참조를 포함하고 하고 싶다면 inner 변경자를 붙여야한다. 
default가 중첩 클래스이며 내부 클래스로 사용 시 inner를 붙여야 한다. 

class Button : View {
  override fun getCurrentState(): State = ButtonState()
  override fun restoreState(state: State) { .... }
  class ButtonState : State { ... } 
}

내부 클래스에서 외부 클래스 참조 : this@클래스명

class Outer {
  inner class Inner {
    fun getOuterRef(): Outer = this@Outer // Outer에 대한 this
  }
}

 

봉인된 클래스

상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. 
어떤 클래스를 상속받는 하위 클래스는 여러 파일에 존재할 수 있기 때문에 컴파일러는 얼마나 많은 하위 클래스들이 있는지 알지 못한다.
sealed class를 사용해 동일 파일에 정의된 하위 클래스 외에 다른 하위 클래스는 존재하지 않는다는 것을 컴파일러에게 알려줄 수 있다.

  • 같은 파일 안에서만 하위 클래스 선언 가능 (1.1 부터)
  • sealed 클래스의 내부 클래스로만 하위 클래스 선언 가능 (1.0)

sealed 클래스의 하위 클래스를 정의할 때는 반드시 중첩시켜야 한다.
sealed 클래스를 상속한 하위 클래스를 다른 파일에서 다시 상속하는 것은 가능하다.

sealed 클래스와 when 식

when 식에 else 분기를 사용하지 않아도, 한 파일에 하위 타입이 몰려 있으므로 분기에서 누락한 타입 확인 가능하다. 

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.right) + eval(e.left)
    else ->
      throw IllegalArgumentException("Unknown expression") // else 체크를 해줬어야 함
}
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 Num ‐> e.value
    is Sum ‐> eval(e.left) + eval(e.right)
}

 

추가로 읽어보면 좋을 자료 

뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언

주 생성자

클래스명 뒤에 오는 괄호로 둘러싸인 부분.
생성자 파라미터를 지정하고 생성자 파라미터로 초기화할 프로퍼티를 정의한다. 
init 블록이나 프로퍼티 초기화 식에서만 주 생성자의 파라미터를 참조할 수 있다. 
생성자 파라미터에 디폴트값을 지정할 수 있다. (자바에서 했던 생성자 오버라이딩을 안해도 됨)

class User constructor(_nickname: String) {
    val nickname: String
    init { // 초기화 블록
        nickname = _nickname
    }
}

// 프로퍼티를 생성자 파라미터로 초기화
class User(_nickname: String) {
    val nickname = _nickname
}

// 파라미터로 프로퍼티를 바로 초기화
class User(val nickname: String)

인스턴스 생성 시에 init 블록이 생성자와 함께 들어간다. 
여기서 this도 사용할 수 있지만 대신 _로 변수를 구분한다. 

부 생성자

여러 방법으로 인스턴스 초기화할 방법이 필요하다면 부 생성자를 사용한다.

open class View {
  constructor(ctx: Context) { ... }
  constructor(ctx: Context, attr: AttributeSet) { ... }
}

class MyButton: View {
  // 다른 부 생성자 호출
  constructor(ctx: Context): this(ctx, MY_STYLE) { ... }
  // 상위 클래스 생성자 호출
  constrcutor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
  }
}

주 생성자가 존재하면 부 생성자는 직접 혹은 다른 부 생성자를 통해 주 생성자를 호출해야한다. 
만약 주 생성자가 없다면 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야한다. 
(자바에서는 권장인게 코틀린에서는 규칙이다.)

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

인터페이스에 추상 프로퍼티를 선언할 수 있다.

interface User {
  val nickname: String 
}

인터페이스의 추상 프로퍼티는 backing field나 게터 등의 정보가 없기 때문에
이를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다. 

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 = getFBName(accountId) // 프로퍼티 초기화 식, backing field에 초기화 식 결과 저장
}

접근자(getter, setter)에서 backing field

접근자 내부에서 field에 접근할 때 사용한다.
getter에서는 filed의 값을 읽을 수만 있으며, 세터에서는 filed 값을 읽거나 쓸 수 있다.

var count = 0
    set(value) {
        if(value >= 0) field = value
    }

접근자의 가시성은 기본적으로 프로퍼티 가시성과 같은데, get이나 set 앞에 가시성 수식어를 추가해 가시성을 변경할 수 있다.

class LengthCounter {
    var counter: Int = 0
        private set
    fun addWord(word: String) {
        counter += word.length 
    }
}

컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임

data 클래스

data class Client(val name: String, val postalCode: Int)

equals(), hashCode(), toString() 메서드를 자동으로 생성한다.

data 클래스의 copy()

val mazzi = Client("마찌", 41225)
println(mazzi.copy(postalCode = 4000)) // name이 그대로 복사된다.

객체 복사를 좀 더 편하게 해주는 copy() 를 제공한다. 
복사할 때 일부 프로퍼티 변경을 허용한다.
객체를 메모리 상에서 바꾸지 않고 그대로 복사해 원본 객체와는 다른 생명주기를 가진다.

클래스 위임: by 키워드 사용

by 키워드를 통해 데코레이터 패턴을 사용할 수 있다. 

➕ 데코레이터 패턴

상속을 허용하지 않는 클래스 대신 새 클래스가 생성되어 원래 클래스와 동일한 인터페이스를 구현하고 원본 클래스의 인스턴스를 필드로 저장하는 것

class CountingSet<T>(
    innerSet: MutableCollection<T> = HashSet<T>()
): MutableCollection<T> by innerSet {
    // Collection<T> 타입에 대한 메서드 호출시 innerList에 위임
    var objectsAdded = 0
    // 필요한 메서드는 재정의 가능
    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }
    override fun addAll(c: Collection<T>): Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

object 키워드: 클래스 선언과 인스턴스 생성

싱글턴

object 키워드로 싱글턴을 만들 수 있다.

object Payroll {
    val allEmployees = arraylistOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()

중첩 객체로도 사용할 수 있다.

data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

동반 객체 (companion object)

companion을 붙인 클래스 안에 정의된 객체.
동반 객체의 멤버 사용은 정적 메서드, 필드 사용과 유사하다. 
동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다.

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

동반 객체도 인터페이스 구현이나 클래스 확장, 동반 객체에 대한 확장 함수도 가능하다.

interface JSONFactory<T> { ... }
class Person(vavl name: String) {
    companion object : JSONFactory<Person> { ... }
}
class Person(val firstName: String, val lastName: String) {
    companion object {  // 비어있는 동반 객체 선언
    }
}

fun Person.Companion.fromJSON(json: String) : Person { // 확장 함수 선언, 동반 객체 기본 이름인 Companion 사용
}

객체 식

object 키워드를 사요애서 익명 객체를 정의할 수 있다.

var clickCount = 0
window.addMouseListener(
object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        clickCount++
    }
    ...
})

부딪히며 배운 코틀린이다 보니 근로를 하며 가끔은 기초적인 부분을 못지키고 지나갈 때가 생긴다.
(확장함수의 가시성이라던가 플랫폼 타입이라던가)
클래스, 생성자 부분은 스스로 정리가 필요했던 부분이라 후라닥 정리해봤다.
정리하다 보니 코틀린 진짜 매력 쩌는거 같다.