Class, Interface

코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다르다.

코틀린 인터페이스에는 프로퍼티 선언이 들어갈 수 있다. 코틀린 선언은 기본적으로 final이며 public이다.

자바의 클래스와 메소드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메소드는 기본적으로 final

어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. (메소드나 프로퍼티도 동일)

open class Apple : ModelClick {
   fun mini() {}
   open fun pro() {}
   override click() {}
}

⇒ Apple 클래스는 open 이므로 다른 클래스가 상속할 수 있다.

⇒ mini() 메소드는 final 이므로, 하위 클래스가 이 메소드를 오버라이드 할 수 없다.

⇒ pro() 함수는 열려 있으므로, 하위 클래스에서 오버라이드 할 수 있다.

⇒ click() 함수는 상위 클래스에서 열려 있는 메소드이기 떄문에 오버라이드한 함수는 기본적으로 열려 있다. 오버라이드 하지 못하게 금지하려면 오버라이드 하는 메소드 앞에 final을 붙이면 된다.

(기반 클래스나 인터페이스의 멤버를 오버라이드 하는 경우 그 메소드는 기본적으로 열려 있다)

abstract class Apple {
   abstract fun mini()
	 fun pro()
	 open fun test()
}

⇒ pro(), test() 함수는 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드를 허용할 수 있다.

❗️ 인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다. 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다.

가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다. 코틀린에서 아무 변경자도 없는 경우 선언은 모두 공개된다.

코틀린 가시성 변경자

internal open class Apple : ModelClick {
  private fun banana() = println("banana?")
	protected fun trash() = println("trash")
}

fun Apple.itemClick() { // public 멤버가 internal을 받기 떄문에 컴파일 오류
   banana() // Apple의 private 멤버라 컴파일 오류
   trash() // Apple의 protected 멤버라 컴파일 오류
}

코틀린은 public 함수인 itemClick 안에서 그보다 가시성이 더 낮은 타입 Apple을 참조하지 못한다. 어떤 클래스의 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다는 더 일반적인 규칙에 해당한다. 따라서 위 코드의 컴파일 오류를 없앨려면 확장함수는 itemClick의 가시성을 internal로 바꾸거나 Apple의 가시성을 public으로 바꿔야 한다.

클래스 안에 다른 클래스를 선언하면 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용하다.

interface State: Serializable

interface View {
   fun getCurrentState(): State
	 fun restoreState(state: State( {}
}

class Apple : View {
  override fun getCurrentState(): State = AppleState()
  override fun restoreState(state: State) { /*..*/ }
 
  class AppleState : State { /*..*/ }
}

관계

⇒ 중첩 클래스 안에는 바깥쪽 클래스에 대한 참조가 없지만 내부 클래스에는 있다.

내부 클래스 inner안에서 바깥쪽 클래스의 참조에 접근 하려면 this@바깥클래스 라고 써야한다.

class Outer {
  inner class Inner {
     fun test(): Outer = this@Outer
  }
}
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 Error")
    }

⇒ 코틀린 컴파일러는 when을 사용해 Expr 타입의 값을 검사할 떄 꼭 디폴트 분기인 else분기를 덧붙이게 강제한다. 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없다. 실수로 새로운 클래스 처리를 잊어버렸더라도 디폴트 분기가 선택되기 떄문에 심각한 버그가 발생할 수 있다. 이 해법을 제공하는 것이 sealed 클래스다. 위 코드를 sealed 클래스를 사용해서 고쳐보자.

❗️ sealed 클래스 : 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.

sealed class Expr { // 기반 클래스를 sealed로 봉인!
    class Num(val value: Int): Expr() // 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열
    class Sum(val left: Expr, val right: Expr) : Expr()
}

// when식이 모든 하위 클래스를 검사하므로 else 분기처리가 없어도 된다.
fun eval(e: Expr): Int =
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

⇒ sealed로 표시된 클래스는 자동으로 open이다.

Data Class

어떤 클래스가 데이터를 저장하는 역할만을 수행한다면 toString, equals, hashCode를 반드시 오버라이드 해야한다. data라는 변경자를 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 만들어 준다.

data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.

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

⇒ Client 클래스는 자바에서 요구하는 모든 메소드를 포함한다.

equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어 진다. 주 생성자 밖에 정의된 프로퍼티는 equals나 hashCode를 계산할 떄 고려의 대상이 아니다!

데이터 클래스의 프로퍼티가 꼭 val일 필요는 없다. var 프로퍼티를 써도 된다. 하지만! 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다. HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적이다. 데이터 클래스 객체를 키로 하는 값을 컨테이너에 담은 다음에 키로 쓰인 데이터 객체의 프로퍼티를 변경하면 컨테이너 상태가 잘못될 수 있다. 게다가 불변 객체를 사용하면 프로그램에 대해 훨씬 쉽게 추론할 수 있다.

데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있는 메소드 copy()는 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해준다.