티스토리 뷰

kotlin

[kotlin/코틀린] 클래스 계층

hrniin 2023. 7. 19. 23:21
728x90

 

클래스 계층

  1. 상속
  • 주생성자 뒤에 :를 넣고 상위 클래스 이름을 넣는다.
  • 상위 클래스를 명시하지 않으면 Any를 상속하는 것으로 간주한다.
  • open 키워드는 상속에 열려있다는 의미다.

 

  • 데이터 클래스: oepn x. 다른 클래스 상속 o
  • 인라인 클래스: open x. 다른 클래스 상속 x
  • 객체: open x. 다른 클래스 상속 o

 

 

  • 클래스 멤버 vs 확장
open class Parent {
    open fun print() { println("parent") }
}

fun Parent.ext() { println("parent extension") }

class Child : Parent() {
    override fun print() { println("child") }
}

fun Child.ext() { println("child extension") }

fun main() {
    val c: Parent = Child()
    c.print()   // child
    c.ext()     // parent
}
    • 클래스 멤버: 런타임에 인스턴스의 구체적 타입에 따라 어떤 구현이 호출될지 결정된다.
    • 확장: 항상 정적으로 호출할 대상의 타입이 결정된다.
    • 정적 타입 → Parent, 동적 타입 → Child

 

open class Parent {
    open val i: Int get() = 0
    open fun test(): Int? = i
}

class Child(override var i: Int) : Parent() {
    override fun test(): Int = i
}
  • 상위 클래스의 메서드나 프로퍼티를 오버라이드할 때는 앞에 override를 붙인다.
  • 오버라이드하는 함수의 시그니처가 상위 클래스의 시그니처와 같아야 한다.
    • 반환 타입을 하위 타입으로 바꿀 수 있다.
  • 주생성자 파라미터로도 프로퍼티를 오버라이드할 수 있다.
  • 불변 프로퍼티를 가변 프로퍼티로 오버라이드할 수 있다.

 

  • 하위 클래스 생성자
open class Person {
    val name: String
    val age: Int
     
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

class Student(name: String, age: Int) : Person(name, age)
    • 상위 클래스 이름 뒤에 괄호는 생성자 호출을 의미한다.
    • 자신의 파라미터를 이용해 상위 클래스의 생성자에 전달할 수 있다.

 

open class Person(val name: String, val age: Int)

class Student : Person {
    constructor(name: String, age: Int) : super(name, age)
}
  • 하위 클래스에서 부생성자를 사용하는 경우, 생성자 시그니처 바로 뒤에 위임 호출을 위치시킨다.
  • 상위 클래스의 이름 뒤에 괄호를 붙이지 않는다. (부생성자에서 위임 호출을 해야하기 때문)
  • 하위 클래스에 주생성자가 있는 경우, 무조건 주생성자에서 위임 호출해야 한다.
  • 상위 클래스에 여러 생성자가 존재하는 경우, 하위 클래스에서는 주생성자를 아예 정의하지 않고 부생성자를 사용해야 한다.

 

  • 생성자 호출 순서
    • 상위 클래스 초기화 → 하위 클래스 초기화
    • 상위 클래스에서 현재 인스턴스를 코드에 누출하면, 현재 인스턴스는 초기화되지 않은 상태이므로 올바르지 않은 값이 사용된다. (this leak)
      • non-nullable 타입이 null이 될 수 있다.

 

 

 

       2. 타입 검사와 캐스팅

fun main() {
    val obj = arrayOf("1", 2, "3", 4) // Any
     
    var sum = 0
     
    for(o in obj) {
        if (o is Int) { // Int로 스마트 캐스트
            sum += o
        }
    }
     
    println(sum)
}
  • is/!is로 타입 검사를 할 수 있다.
  • 타입 검사 후 변수가 변경되지 않으면 스마트 캐스트가 동작한다.
    • 프로퍼티, 커스텀 접근자가 정의된 변수 x
    • 위임을 사용하는 프로퍼티/지역 변수, open 멤버 프로퍼티 x
    • 검사 후 값을 변경하는 가변 지역 변수 x

 

  • as 연산자를 통해 값의 타입을 강제로 변환할 수 있다.
  • 실제 타입과 변환하려는 타입이 일치하지 않은 경우
    • as: 예외
    • as?: null

 

 

 

       3. 추상 클래스

  • 생성자를 가질 수 있다. 인스턴스를 호출할 수 없으므로 하위 클래스의 생성자에서 위임 호출로만 호출될 수 있다.
  • 추상 프로퍼티는 명시적 접근자, by, 초기화가 불가능하다.
  • 암시적으로 상속에 열려있으므로 open 키워드를 지정하지 않아도 된다.

 

 

 

       4. 인터페이스

interface A {
    fun print()
}

class B : A {
    override fun print() {
        println("test")
    }
}

fun main() {
    val b = B()
    b.print()
}
  • 생성자가 없기 때문에 하위 클래스/인터페이스에서 상위 인터페이스의 이름 뒤에 괄호를 붙이지 않는다.
  • 클래스와 동일하게 :을 사용하여 상속한다.
  • 멤버 함수와 프로퍼티를 구현할 수 있다. 하위 클래스/인터페이스에서 override하지 않아도 된다.
  • backing field를 사용할 수 없다.

 

interface Car {
    fun move() { println("car") }
}

interface Ship {
    fun move()
}

class Amphibia : Car, Ship {
    override fun move() { super.move() } // Car의 메서드 호출
}

fun main() {
    Amphibia().move()
}
  • 자바와 동일하게 여러 인터페이스를 상속받을 수 있다.
  • 동일한 시그니처의 멤버를 가지는 인터페이스를 둘 이상 상속할 경우, 한 멤버로 합쳐진 멤버를 상속하는 것과 같다.
  • 상위 멤버에서 구현이 존재하더라도, 반드시 하위 클래스에서 구현해야 한다.

 

interface Car {
    fun move() { println("car") }
}

interface Ship {
    fun move() { println("ship") }
}

class Amphibia : Car, Ship {
    override fun move() {
        super<Car>.move()
        super<Ship>.move()
    }
}

fun main() {
    Amphibia().move()
}
  • 멤버에 대한 구현이 둘 이상 존재하는 경우, super 호출을 한정된 키워드로 사용해야 한다.

 

 

 

       5. sealed class

sealed class Result {
    class Success(val value: Any) : Result() { /**/ }
    class Error(val message: String) : Result() { /**/ }
}

fun message(result: Result) = when(result) {
    is Result.Success -> "Success"
    is Result.Error -> "Error"
}
  • 이넘 클래스와 유사하게 미리 만들어 놓은 자료형을 묶어서 제공하는 클래스
  • 같은 패키지 내에서만 sealed class를 상속할 수 있다.
  • 직접 인스턴스를 만들 수 없는 추상 클래스에 속한다. (추상 멤버를 가질 수 있음)
  • 생성자의 접근 제한자는 디폴트로 protected이며, protected/private만 가능하다.
  • 빠진 자료형 없이 when 식을 작성하면 else 를 생략해도 된다.

 

 

 

       6. 위임

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}
 
  • 상위 인터페이스 이름 뒤에 by 키워드를 붙이고, 위임할 인스턴스를 적는다.
  • 위임할 인스턴스에 있는 메서드를 통해 인터페이스의 멤버가 구현된다.

 

  • 컴파일러는 위임된 값을 저장하는 필드를 자동으로 만들어준다.
  • 클래스 본문에 정의된 프로퍼티를 클래스 위임에 사용할 수는 없다,
  • 인터페이스의 멤버를 구현하는 경우만 위임을 쓸 수 있다,

 

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함