본문 바로가기

Language/Kotlin

코틀린[Kotlin] 기본 문법 2 (클래스, 제너릭, 상속)

코틀린 기본 문법 1편에 이어 이번엔 객체지향 프로그래밍의 기본인 클래스, 제너릭, 상속을 코틀린에서 어떻게 사용하는지 알아보겠습니다.

코틀린 공식 문서를 참고했습니다.

 
 

클래스 (Classes)

클래스 선언은 클래스 이름, 클래스 헤더(유형 매개변수 지정, 기본 생성자 등) 및 클래스 본문으로 구성되며 중괄호로 둘러싸여 있습니다. 헤더와 본문은 모두 선택 사항입니다. 클래스에 본문이 없으면 중괄호를 생략할 수 있습니다.

class Customer                                  // 1

class Contact(val id: Int, var email: String)   // 2

fun main() {

    val customer = Customer()                   // 3
    
    val contact = Contact(1, "mary@gmail.com")  // 4

    println(contact.id)                         // 5
    contact.email = "jane@gmail.com"            // 6
}
  1. 프로퍼티와 사용자가 정의한 생성자가 없는 Customer라는 클래스를 선언합니다. 매개변수가 없는 기본 생성자는 Kotlin에서 자동으로 생성됩니다.
  2. 두 개의 프로퍼티와 생성자가 있는 클래스를 선언합니다. (두 개의 프로퍼티: immutable id와 mutable email,  id와 email이 있는 생성자)
  3. 기본 생성자를 통해 Customer 클래스의 인스턴스를 만듭니다. Kotlin에는 new 키워드가 없습니다.
  4. 두 개의 인자가 있는 생성자를 사용하여 Contact 클래스의 인스턴스를 만듭니다.
  5. 프로퍼티 ID에 액세스합니다.
  6. 프로퍼티 이메일의 값을 업데이트합니다.

제너릭 (Generics)

제네릭 클래스와 함수는 List<T> 내부의 로직이 T와 무관한 것처럼 특정 제네릭 타입과 독립적인 공통 로직을 캡슐화하여 코드 재사용성을 높입니다.

Kotlin의 클래스는 Java와 마찬가지로 타입 매개변수를 가질 수 있습니다.

class Box<T>(t: T) {
    var value = t
}

이러한 클래스의 인스턴스를 만들려면 타입 인자를 제공하기만 하면 됩니다.

val box: Box<Int> = Box<Int>(1)

그러나 예를 들어 생성자 인자에서 매개변수를 유추할 수 있는 경우 타입을 생략할 수 있습니다.

val box = Box(1) // 1의 타입은 Int이기 때문에 컴파일러는 Box<Int>라는 것을 알아낸다.

 

제너릭 클래스

Kotlin에서 제네릭을 사용하는 첫 번째 방법은 제네릭 클래스를 만드는 것입니다.

class MutableStack<E>(vararg items: E) {              // 1

  private val elements = items.toMutableList()

  fun push(element: E) = elements.add(element)        // 2

  fun peek(): E = elements.last()                     // 3

  fun pop(): E = elements.removeAt(elements.size - 1)

  fun isEmpty() = elements.isEmpty()

  fun size() = elements.size

  override fun toString() = "MutableStack(${elements.joinToString()})"
}
  1. E가 제네릭 형식 매개변수라고 하는 제네릭 클래스 MutableStack<E>를 정의합니다. 사용할 때는 MutableStack<Int>를 선언하여 Int와 같은 특정 타입에 할당한다.
  2. 제네릭 클래스 내에서 E는 다른 타입과 마찬가지로 매개변수로 사용할 수 있습니다.
  3. E를 반환 유형으로 사용할 수도 있습니다.

구현에서는 단일 표현식으로 정의할 수 있는 함수에 대해 Kotlin의 약식 구문을 많이 사용합니다.

제너릭 함수

로직이 특정 타입과 독립적인 경우 함수를 생성할 수도 있습니다. 예를 들어, 가변 스택을 생성하는 유틸리티 함수를 작성할 수 있습니다.

