Kotlin In Action #3
클래스, 객체, 인터페이스
코틀린의 클래스와 인터페이슨느 자바의 클래스, 인터페이스와 약간 다름.
자바와 달리 코틀린 선언은 기본적으로 public, final 임.
코틀린은 중첩 클래스는 기본적으로 내부 클래스가 아님.
클래스 계층 정의
코틀린 인터페이스
- 코틀린은 자바 8 인터페이스와 비슷.
- 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현 메서드도 정의 가능. (자바 8의 디폴트 메서드와 비슷)
- 인터페이스에 아무런 필드가 들어갈 수 없음.
interface Clickable {
fun click()
}
class Button: Clickable {
override fun click() = println("Button Click")
}
- 자바에서는 extends와 implements 키워드를 사용하지만, 코틀린에서는 클래스 이름 뒤에 콜론(:)을 사용해 클래스 확장과 인터페이스 구현을 모두 처리.
- 자바와 동일하게 인터페이스는 개수 제한 없이 구현 가능하지만, 클래스는 오직 하나만 확장 가능.
- override 변경자는 자바의 @Override 어노테이션과 비슷하지만 필수적으로 사용해야 함.
interface Clickable {
fun click()
fun showOff() = println("Show Off")
}
자바와 다르게 메서드에 디폴트 구현을 할 때 특별한 키워드가 필요 없음.
interface Focusable {
fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("Focusable")
}
class Button: Clickable, Focusable {
override fun click() = println("Button Click")
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
이름과 시그니처가 동일한 멤버 메서드에 대해 둘 이상의 디폴트 구현이 있는 경우 하위 클래스에서 명시적으로 새로운 구현을 제공해야 함.
open, final, abstract 변경자: 기본적으로 final
자바에서 final로 명시적으로 상속을 금지하지 않은 모든 클래스는 다른 클래스가 상속 가능.
기본적으로 상속이 가능하면 편리한 경우도 있지만 문제가 생기는 경우도 많음.
- 취약한 기반 클래스라는 문제는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우.
- 코틀린은 클래스와 메서드는 기본적으로 final임.
열린 메서드를 포함하는 열린 클래스 정의
open class RichButton : Clickable { // 해당 클래스는 다른 클래스가 상속 가능
fun disable() {} // final 함수, 하위 클래스가 오버라이드할 수 없음
open fun animate() {} // 열려있는 함수, 하위 클래스에서 오버라이드 가능
override fun click() {} // 오버라이드한 함수는 기본적으로 열려있음
}
- 오버라이드한 함수는 기본적으로 열려있음. 이를 방지하려면 앞에 final을 붙여줘야 함.
추상 클래스 정의
abstract class Animated { // 추상 클래스는 인스턴스를 만들 수 없음. (구현이 없는 추상 멤버가 있기 때문)
abstract fun animate() // 추상 함수는 기본적으로 open
open fun stopAnimating() {} // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 final
fun animateTwice() {}
}
가시성 변경자: 기본적으로 공개
- 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어함.
- 특정 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 잇음.
- 가시성 변경자 종류
- public (default)
- protected
- private
- 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용하여 package-private (자바의 default)를 사용하지 않음.
- 패키지 전용 가시성에 대한 대안으로 internal이라는 새로운 가시성 변경자를 도입.
- internal은 "모듈 내부에서만 볼 수 있음"
- 모듈은 한 번에 컴파일되는 코틀린 파일들을 의미.
변경자 | 클래스 멤버 | 최상위 선언 |
public (default) | 모든 곳에서 볼 수 있음. | 모든 곳에서 볼 수 있음. |
internal | 같은 모듈 안에서만 볼 수 있음. | 같은 모듈 안에서만 볼 수 있음. |
protected | 하위 클래스 안에서만 볼 수 있음. | 최상위 선언에 적용할 수 없음 |
private | 같은 클래스 안에서만 볼 수 있음. | 같은 파일 안에서만 볼 수 있음. |
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // "public" 멤버가 자신의 "internal" 수신 타입인 "TalkativeButton"을 노출함
yell() // "yell"에 접근할 수 없음.
whisper() // "whisper"에 접근할 수 없음.
}
- public 함수 안에서 그보다 가시성이 더 낮은 타입을 참조하지 못하게 함.
- giveSpeech 함수는 public / TalkativeButton 클래스는 internal
- 가시성은 같거나 더 높아야 한다는 일반적인 규칙이 있음.
코틀린의 가시성 변경자와 자바
코틀린의 public, protected, private 변경자는 컴파일된 자바 바이트 코드 안에서도 그대로 유지 됨.
유일한 예외는 자바에서는 클래스를 private로 만들 수 없으므로 내부적으로 코틀린의 private 클래스를 패키지 전용 클래스로 컴파일 함.
internal 변경자는 자바에 딱 맞는 가시성이 없음. 바이트 코드상으로는 public이 됨.
코틀린과 자바 가시성 규칙의 다른 차이는 코틀린에서는 외부 클래스가 내부 클래스나 중첩 클래스의 private 멤버에 접근할 수 없음.
내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스
코틀린의 중첩 클래스는 자바와 다르게 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음.
예시1)
직렬화할 수 있는 상태가 있는 뷰 선언
import java.io.Serializable
interface State: Serializable
interface View {
fun getCurrentState() : State
fun restoreState(state: State) {}
}
자바에서 내부 클래스를 사용해 View 구현
import org.jetbrains.annotations.NotNull;
public class Button implements View{
@NotNull
@Override
public State getCurrentState() {
return new ButtonState();
}
@Override
public void restoreState(@NotNull State state) {
/* ... */
}
public class ButtonState implements State {
/* ... */
}
}
- State 인터페이스를 구현한 ButtonState 클래스를 정의하여 Button에 대한 구체적인 정보를 저장.
- 버튼의 상태를 직렬화하면 NotSerializableException: Button 발생.
- 자바에서는 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 됨.
- ButtonState 클래스는 바깥쪽 Button 클래스에 대한 참조를 묵시적으로 포함.
- 해결하기 위해서는 ButtonState를 static 클래스로 선언해야 함. (묵시적인 참조가 사라짐)
중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) {
/* ... */
}
class ButtonState :State {
/* ... */
}
}
- 코틀린 중첩 클래스에는 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같음.
- 반대로 바깥족 클래스에 대한 참조를 포함하게 만들고 싶으면 inner 변경자를 붙여야 함.
Inner 클래스에서 Outer 클래스의 참조에 접근
class Outer {
val a = 1;
inner class Inner {
fun getOuterReference() : Outer = this@Outer
fun getValue() : Int = a
}
}
봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
인터페이스 구현을 통한 식 표현하기
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")
}
- 코틀린은 when을 사용하여 Expr 타입의 값을 검사할 때 꼭 디폴트 분기인 else 분기를 덧붙이게 강제함.
- 새로운 하위 클래스를 추가했을때 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사가 불가능.
- sealed 변경자를 붙이면 하위 클래스 정의를 제한할 수 있음.
sealed class Expr { // sealed로 봉인
class Num(val value: Int): Expr() // 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열
class Sum(val left: Expr, val right: Expr): Expr()
}
fun eval(e: Expr): Int =
when (e) { // when 식이 모든 하위 클래스를 검사하므로 별도의 else 분기 필요 없음.
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
- sealed로 표시된 클래스는 자동으로 open 변경자.
- 내부적으로 Expr 클래스는 private 생성자를 가짐.
코틀린 1.0에서는 하위 클래스는 중첩 클래스여야 하고, 데이터 클래스로 sealed 클래스를 상속할 수 없음.
1.1 부터는 해당 제한이 완화 됨.
'Programming > Kotlin' 카테고리의 다른 글
[당근마켓 밋업 정리] - Kotlin Coroutines 톺아보기 (0) | 2022.01.09 |
---|---|
Kotlin In Action #4 (0) | 2022.01.05 |
Kotlin In Action #2 (0) | 2021.12.11 |
Kotlin In Action #1 (0) | 2021.12.06 |
Kotlin In Action #0 (0) | 2021.12.05 |