4일차 시작하기 - Generics, Inheritance
지난 3일차에 이어서 Learn Kotlin by Example > Introduction
의 Generics, 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()})"
}
제네릭 타입
E
의 파라미터item
의 `MutableStack Class를 정의한다.MutableStack<Int>
를 선언하면item
인자의 타입은Int
타입으로 정의된다.
클래스를 정의할 때 지정하는 파라미터는 클래스를 초기화할 때 프로퍼티(property)에서 바로 사용할 수 있다.
코틀린에서는 이를Primary Constructor
라고 부르는 것 같다. 흔이 알고 있는constructor
키워드를 사용해서 정의하는 것을Secondary Constructor
라고 부른다.
자세한 내용은 https://kotlinlang.org/docs/reference/classes.html#constructors 참고
클래스를 정의할 때 지정한 제네릭 타입
E
를 제네릭 클래스 내의push()
메서드의element
의 타입으로 지정한다.- 제네릭 클래스 내에서
E
는 다른 타입과 마찬가지로 타입으로 사용이 가능하다.
- 제네릭 클래스 내에서
클래스를 정의할 대 지정한 제네릭 타입
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()
}
클래스를 상속 가능한 클래스로 만들기 위해
open
키워드를 클래스 앞에 추가하여 클래스를 정의한다.- 코틀린의 클래스는 기본으로 최종 클래스(final class)이며 최종 클래스는 상속이 불가능하다.
- 상속이 가능하게 하려면,
open
키워드를 이용해야 한다.
상속이 가능하도록 한
open class Dog
의 메서드sayHello()
의 재정의가 가능하도록fun
앞에open
키워드를 추가한다.- 클래스의 메서드에서도
open
키워드를 사용하지 않으면 상속받은 클래스에서 재정의가 불가능하다.
실제로
open fun sayHello()
에서open
을 제거하게 되면'sayHello' in 'Dog' is final and cannot be overridden
란 에러가 발생한다.- 클래스의 메서드에서도
Yorkshire
클래스에Dog
클래스를 상속한다.- 상속받을 클래스의 이름 뒤에
:SuperclassName()
형식으로 클래스 이름을 지정하여 클래스 상속을 표현한다. 이름 뒤에 붙어있는 빈 괄호(()
)는 상속한 클래스의 기본constructor
호출한다. - 코틀린의 클래스에는 공통의 슈퍼 클래스가 존재하고 클래스를 정의할 때
:SuperclassName()
을 정의하지 않은 클래스의 경우Any
라는 코틀린 공통의 슈퍼 클래스를 상속받는다. 이Any
클래스는equals()
,hashCode()
,toString()
의 메서드를 가지고 있다.
- 상속받을 클래스의 이름 뒤에
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()
}
SiberianTiger
클래스에Tiger
클래스를 상속하면서 첫 번째 인자로"Siberia"
를 전달한다.SiberianTiger
는Tiger
클래스를 상속만 받고 별다른 정의는 하지 않았다.- 이전 예제에서
()
의 의미가 생성자(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()
}
String
타입의name
이란 이름의 파라미터를 전달받는Asiatic
클래스를 정의하면서Lion
클래스를 상속하고,Lion
클래스의 생성자에게 파라미터를Asiatic
클래스의name
파라미터와,"India"
를 전달하도록 한다.- 상속받은 클래스
Asiatic
에서 정의한 파라미터name
을 그대로 상속한 클래스Lion
의name
으로 바로 전달하는데 인자 이름(argument name
)을 이용한 방식으로 전달하였다. - 그리고 고정된 값
"India"
를 동일한 인자 이름을 이용한 방식으로 전달하였다.
- 상속받은 클래스
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
(코틀린의 제어문)에 대해서 알아보겠다.
- Kotlin 무작정 따라해보기 관련 Post
- 1일차 - Get Started
- 2일차 - Introduction > Hello World, Functions
- 3일차 - Introduction > Variables, Null Safety, Classes
- 4일차 - Introduction > Generics, Inheritance
- 5일차 - Control Flow
Comments