본문 바로가기

Kotlin

[Kotlin] Kotlin 기본 문법 by "Code with Joyce" 1편 - (2)

1. 클래스(Class)

클래스의 기본형은 다음과 같다.

 

// class 클래스명 constructor(변수명 : 자료형)

class Human constructor(name: String) {
    val name = name
    
    fun eatingCake() {
        println("This is so YUMMYYYY")
    }
}

fun main() {

    // 인스턴스를 생성할 때 new 연산자를 사용하지 않음
    val human = Human()
    
    human.eatingCake()
}

 

클래스명 옆에 오는 생성자를 기본 생성자(primary constructor)라고 하며, 기본 생성자에 주석 혹은  visibility modifiers(public, private 등의 접근 제한자)가 없을 경우 constructor 연산자를 생략할 수 있다.

 

class Human(name: String) { /*...*/ }

// visbility modifier를 지정한 경우
class Human private constructor(name: String) { /*...*/ }

 

기본 생성자 내부에는 선언문을 제외한 어떤 코드도 올 수 없지만 init 키워드를 사용하는 초기화 블록(initializer blocks)을 활용하면 된다. 심지어 초기화 블록은 2개 이상 선언될 수 있고 인스턴스가 생성되는 동안에 클래스 속성과 초기화 블록이 작성된 순서대로 실행된다.

 

class Human(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints $name")
    }
    
    // 위의 init이 먼저 실행되고 secondProperty 초기화
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

fun main() {
    val mimi = Human("mimi")
}

/* 
result

    First property: mimi
    First initializer block that prints mimi
    Second property: 4
    Second initializer block that prints 4
*/

 

다음과 같은 방법으로도 클래스 속성을 초기화할 수 있다.

 

class Human(name: String = "Anonymous") {
    val name = name
    
    fun getName() {
        println("My name is $name")
    }
}

fun main() {
    val human = Human("mimi")
    
    human.getName()
    
    val stranger = Human()
    
    stranger,getName()
}

/*
result

    My name is mimi
    My name is Anonymous
*/

 

보조 생성자(Secondary constructor)의 개념이 있는데 자바의 사용자 정의 생성자와 비슷해 보인다.

 

class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {
    var strname: String? = null
    constructor(owner: Person, name: String) {
        strname = name
        owner.pets.add(this) // adds this pet to the list of its owner's pets
    }
}

fun main() {
    val person = Person()
    val pet1 = Pet(person, "jido")
    val pet2 = Pet(person, "retriever")
    
    // 클래스 속성은 기본적으로 public
    println("${person.pets[0].strname}, ${person.pets[1].strname}")
}

/*
result
    
    jido, retriver
*/

 

그리고 기본 생성자와 보조 생성자를 함께 사용하면 오버로딩(Overloading)이 가능하다. 이때 this 키워드를 사용해서 보조 생성자가 기본 생성자를 delegate한다. (위임? 계승? 상속? 적절한 단어가 생각나지 않는다.)

 

Delegation은 보조 생성자의 첫 번째 명령문으로 실행되므로 모든 초기화 블록 및 속성 초기화 코드는 보조 생성자의 본문보다 먼저 실행된다.

 

class Person(val name: String) {
    val children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

 

클래스에 기본 생성자가 없더라도 delegation은 암시적으로 발생하고 초기화 블록은 계속 실행된다.

 

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor $i")
    }
}

fun main() {
    val con1 = Constructors(1)
    val con2 = Constructors(2)
}

/*
result
    
    Init block
    Constructor 1
    Init block
    Constructor 2
*/

 

만약 추상 클래스가 아닌데 생성자가 아무것도 없다면, 인수가 없는 기본 생성자가 자동으로 생성된다. 이렇게 만들어진 기본 생성자는 public 접근 제한자를 가진다.

 

Java에서 상속은 extends 키워드를 사용했는데 kotlin에서는 콜론(:) 키워드를 사용한다. 그리고 kotlin에서 클래스와 메서드는 기본적으로 final이기 때문에 상속하고자 하는 클래스와 오버라이딩(Overriding)하려는 메서드에 대해 open 키워드(inheritance modifier)를 사용해야 한다.

 

// 상속을 위한 open
open class Human constructor(name: String = "Anonymous"){
    val name = name

    init {
        println("New human has been born!")
    }

    // 오버라이딩을 위해 open
    open fun singASong() {
        println("lalala")
    }
}

// Human 클래스 상속
class Korean : Human() {
    override fun singASong() {
        
        // 부모의 method 호출 가능
        super.singASong()
        println("라라라")
        
        // 부모의 property 접근 가능
        println("my name is ${name}")
    }
}

fun main() {
    val korean = Korean("mimi")
    korean.singASong()
}

/*
result

    New human has been born!
    lalala
    라라라
    my name is mimi
*/

 

이 부분에서 조금 특이한 점이 클래스와 함수(function & method)는 public final인데 (참고로 함수의 형식인자도 final) 반해, 클래스의 속성은 public이 default이라는 것이다. (Visibility Modifier 참고)

 

그래서 이런 일도 생길 수 있다.

 

class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {

    // var는 mutable한 변수
    var strname: String? = null
    constructor(owner: Person, name: String) {
        strname = name
        owner.pets.add(this) // adds this pet to the list of its owner's pets
    }
}

fun main() {
    val person = Person()
    val pet1 = Pet(person, "jido")
    
    // pet1의 name 수정
    person.pets[0].strname = "mimi"
    
    println("${person.pets[0].strname}")
}

/*
result

    mimi
*/

 

kotlin은 따로 명시하지 않으면 접근 제한자가 public이고(Kotlin Visibility Modifiers) 클래스 속성이 수정 가능한 var로 선언되어서 위와 같은 일이 발생한다. 클래스 외부에서 멋대로 속성값을 수정하게 되면 예기치 못한 에러가 발생할 수 있다고 하니 적절한 접근 제한자와 val를 사용하도록 하고 getter와 setter를 이용하자.

 

참고로 코틀린 블로그에 작성된 글을 보면 "mutable한 매개변수는 혼란을 야기시키므로 매개변수를 immutable한 val로 고정했다."라고 한다. 클래스가 final인 것도 비슷한 맥락인 것 같다.

 

마지막으로 추상 클래스이다. 추상 클래스는 일부 또는 전체 멤버와 함께 선언될 수 있다. 추상 멤버는 해당 클래스에 구현이 없고 따라서 오버라이딩 할 때 open을 사용할 필요가 없다.

 

abstract class Polygon {
    abstract fun draw()
}

class Rectangle : Polygon() {
    override fun draw() {
        // draw the rectangle
    }
}

 

 

그리고 open된 일반 클래스 및 메서드를 추상 멤버로 오버라이딩할 수 있다.

 

open class Polygon {
    open fun draw() {
        ...
    }
}

abstract class WildShape : Polygon() {

    // WildShape를 상속하는 클래스들은 draw를 오버라이딩할 필요가 있다.
    // 이때 오버라이딩 대상은 Polygon.draw()가 아니다.
    abstract override fun draw()
}