본문 바로가기

Kotlin

Kotlin 도큐먼트: 클래스 상속

더보기

Kotlin에서 정의하는 모든 클래스는 부모 클래스 Any를 상속합니다.

예를 들어 다음 클래스 Empty는 Kotlin에 의해서 암묵적으로 Any를 상속합니다.

class Empty

Anyequals() hashCode() 그리고 toString() 메소드를 제공하며 Kotlin의 모든 클래스에서 해당 메소드에 엑세스 할 수 있습니다.

클래스 상속

기본적으로 Kotlin의 모든 클래스는 final로 선언되며 상속 할 수 없습니다.

클래스를 다른 클래스에 상속시키려면 open 키워드를 사용합니다.

open class Base

상속은 클래스 선언부에 이어 콜론(:)으로 진행합니다.

자식 클래스에서 주 생성자(Primary constructor)가 있으면 부모 클래스는 자식 클래스의 주 생성자로부터 초기화됩니다.

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)
}

함수 오버라이딩

Kotlin은 오버라이드 가능한 멤버 함수에 대해서 override 키워드를 명시해야 합니다.

open class Shape {
    open fun draw() { }
    fun fill() { }
}

class Circle() : Shape() {
    override fun draw() { }
}
코드 비고
Line 1 open 상속되는 클래스는 open 키워드로 선언됩니다.
Line 2 open fun draw() open 키워드로 선언되어 자식 클래스에서 오버라이드 될 수 있습니다.
Line 3 fun fill() 암묵적으로 final이므로 자식 클래스에서 오버라이드 될 수 없습니다.
Line 6 Circle() : Shape() Shape 클래스를 상속합니다. Shapeopen 키워드로 선언됩니다.
Line 7 override fun draw() 부모 클래스의 함수를 오버라이드합니다.
명시적으로 override 키워드를 선언해야 하며 부모 클래스에서 해당 함수는 open 키워드로 선언됩니다.
더보기

override 된 함수는 그 자체로 open 키워드를 포함합니다.

만약 오버라이드 함수를 자신의 자식 클래스에서 re-overriding하는 것을 방지하려면 final 키워드와 함께 오버라이드합니다.

open class Rectangle() : Shape() {
	// 이 함수는 Shape()의 draw()를 재정의합니다.
	// final 키워드로 인해 이 클래스를 상속하는 다른 클래스가 재정의하지 못합니다.
	final override fun draw() { }
}

프로퍼티 오버라이딩

함수 오버라이딩과 동일한 메커니즘으로 프로퍼티를 오버라이딩 할 수 있습니다.

부모 클래스의 프로퍼티를 재정의하려면 반드시 override 키워드와 함께 사용하며 데이터 타입은 호환되어야 합니다.

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

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

부모 클래스의 val 프로퍼티는 자식 클래스에서 var 프로퍼티로 재정의 될 수 있습니다.

단, 반대의 방향인 var(Mutable)에서 val(Immutable)로 재정의는 불가능합니다.

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

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

멤버 변수 뿐만 아니라 주 생성자에서도 프로퍼티를 오버라이딩 할 수 있습니다.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape

상속 관계에서의 초기화 코드 실행 우선 순위

자식 클래스를 인스턴싱 할 때 부모 클래스의 초기화 코드가 먼저 실행됩니다.

부모 클래스의 초기화 코드가 선행되고 나면 자식 클래스의 초기화 코드가 이어서 진행됩니다.

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") }
}
더보기

부모 클래스를 설계할 때 초기화 코드에서는 open 키워드로 지정된 함수 또는 프로퍼티에 접근하지 않는 것이 좋습니다.

초기화 코드가 실행된 이후에 open 함수와 프로퍼티의 값이 override로 바뀌거나 수행되는 방식에 차이가 있을 수 있습니다.

만약 코드 실행에는 문제가 없다고 하더라도 가독성이 떨어지거나 유지보수에 악영향을 줄 수 있습니다.

super 키워드

자식 클래스는 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
}
코드 비고
Line 8 super.draw() 부모클래스의 함수를 호출합니다.
Line 12 super.borderColor 부모 클래스의 프로퍼티에 엑세스합니다.

내부 클래스에서 외부 클래스의 부모 클래스에 엑세스하려면 super@<외부 클래스 이름>과 같이 사용합니다.

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()
        }
    }
}
코드 비고
Line 1 FilledRectangle: Rectangle Rectangle의 자식 클래스이며 Filler의 외부 클래스입니다.
Line 7 inner class Filler FilledRectangle의 자식 클래스입니다.
Line 10 super@FilledRectange.draw() FilledRectangle의 부모 클래스 Rectangle에 엑세스합니다.

동시 상속에 대한 오버라이딩 규칙

Kotlin에서도 Java처럼 여러 개의 클래스를 동시에 상속 받을 수 있습니다.

open class Rectangle { }

interface Polygon { }

class Square() : Rectangle(), Polygon { }

이때 부모 클래스 간에 멤버가 중복되면 자식 클래스는 이를 반드시 오버라이딩해야 합니다.

다음 예시처럼 RectanglePolygon을 동시에 상속하는 것을 허용합니다.

단, 부모 클래스에서 draw() 멤버 함수가 중복되기 때문에 모호성을 제거하기 위한 의무로 draw()를 반드시 오버라이딩합니다.

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()
    }
}

정리 및 복습

  • Koltin의 모든 클래스는 Any를 상속합니다.
  • Any는 equals() hashCode() 그리고 toString() 메소드를 제공합니다.
  • 클래스를 상속시키려면 open 키워드를 사용합니다.
  • 상속은 클래스 선언부에서 콜론(:)으로 수행합니다.
open class Base(p: Int)

class Derived(p: Int) : Base(p)
  • 상속 관계에서 초기화 코드는 부모 클래스의 코드가 먼저 실행됩니다.
  • 자식 클래스는 override 키워드로 부모 클래스의 함수와 프로퍼티를 오버라이딩 할 수 있습니다.
  • 부모 클래스의 함수나 프로퍼티에 엑세스하려면 super 키워드를 사용합니다.
super.draw()
  • 내부 클래스가 외부 클래스의 부모 클래스에 엑세스하려면 super@<외부 클래스 이름>과 같이 사용합니다.
super@outerClassName.draw()
  • Java처럼 여러 개의 부모 클래스를 동시에 상속하는 것이 가능합니다.
open class Rectangle { }

interface Polygon { }

class Square() : Rectangle(), Polygon { }
  • 만약 동시 상속하는 부모 클래스의 멤버가 중복되는 경우 자식 클래스는 멤버를 반드시 오버라이딩하여 모호성을 제거해야합니다.