fun <E> mutableStackOf(vararg elements: E) = MutableStack(*elements)

fun main() {
  val stack = mutableStackOf(0.62, 3.14, 2.7)
  println(stack)
}

컴파일러는 mutableStackOf의 매개변수에서 제네릭 유형을 유추할 수 있으므로 mutableStackOf<Double>(...) 작성할 필요가 없습니다.

상속 (Inheritance)

Kotlin의 모든 클래스에는 슈퍼타입이 선언되지 않은 클래스의 기본 슈퍼클래스인 Any라는 공통 슈퍼클래스가 있습니다.

class Example // 암시적으로 Any를 상속받는다.

Any에는 equals(), hashCode() 및 toString()의 세 가지 메서드가 있습니다. 따라서 이러한 메서드는 모든 Kotlin 클래스에 대해 정의됩니다.

기본적으로 Kotlin 클래스는 final 클래스이며 상속할 수 없습니다. 클래스를 상속 가능하게 만들려면 open 키워드를 명시하면 됩니다.

open class Base // 상속을 위해 클래스가 열려 있습니다

명시적 상위 유형을 선언하려면 클래스 헤더에서 콜론 뒤에 유형을 배치합니다.

open class Base(p: Int)

class Derived(p: Int) : Base(p)

 

파생 클래스에 기본 생성자가 있는 경우 기본 클래스는 매개 변수에 따라 해당 기본 생성자에서 초기화될 수 있고 초기화되어야 합니다.

파생 클래스에 기본 생성자가 없는 경우 각 보조 생성자는 super 키워드를 사용하여 기본 형식을 초기화해야 하며 그렇지 않으면 다른 생성자에 위임해야 합니다. 이 경우 다른 보조 생성자가 상위 클래스의 다른 생성자를 호출할 수 있습니다.

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

 

클래스 상속 예시

open class Dog {                // 1
    open fun sayHello() {       // 2
        println("wow wow!")
    }
}

class Yorkshire : Dog() {       // 3
    override fun sayHello() {   // 4
        println("wif wif!")
    }
}

fun main() {
    val dog: Dog = Yorkshire()
    dog.sayHello()
}
  1. Kotlin 클래스는 기본적으로 final입니다. 클래스 상속을 허용하려면 클래스를 open 한정자로 표시합니다.
  2. Kotlin 메서드도 기본적으로 final입니다. 클래스와 마찬가지로 open 수정자는 재정의(overrinding)을 허용합니다.
  3. 클래스는 이름 뒤에 : SuperclassName()을 지정하면 슈퍼클래스를 상속합니다. 빈 괄호()는 수퍼클래스 기본 생성자의 호출을 나타냅니다.
  4. 메서드 또는 프로퍼트를 오버라이딩하려면 override 한정자가 필요합니다.

파라미터를 가진 생성자를 사용한 상속

open class Tiger(val origin: String) {
    fun sayHello() {
        println("A tiger from $origin says: grrhhh!")
    }
}

class SiberianTiger : Tiger("Siberia")                  // 1

fun main() {
    val tiger: Tiger = SiberianTiger()
    tiger.sayHello()
}
  1. 서브클래스를 생성할 때 슈퍼클래스의 매개변수화된 생성자를 사용하려면 서브클래스 선언에 생성자 인수를 제공하십시오.

생성자 인자를 상위클래스의 인자로 전달하기

open class Lion(val name: String, val origin: String) {
    fun sayHello() {
        println("$name, the lion from $origin says: graoh!")
    }
}

class Asiatic(name: String) : Lion(name = name, origin = "India") // 1

fun main() {
    val lion: Lion = Asiatic("Rufo")                              // 2
    lion.sayHello()
}
  1. Asiatic 선언할 때 name은 var도 val도 아닙니다. 생성자 인수이며, 그 값은 상위 클래스 Lion의 name 속성에 전달됩니다.
  2. 이름이 Rufo인 Asiatic 인스턴스를 생성합니다. 이 호출은 Rufo 및 India 인수를 사용하여 Lion 생성자를 호출합니다.