본문 바로가기

Kotlin/Basic

Kotlin - 클래스 (Class)

내가 코틀린을 배우면서 코틀린에 있어 특별하다고 생각되는 부분들 또는 메모해두어야 할 점들을 여기에 적어놓으려고 한다.

생성자

코틀린은 클래스를 생성자와 함께 선언할 수 있다. 이 경우 마치 함수와 같이 들어갈 변수들을 지정하는 방식으로 진행된다. 이 같은 방법을 통해 생성자와 속성을 동시에 선언하는 방법이다.

 

예시

 

fun main() {
    val beom = Person(24, "Beom Seok")
    
    beom.sayName();
}

class Person (val age: Int, val name: String) {
    fun sayName () {
        println("My name is ${name}")
    }
}

 

출력

 

My name is Beom Seok

 

그러나 위와 같은 경우 생성자의 기능이 제한적이다. 단순히 속성을 선언하는 것과 더불어 어떠한 코드를 실행시키고 싶을 때에는 init 함수를 사용할 수 있다. init 함수는 파라미터나 반환형이 없는 특수한 함수로, 생성자를 통해 인스턴스가 만들어질 때 호출되는 함수다.

 

예시

 

fun main() {
    val beom = Person(24, "Beom Seok")
    
    beom.sayName();
}

class Person (val age: Int, val name: String) {
    init {
        println("${age} ${name} user created.")
    }
    fun sayName () {
        println("My name is ${name}")
    }
}

 

출력

 

24 Beom Seok user created.
My name is Beom Seok

 

보조 생성자는 constructor라는 키워드를 사용한다. 이 경우 필요에 따라 추가적인 구문을 수행하도록 하게 할 수 있다. 또한 기본값을 여기서 지정해줄 수도 있다.

 

여러 클래스들

기본적인 클래스 외에도 데이터 클래스 등등 여러 클래스들이 존재한다.

 

데이터 클래스

데이터 클래스 (data class) 는 class 앞에 data 키워드를 사용하여 선언한다 다른 점은 중괄호로 열고 닫는 것이 아닌 소괄호로 열고 닫는다는 것이며, 내부에서는 변수 또는 상수만 선언할 수 있다는 것이다. 또한, 프로퍼티를 초기화해줄 필요가 없으며, 생성과 동시에 클래스 내의 프로퍼티를 기준으로 생성자가 만들어지게 된다. 또한, 객체화하고 나서 클래스와 같이 프로퍼티의 값을 변경하는 것 또한 가능하다 (var에 한해).

 

예시

 

fun main() {
    val beom = Person(24, "Beom Seok")
    
    println("${beom.name}: ${beom.age}")
}

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

 

출력

 

Beom Seok: 24

 

데이터 클래스는 기본적으로 toString(), equals(), 그리고 객체를 복사하여 새로운 객체를 반환하는 copy() 가 구현이 되어있다. 또한, 각각의 컴포넌트를 component1(), component2(), ... 등의 함수로 불러올 수 있음과 더불어, 반복문에서도 다음과 같이 각각의 컴포넌트를 불러 유용하게 사용할 수 있다.

 

예시

 

fun main() {
    val list = listOf(Data("beom", 1),
                      Data("seok", 2),
                      Data("kang", 3),)
    for ((n, i) in list) {
        println("${n}: ${i}")
    }
}

data class Data(var name: String, val id: Int)

 

출력

 

beom: 1
seok: 2
kang: 3

 

오브젝트

오브젝트는 정확히 말하면 클래스는 아니다. 오브젝트는 그 자체로 객체이다. 따라서 오브젝트는 생성자를 갖지 못한다. 또한, 어느 클래스, 함수에서든 별도의 객체화 과정 없이 접근이 가능하며, 프로그램이 실행되는 동안 저장된 데이터는 손실되지 않는다. 다만 프로그램이 종료되면 소멸된다.

 

예시

 

fun main() {
    BeomSeok.age = 20;
    BeomSeok.introduce();
}

object BeomSeok {
    var name: String = "Beom Seok"
    var age: Int = 0
    fun introduce() {
        println("I am ${name}, ${age} years old.")
    }
}

 

