KotlinLanguage

Kotlin 무작정 따라해보기 4일차 - Generics, Inheritance

4일차 시작하기 - Generics, Inheritance

지난 3일차에 이어서 Learn Kotlin by Example > IntroductionGenerics, Inheritance에 대한 학습을 시작합니다.

Generics

제네릭(Generics)은 현대적인 언어에서 표준이 된 범용적인 메커니즘입니다. 제네릭은 클래스와 함수의 특정 유형에 대하여 독립적이고 공통적인 로직을 캡슐화하여 재사용성을 높입니다. 예를 들어 List<T>에서의 T는 독립적입니다.

Generic Classes

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의 파라미터 item의 `MutableStack Class를 정의한다.

    • MutableStack<Int>를 선언하면 item인자의 타입은 Int 타입으로 정의된다.

클래스를 정의할 때 지정하는 파라미터는 클래스를 초기화할 때 프로퍼티(property)에서 바로 사용할 수 있다.
코틀린에서는 이를 Primary Constructor라고 부르는 것 같다. 흔이 알고 있는 constructor 키워드를 사용해서 정의하는 것을 Secondary Constructor라고 부른다.
자세한 내용은 https://kotlinlang.org/docs/reference/classes.html#constructors 참고

  1. 클래스를 정의할 때 지정한 제네릭 타입 E를 제네릭 클래스 내의 push() 메서드의 element의 타입으로 지정한다.

    • 제네릭 클래스 내에서 E는 다른 타입과 마찬가지로 타입으로 사용이 가능하다.
  2. 클래스를 정의할 대 지정한 제네릭 타입 E를 제네릭 클래스의 peek()메서드의 리턴 타입으로 지정한다.

    • 제네릭 클래스 내에서 E는 타른 타입과 마찬가지로 리턴 타입으로도 사용 가능하다.

코틀린에서는 단일 표현식으로 정의할 수 있는 함수를 표현할 때는 축약 구문(shorthand syntax)의 표현 방식을 많이 사용한다.
ex) fun peek(): E = elements.last() 이런 식으로 function을 정의하는 데 {}return 대신 =으로 표현하였다.
https://kotlinlang.org/docs/reference/functions.html#single-expression-functions 참고

Generic Functions

함수도 제네릭 함수로 정의할 수 있습니다. 예를 들어, 가변 스택(?)을 생성하는 유틸리티 함수를 작성하는데 사용할 수 있다.

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

fun main() {
  val stack = mutableStackOf(0.62, 3.14, 2.7)
  println(stack)
}
  • Class를 정의할 때 표현하는 방식과는 다르게 Function의 경우에는 fun 키워드와 함수 이름 사이에 <E> 형태로 표현되어 있다. 이건 조금 헷갈릴 수 있기 때문에 주의해야 한다.

제네릭(Generic) 관련 내용이 이게 전부이지 않을 것이다. 무작정 따라 하는 예제가 당장 사용 가능한 예제 위주로 작성된 것 같다. 그러다 보니 자세한 설명이나 추가적인 내용은 레퍼런스를 찾아봐야 한다. 그리고 Generic 하나만으로도 며칠은 봐야 할 것이다. 이렇게 조금 더 보충이 필요한 부분은 나중에 추가로 학습하여 글을 작성하겠다.

Inheritance

코틀린의 클래스 상속에 관한 부분이다. 예제에서는 코틀린은 기존의 객체 지향의 상속 메커니즘을 완벽하게 지원한다고 써져있으니, 아마 자바의 상속 모델을 그대로 가져오지 않았을까 생각된다.

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. 클래스를 상속 가능한 클래스로 만들기 위해 open 키워드를 클래스 앞에 추가하여 클래스를 정의한다.

    • 코틀린의 클래스는 기본으로 최종 클래스(final class)이며 최종 클래스는 상속이 불가능하다.
    • 상속이 가능하게 하려면, open 키워드를 이용해야 한다.
  2. 상속이 가능하도록 한 open class Dog의 메서드 sayHello()의 재정의가 가능하도록 fun 앞에 open 키워드를 추가한다.

    • 클래스의 메서드에서도 open키워드를 사용하지 않으면 상속받은 클래스에서 재정의가 불가능하다.

    실제로 open fun sayHello()에서 open을 제거하게 되면 'sayHello' in 'Dog' is final and cannot be overridden란 에러가 발생한다.

  3. Yorkshire 클래스에 Dog 클래스를 상속한다.

    • 상속받을 클래스의 이름 뒤에 :SuperclassName()형식으로 클래스 이름을 지정하여 클래스 상속을 표현한다. 이름 뒤에 붙어있는 빈 괄호(())는 상속한 클래스의 기본 constructor호출한다.
    • 코틀린의 클래스에는 공통의 슈퍼 클래스가 존재하고 클래스를 정의할 때 :SuperclassName()을 정의하지 않은 클래스의 경우 Any라는 코틀린 공통의 슈퍼 클래스를 상속받는다. 이 Any 클래스는 equals(), hashCode(), toString()의 메서드를 가지고 있다.
  4. override 키워드를 이용해서 상속받은 Dog클래스의 메서드 sayHello()를 재정의한다.

    • 상속받은 클래스의 메서드를 재정의할 때는 override키워드를 사용하고, override키워드를 누락하게 되면 'sayHello' hides member of supertype 'Dog' and needs 'override' modifier란 에러가 발생한다.

Inheritance with Parameterized Constructor

상속하기 위한 클래스에서 파라미터를 정의했을 경우 상속받은 클래스에서 상속한 클래스의 생성자에게 파라미터를 전달하기 위한 방법에 대한 예제이다. (조금 독특한 방식인 것 같다.)

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. SiberianTiger 클래스에 Tiger클래스를 상속하면서 첫 번째 인자로 "Siberia"를 전달한다.

    • SiberianTigerTiger클래스를 상속만 받고 별다른 정의는 하지 않았다.
    • 이전 예제에서 ()의 의미가 생성자(constructor)의 호출을 의미하는 것이라고 했는데 인자를 같이 넘기는 것으로 봤을 때, Tiger 클래스의 Primary Constructor를 호출하는 것으로 생각된다.

Passing Constructor Arguments to Superclass

상속받은 클래스에서 상속한 클래스로 인자를 전달하기 위한 예제이다. 코드를 보면 바로 이해가 되긴 하는데, 슈퍼 클래스와 클래스의 인자의 이름이 동일하면 해깔리기 쉬울 것 같다.

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. String타입의 name이란 이름의 파라미터를 전달받는 Asiatic클래스를 정의하면서 Lion 클래스를 상속하고, Lion클래스의 생성자에게 파라미터를 Asiatic클래스의 name파라미터와, "India"를 전달하도록 한다.

    • 상속받은 클래스 Asiatic에서 정의한 파라미터 name을 그대로 상속한 클래스 Lionname으로 바로 전달하는데 인자 이름(argument name)을 이용한 방식으로 전달하였다.
    • 그리고 고정된 값 "India"를 동일한 인자 이름을 이용한 방식으로 전달하였다.
  2. Asiatic 클래스의 인스턴스를 생성하면서 name의 값으로 "Rufo"를 전달한다.

    • Asiatic클래스의 생성(초기화)과 함께 name파라미터로 전달받은 Rufo을 상위 클래스인 Lion클래스의 name으로 전달하면서 Lion의 생성자를 호출한다.
    • 여기서 말한 생성자도 역시 Primary Constructor 인 것 같다.

추가로 클래스의 상속을하면 상속된 클래스와 상속받은 클래스의 초기화 순서가 궁금해져서 메뉴얼을 찾아보니 참고할 만한게 있어 첨부한다.
https://kotlinlang.org/docs/reference/classes.html#derived-class-initialization-order

4일차 마무리 및 Next

4일차에는 코틀린의 제네릭(Generic)과 클래스의 상속(Inheritance)에 대한 간단한 예제를 다뤄본 것 같다. 제네릭이란 것이 코틀린에만 존재하는 것도 아니고 다른 언어에서도 많이 사용되고 개념 또한 쉽지 않아 예제의 내용만으론 어려운 것 같다. Reference를 봐도 설명하는 내용이 많기 때문에, 이 부분은 따로 정리를 해야 할 것 같다. Class의 경우도 마찬가지로, 지금 예제로 다룬 부분은 코틀린 클래스의 상속에 대한 가장 기본적인 예제이니 클래스의 특징/특성 등에 대해 따로 정리의 글을 작성하겠다. 다음 시간에는 Control Flow(코틀린의 제어문)에 대해서 알아보겠다.