Lewis's Tech Keep

[Kotlin] 코틀린 적응기 - 클래스, 상속 본문

Kotlin/Kotlin 적응기

[Kotlin] 코틀린 적응기 - 클래스, 상속

Lewis Seo 2024. 10. 1. 21:17

시작

코틀린에서 OOP를 잘 사용하기 위해서는 이번에 배운 것들을 잘 기억해야 할 것 같다.

자주 까먹을테니 치트시트처럼 써보자.

 

배우고 있는 것

https://www.inflearn.com/course/java-to-kotlin

 

자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의 | 최태현 - 인프런

최태현 | 이 강의를 통해 Kotlin 언어의 특성과 배경, 문법과 동작 원리, 사용 용례, Java와 Kotlin을 함께 사용할 때에 주의할 점 등을 배울 수 있습니다., 요즘 대세인 코틀린을 공부하고 싶다면?⭐ J

www.inflearn.com

 

 

1. 클래스

클래스 생성 시 var, val을 해주면 바로 필드를 설정 해 줄 수 있다. 

class Person(
    val name: String,
    var age: Int
) {

}

 

init (validation)

 

생성 시점에 무언가 해주고 싶다면 init 을 설정하고 하고 싶은 expression을 넣어준다.

class Person(
    name: String,
    var age: Int
) {
    // construct validation
    init {
        if (age < 0) {
            throw IllegalArgumentException("${age} 가 이상해요")
        }
    }

}

 

생성자 chaining

 

생성자 chaining 을 통한 생성도 가능하다.

class Person(
    name: String,
    var age: Int
) {
    // default parameter 를 권장하지만 converting 과 같은 경우에 필요할 수도 있음
    // secondary constructor with different options
    // this 는 위의 person 을 호출한다는 뜻
    // chaining 가능
    constructor(name: String): this(name, 1)

    constructor(): this("홍길동")

}

 

getter setter

 

getter setter 는 아래와 같이 다양한 방법으로 표현할 수 있다.

함수처럼 보이게 할 수도 있고 (자바의 메서드 표현과 비슷)

프로퍼티처럼 보이게 할 수도 있다. (함수 자체를 하나의 프로퍼티로 보는 관점으로)

custom getter와 custom setter를 만들수도 있고 이를 backing field로 활용할 수 있다.

backing field는 아래와 같이 get(), set(value)로 설정이 가능하지만

실제로 호출할 때 person.name.get() 과 같이 호출하는게 아니라 person.name만 호출해도 되는 것이다.

class Person(
    name: String,
    var age: Int
) {

    // 함수로 이용
    // getter
    fun isAdult(): Boolean {
        return this.age >= 20
    }

    // 프로퍼티처럼 보이게 표현
    // custom getter
    val isAdult: Boolean
        get() = this.age >= 20 
        get() {
            return this.age >= 20
        }

    // backing field
    // custom getter
    // field.uppercase가 아니라 name.uppercase를 하면
    // name이 getter를 부르고 getter 내부에서 다시 name을 부르기 때문에 무한 루프
    val name: String = name
        get() = field.uppercase()

    // 캡슐화 때문에 자주 쓰지는 않음
    // backing field
    // custom setter
    var name = name
        set(value) {
            field = value.uppercase()
        }

}

fun main () {
    val person = Person("hi", 100)
    println(person.name)
    person.age = 10 // setter
    println(person.age)

    val jPerson = JavaPerson("hi", 100)
    jPerson.age = 10 // setter
}

 

 

2. 상속

추상클래스

 

추상 클래스 생성 방법은 특별히 다르진 않지만

추상 클래스를 상속해서 쓰는 자식 클래스에서 필드나 함수를 override한다면 open 키워드를 명시해줘야 한다.

// 자식 클래스에서 legCount 값을 override 한다면 open 키워드가 필요
abstract class Animal (
    protected val species: String,
    protected open val legCount: Int
) {

    abstract fun move()
}

 

자식 클래스 예제

생성 시 : 키워드를 통해 생성한다.

자바에서는 상속할 때 override 키워드를 적어주지 않아도 되지만 코틀린에서는 반드시 적어주어야 한다.

// 상속 받을 때는 한칸 띄우고 :
// 상위 클래스 생성자를 바로 호출
class Cat(
    species: String
) : Animal(species, 4) {

    override fun move() {
        println("고양이 이동")
    }

}

 

인터페이스

 

자바의 interface에서는 변수를 넣지 않고 해당 인터페이스에 필요한 명세(메서드, 행동)들만 넣지만,

코틀린에서는 interface에도 변수처럼 사용될 것을 넣을 수가 있다.
(그렇다고해서 변수를 넣는게 좋을 지는 아직 고민이 되기는 한다.)

내부 구현을 넣지 않고 명세만 넣으면 구현 클래스에서 반드시 구현하도록 제한되는 점은 같다.

interface Swimable {
    val swimAbility: Int
        // default value
        get() = 3

    fun act() {
        println("첨벙")
    }
}

 

interface 구현 시에 특별히 다른 키워드는 없고 : 를 사용하여 똑같이 적용한다.

또한 override 하고 super에 있는 것을 쓸 때 super<Type>.함수() 로 쓰는 문법이 조금 다르다.

// interface도 같이 : 를 사용
class Penguin (
    species: String,
    private val wingCount: Int = 2
) : Animal(species, 2), Flyable, Swimable {

    override fun move() {
        println("펭귄 가는중")
    }

    override val legCount: Int
        get() {
            return super.legCount + this.wingCount
        }

    // override 시 문법이 좀 다름
    override fun act() {
        super<Flyable>.act()
        super<Swimable>.act()
    }

    // backing field없는 interface를 구현할 수 있음
    override val swimAbility: Int
        get() = 3
}

 

궁금해서 찾아본 다중 상속

역시 컴파일하면 자바로 바뀌는 구조도 있고 해서 그런지 다중 상속이 불가능하다. 

 

상속 시 주의할 점

 

아래와 같은 예제에서 Derived를 생성하면 number는 100이 아니라 0이다.

그 이유는 Derived를 만들 때 default value 설정 전에 init 에서 number는 100이 할당된 시점이 아니기 때문

open class Base (
    open val number: Int = 100
){
    init {
        println("Base")
        // 해당 값은 하위 클래스의 값이 초기화 되어 있지 않기 때문에 0이 나옴
        // final 이 아닌 필드에 접근하면 안됨
        println(number)
        // 상위 클래스 설계 시 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 함
    }
}

class Derived(
    override val number: Int
) : Base(number) {
    init {
        println("Derived")
    }
}

 

Comments