출력

 

I am Beom Seok, 20 years old.

 

클래스 내에서도 object를 설정해줄 수 있다. 바로 companion object라는 것인데, 이는 다른 인스턴스들끼리 하나의 object를 공유할 수 있도록 해준다.

 

예시

 

fun main() {
    var chicken = FoodPoll("Chicken")
    var pizza = FoodPoll("Pizza")
    
    chicken.vote()
    chicken.vote()
    
    pizza.vote()
    pizza.vote()
    pizza.vote()
    
    println("${chicken.name}: ${chicken.count}");
    println("${pizza.name}: ${pizza.count}");
    println("Total: ${FoodPoll.total}");
}

class FoodPoll(val name: String) {
    var count = 0;
    companion object {
        var total = 0;
    }
    fun vote() {
        count++
        total++
    }
}

 

출력

 

Chicken: 2
Pizza: 3
Total: 5

 

이 경우에 공유되는 companion object인 total은 클래스 자체적인 속성으로써 가져와야 한다. 인스턴스에서 불러올 수 없다.

 

열거형 클래스 (enum)

열거형 클래스 또한 자주 사용이 된다. 나열한 이름에 따라 클래스 이름.name으로 이름 값을 가져올 수 있고, 순서에 따라 클래스 이름.ordinal로 해당 값이 몇 번째에 적혀있는지 순서 값을 가져올 수 있다. 이늄 클래스 안의 객체들은 관행적으로 상수를 나타낼 때 사용하는 대문자로 기술한다.

 

예시

 

fun main() {
    var state = State.SLEEP
    
    println("isSleeping: ${state.isSleeping()}")
    println("Ordinal: ${state.ordinal}")
    println("Name: ${state.name}")
    println("Message: ${state.message}")
}

enum class State(val message: String) {
    SING("노래를 부릅니다."),
    EAT("밥을 먹습니다."),
    SLEEP("잠을 잡니다.");
    
    fun isSleeping() = this == State.SLEEP
}

 

출력

 

isSleeping: true
Ordinal: 2
Name: SLEEP
Message: 잠을 잡니다.

 

중첩클래스와 내부클래스

중첩클래스 (Nested Class) 는 하나의 클래스가 다른 클래스의 기능과 매우 밀접하게 연관이 되어있다는 형태의 표시로 다음과 같이 선언한다.

 

예시

 

fun main() {
    var a = A.B()
}

class A {
    class B {}
}

 

내부클래스 (Inner Class) 는 중첩클래스와 비슷한 용도로 사용할 수 있는데, class 앞에 inner 키워드를 붙여 선언하지만, 중첩클래스와 달리 외부 클래스의 객체가 있어야만 생성이 가능하다는 특징이 있다.

 

예시

 

fun main() {
    var a = A().B()
}

class A {
    inner class B {}
}

 

중첩클래스는 형태만 하나의 클래스가 다른 클래스 내부에 들어있을 뿐, 사실 상 두 개의 클래스가 서로 다른 클래스이며 서로 정보를 공유할 수 없는 반면, 내부클래스는 외부클래스 객체 안에서 사용되는 클래스이므로, 외부 클래스의 속성과 함수를 사용할 수 있다.

 

상속, 오버라이딩과 추상화

상속과 오버라이딩

먼저, 오버라이딩 또는 클래스의 상속을 위해서 필요한 키워드인 open에 대해서 알아둘 필요가 있다. open 키워드는 해당 클래스를 다른 곳에서 상속할 수 있도록 해주는 키워드로써, open 키워드가 없을 경우에는 다른 곳에서 상속 받지 못하는 final class로 정의된다.

 

예시

 

fun main() {
    var std1 = FirstGrader(16, "beom seok");
    std1.introduce()
}

open class Student(var age: Int, val name: String, var grade: Int) {
    open fun introduce() {
        println("${age}, ${name}, ${grade}")
    }
}

class FirstGrader(age: Int, name: String): Student(age, name, 1) {
    override fun introduce() {
        println("First Grader: ${age}, ${name}")
    }
}

 

출력

 

First Grader: 16, beom seok

 

또는 아래와 같이 확장하는 방법도 있다.

 

