타입 파라미터
List
라는 타입이 있다면 그 안에 들어가는 원소의 타입을 안다면 도움이 될 것이다. 타입 파라미터를 사용한다면 ‘이 변수는 리스트다’ 대신 ‘이 변수는 문자열을 담는 리스트다’ 로 의미를 명확하게 할 수 있다.
코틀린에서 ‘문자열을 담는 리스트’ 를 표현하는 구문은 자바와 같은 List<String>
이다. 이런 타입 파라미터는 여러개 있을 수 있다(Map<String, Person>
).
코틀린은 타입 인자 또한 추론이 가능하다.
val authors = listOf("Dmitry", "Svetlana")
listOf
에 전달돤 값이 String
이기 때문에 컴파일러는 이 리스트가 List<String>
임을 추론할 수 있다. 다만 빈 리스트를 만들 때는 추론할 근거가 없기 때문에 직접 명시해야 한다. 변수의 타입을 직접 지정할 수도 있고 타입 인자를 지정할 수도 있다.
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
제네릭 함수와 프로퍼티
제네릭 함수를 선언하는 방법은 다음과 같다.
fun <T> List<T>.slice(indices: IntRange): List<T>
제네릭 함수를 선언할 때는 타입 파라미터를 fun
키워드 오른쪽에 선언한다. 그러면 타입 파라미터로 선언한 T
를 수신 객체와 반환 타입에 사용할 수 있다. 이 함수는 다음과 같이 호출할 수 있다.
val letters = ('a'...'z').toList()
letters.slice<Char>(0..2)
위 함수를 호출할 때는 타입 인자를 명시적으로 지정할 수 있지만 대부분은 컴파일러가 타입 인자를 추론할 수 있기 때문에 타입 인자를 생략할 수 있다.
letters.slice(10..13) //컴파일러가 T가 Char라는 사실을 추론한다.
제네릭 고차 함수
조금 더 응용해 제네릭 고차 함수를 만들 수 있다.
fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T> { /*...*/ }
val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>( /* ... */ )
readers.filter { it !in authors }
여기서 it
의 타입은 T
라는 제네릭 타입이다. 컴파일러는 filter
가 List<T>
타입의 리스트에서 호출할 수 있으며 filter
의 수신 객체인 reader
의 타입이 List<String>
임을 통해 T
가 String
임을 추론할 수 있다.
제네릭 확장 프로퍼티
제네릭 함수와 마찬가지로 제네릭 확장 프로퍼티도 선언할 수 있다. 여기서 중요한 것은 일반 프로퍼티는 타입 파라미터를 가질 수 없기 때문에 제네릭하게 만들 수 없다.
val <T> List<T>.penultimate: T //리스트의 마지막 원소 바로 앞의 원소를 반환
get() = this[size - 2]
println(listOf(1, 2, 3, 4).penultimate)
// 3
제네릭 클래스
자바와 마찬가지 타입 파라미터를 넣은 꺽쇠 괄호(<>
)를 클래스 또는 인터페이스 이름 뒤에 붙여 제네릭한 클래스 또는 인터페이스를 만들 수 있다.
interface List<T> {
operator fun get(index: Int): T //T를 일반 타입처럼 사용할 수 있다.
//...
}
제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다. 구체적인 타입을 지정할 수도 있고 타입 파라미터로 받은 타입을 넘길 수도 있다.
class StringList : List<String> {
override fun get(index: Int): String = ...
}
class ArrayList<T> : List<T> {
override fun get(index: Int): T = ...
}
클래스가 자기 자신을 타입 인자로 참조할 수도 있다.
class String : Comparable<String> {
override fun compareTo(other: String): Int = /* ... */
}
타입 파라미터 제약
클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다. 제약을 가하려면 타입 파라미터 이름 뒤에 콜론을 표시하고 그 뒤에 상한 타입을 적으면 된다. 자바의 <T extends Number>
와 같은 개념이다.
fun <T : Number> List<T>.sum(): T
타입 파라미터에 상한을 정하면 그 타입의 값을 상한 타입의 값으로 취급할수 있다. Number
를 예시로 들면
fun <T : Number> oneHalf(value: T): Double {
return value.toDouble() / 2.0 //Number클래스에 정의된 메소드를 호출할 수 있다.
}
여러 제약을 가하기
드물지만 파라미터에 둘 이상의 제약을 가해야 할 수도 있다. 이럴 땐 다음과 같이 한다.
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
if(!seq.endsWith('.')) {
seq.append('.')
}
}
타입 파라미터를 널이 될 수 없는 타입으로 한정하기
아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?
를 상한으로 정한 파라미터와 같다.
class Processor<T> {
fun process(value: T) {
value?.hashCode() //value 는 null이 될 수 있기 때문에 안전한 호출을 사용해야 한다.
}
}
널이 될 수 없는 타입만 타입 인자로 받으려면 타입 파라미터에 제약을 가해야 한다.
class Processor<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
이런 제약은 T
타입이 항상 널이 되지 않음을 보장하기 때문에 String?
같은 타입을 거부한다(String?
는 Any?
의 자손 타입이고, Any?
는 Any
보다 덜 구체적인 타입이다).