본문 바로가기
코틀린/코틀린 공식문서

[코틀린 공식문서] Inheritance; 상속

by 측면삼각근 2023. 8. 13.
728x90
반응형

들어가기 전에

해당 블로깅은 코틀린 공식문서의 Inheritance를 번역하며 학습한 내용입니다.

학습 중임에 따라 이해하는데 도움이 되는 부분들을 추가되고 의역된 부분이 있습니다. 혹시 잘못된 설명이 있다면 얼마든지 제보해 주세요.


Inheritance 상속

Any는 모든 클래스의 공통적으로 상위 클래스이다. (더 이상 슈퍼 타입이 존재하지 않음)

class Example // Implicitly inherits from Any

Any에는 euals(), hashCode(), toString() 세 가지 메서드가 있고, 따라서 모든 코틀린 클래스에 대하여 해당 메서드가 정의되어 있다.

기본적으로 코틀린 클래스는 final로 지정되어 있고, 상속이 될 수 없다.
상속을 가능하게 만들려면 open 키워드를 사용해야 한다.

open class Base // Class is open for inheritance

명시적으로 슈퍼타입을 선언하려면, 클래스 헤더에 콜론 뒤에 타입을 배치해야 한다.

open class Base(p: Int)

class Derived(p: Int) : Base(p)

파생된 클래스(자식클래스)에 기본 생성자가 있는 경우 매개변수로 초기화할 수 있다.

파생 클래스에 기본 생성자가 없는 경우 보조 생성자(secondary constructor)는 super 키워드를 사용하여 초기화할 수 있다.

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

Overriding methods

재정의 가능한 곳에 명시적으로 수정자(open)가 필요하다.

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

Shape을 상속받은 Circle클래스에서, draw 함수를 오버라이드 하기 위해서는 override키워드가 필요하다.

부모 함수에 open 키워드가 기재되지 않는 경우 (fill 함수의 경우) 하위 클래스에서 동일한 이름을 가진 메서드를 선언하는 것은 허용되지 않는다.
open은, final클래스(open이 기재되지 않은 클래스)의 멤버에 추가되어도 적용되지 않는다.

override로 표기된 맴버는 그 자체로 열려있어서, 하위클래스에서 재정의를 금지하려면 final 키워드를 사용해야 한다.

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

Overriding properties

속성의 오버라이딩 메커니즘은 메서드 오버라이딩과 동일하다.
상위 클래스에서 선언 후 자식 클래스에서 다시 선언되는 속성은 override를 앞에 기재해야 하며, 형식이 호환되어야 한다.
선언된 속성은 이니셜라이저나 get메서드가 있는 속성으로 재정의 할 수 있다.

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

val 프로퍼티를 var 프로퍼티로 재정의 할 수도 있지만, 그 반대의 경우는 불가능하다.

  • ✅ open val <- override var
  • ❌ open var <- override  val

val속성이 get메서드를 기본적으로 선언하고, var로 재정의 하면 자식 클래스에서 set 메서드를 추가로 선언하기 때문에 var로 재정의가 허용된다.

override 키워드는 기본 생성자에서 프로퍼티 선언의 일부로 사용할 수 있다.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 vertices

class Polygon : Shape {
    override var vertexCount: Int = 0  // Can be set to any number later
}

Derived class initialization order; 파생 클래스 초기화 순서

파생 클래스의 새 인스턴스를 생성하는 동안, 자식클래스보다 부모 클래스의 초기화가 먼저 수행된다.

open class Base(val name: String) {

    init { println("Initializing a base class") }

    open val size: Int = 
        name.length.also { println("Initializing size in the base class: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {

    init { println("Initializing a derived class") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}

fun main() {
    println("Constructing the derived class(\"hello\", \"world\")")
    Derived("hello", "world")
}
/*
Constructing the derived class("hello", "world")
Argument for the base class: Hello
Initializing a base class
Initializing size in the base class: 5
Initializing a derived class
Initializing size in the derived class: 10

*/

기본 클래스 생성자(constructor)가 실행될때  파생(자식) 클래스에서 선언되거나 재정의된 속성이 아직 초기화되지 않는다.
따라서 기본 클래스 초기화 로직에서 프로퍼티 중 임의의 것을 것(직-간접적으로 재정의된 프로퍼티)을 사용하는 것은 에러를 마주하기 쉽다. 따라서 기본 클래스를 설계할 때, 생성자, 속성 초기화자(property initializer) 혹은 init블록에서 open맴버를 사용하는 것을 피해야 한다.

Calling the superclass implementation; 슈퍼 클래스 구현 호출

파생된 클래스의 코드는 super키워드를 통하여 상위 클래스 함수와 프로퍼티 구현을 호출 할 수 있다.

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }

    val fillColor: String get() = super.borderColor
}

내부 클래스(inner class) 내에서 외부 클래스(outer class)의 상위 클래스에 엑세스 하는것은 super@Outer 키워드를 사용하면 된다.

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}


class FilledRectangle: Rectangle() {
    override fun draw() {
        val filler = Filler()
        filler.drawAndFill()
    }
    
    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
        }
    }
}


fun main() {
    val fr = FilledRectangle()
        fr.draw()
}

/*
Drawing a rectangle
Filling
Drawn a filled rectangle with color black
*/

Overriding rules 규칙 재정의

 

클래스가 직계 수퍼클래스에서 동일한 맴버의 여러 구현을 상속하는 경우, 이 맴버를 재 정의하고 자체 구현을 제공해야 한다.

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // The compiler requires draw() to be overridden:
    override fun draw() {
        super<Rectangle>.draw() // call to Rectangle.draw()
        super<Polygon>.draw() // call to Polygon.draw()
    }
}

Rectangle과 Polygon을 모두 상속할 수 있지만, 두 클래스 모두에서 draw의 구현이 있으므로, Square에서 draw를 재정의하고 별도의 구현을 제공해야 한다.

반응형