예시

 

fun main() {
    var std1 = Student(16, "beom seok", 1);
    std1.introduce()
}

open class Person(var age: Int, val name: String) {
    open fun introduce() {
        println("${age}, ${name}")
    }
}

class Student(age: Int, name: String, var grade: Int): Person(age, name) {
    override fun introduce() {
        println("${grade}: ${age}, ${name}")
    }
}

 

출력

 

1: 16, beom seok

 

추상화

추상화를 위해서는 abstract 키워드를 클래스 앞에 넣어 사용한다. abstract로 선언된 클래스는 변수에 부여할 수 없다. abstract로 추상 함수를 구현할 수도 있는데 아래와 같이 사용하면 된다.

 

예시

 

fun main() {
    var doge = Dog("Doge");
    doge.introduce();
    doge.makeSound();
}

abstract class Animal(val name: String, val type: String) {
    fun introduce() {
        println("I am ${name} and I am a ${type}!")
    }
    abstract fun makeSound ()
}

class Dog(name: String): Animal(name, "dog") {
    override fun makeSound() {
        println("bark bark.")
    }
}

 

출력

 

I am Doge and I am a dog!
bark bark.

 

abstract로 구현된 추상함수도 override를 해주어야 한다. 추상화를 하는 또다른 방법이 있는데, 바로 interface라는 기능이다. 다른 언어에서의 interface는 추상화함수로만 이루어져 있는 순수 추상화 기능을 이르는 것이지만, 코틀린에서는 추상함수 이외에도 일반함수를 가질 수 있다. 그러나 인터페이스는 생성자를 가질 수 없다는 점이 다른 점이다.

인터페이스에서 구현부가 있는 함수는 open 함수로 간주하며, 구현부가 없는 함수는 abstract 함수로 간주한다. 따라서 별도의 키워드가 없더라도 서브클래스에서 구현 및 재정의가 가능하다. 또한, 한번에 여러 interface를 상속받을 수 있으므로 좀 더 유연한 설계가 가능하다.

 

예시

 

fun main() {
    var myPikachu = Pikachu();
    myPikachu.makeSound();
    myPikachu.emitElectricity();
}

interface Mouse {
    fun makeSound()
}

interface ElectricType {
    fun emitElectricity() {
        println("Million Volts")
    }
}

class Pikachu : Mouse, ElectricType {
    override fun makeSound() {
        println("Pika Pika")
    }
}

 

출력

 

Pika Pika
Million Volts

 

위 예시에서 emitElectricity 함수는 구현부가 있더라도 open 형태의 함수이기 때문에 override를 통해 재구현이 가능하다.

 

주의해야할 점은 만약 두 인터페이스에서 같은 이름과 형태를 가진 함수를 구현하고 있다면 서브클래스에서는 혼선이 일어나지 않도록 반드시 오버라이딩하여 재구성해주어야 한다.

 

코틀린에서의 this

다른 프로그래밍 언어에서의 this는 객체 자신을 가리킬 때 쓰는 키워드이다. 위에서 클래스를 선언하고 객체 내부의 요소를 가리킬 때 this.property 형식으로 부르는 것이 아닌 그냥 property 형식으로 불렀는데, 이것은 만약 this에 한정자가 없다면 가장 안쪽의 범위를 참조하기 때문이다. 그러나 이 경우에도 반복문에서 하듯 레이블을 사용하여 this로 가리키려는 객체가 어떤 객체를 의미할지 지정할 수 있는데, 아래 예시를 보자.

 

예시

 

fun main() {
    Outer(5).Inner().c()
}

class Outer (var a: Int) {
    inner class Inner {
        var b = this@Outer.a + 1
        
        fun c() {
            println("Outer a: ${this@Outer.a}")
            println("Inner b: ${this.b}")
        }
    }
}

 

출력

 

Outer a: 5
Inner b: 6

 

출처

https://www.youtube.com/playlist?list=PLQdnHjXZyYadiw5aV3p6DwUdXV2bZuhlN 

https://medium.com/@sket8993/kotlin-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B4%88%EA%B0%84%EB%8B%A8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-38416cd8d63d