제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있다. 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야 한다.
코틀린 컴파일러는 보통 타입과 마찬가지로, 타입 인자 추론이 가능하며, 빈 리스트를 만들어야 할 경우, 타입 인자를 타입 인자를 추론할 근거가 없기 때문에 직접 타입 인자를 명시해야 한다.
fun main(args: Array<String>) {
val authors = listOf("Dmitry", "Svetlana") //타입인자추론
//val readers: MutableList<String> = mutableListOf() //변수 타입을 지정
val readers = mutableListOf<String>()//변수를 만드는 함수의 인자를 지정
}
제네릭 함수와 프로퍼티
제네릭 함수를 호출할 때는 반드시 구체적인 타입으로 타입 인자를 넘겨야 한다. 컬렉션을 다루는 라이브러리 함수의 대부분은 제네릭 함수이다.
List의 slice 메서드
public fun <T> List<T>.slice(indices: IntRange): List<T> {
if (indices.isEmpty()) return listOf()
return this.subList(indices.start, indices.endInclusive + 1).toList()
}
- 함수의 타입 파라미터 T가 수신 객체와 반환 타입에 쓰인다
fun main(args: Array<String>) {
val letters = ('a'..'z').toList()
println(letters.slice<Char>(0..2))//타입인자를 명시적으로 지정한다
println(letters.slice(10..13))//컴파일러는 여기서 T가 Char라는 사실을 추론
}
- 두 호출 결과 타입 모두 List<Char>이며 컴파일러는 반환 타입 List<T>의 T를 자신이 추론한 Char로 치환한다.
클래스나 인터페이스 안에 정의된 메서드, 확장 함수 또는 최상위 함수에서는 수신 객체나 파라미터 타입에 타입 파라미터를 사용할 수 있다.
fun main(args: Array<String>) {
println(listOf(1, 2, 3, 4).penultimate)//이 호출에서 T는 Int로 추론
}
val <T> List<T>.penultimate: T //모든 리스트 타입에 이 제네릭 확장 프로퍼티를 사용할 수 있다
get() = this[size - 2]
- 제네릭 함수를 정의할 때와 마찬가지 방법으로 제네릭 확장 프로퍼티를 선언할 수 있다.
제네릭 클래스 선언
코틀린에서도 자바와 마찬가지로 타입 파라미터를 넣은 <>를 클래스 이름 뒤에 붙이면 클래스를 제네릭하게 만들 수 있다.
타입 파라미터를 이름 뒤에 붙이고 나면, 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있다

class StringList: List<String>{ //이 클래스는 구체적인 타입인자로 String을 지정해 List를 구현한다
override fun get(index: Int): String {
}
}
class ArrayList<T> : List<T> { //ArrayList의 제네릭 타입 파라미터 T를 List의 타입인자로 넘긴다
override fun get(index: Int): T {
TODO("Not yet implemented")
}
}
- 제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다
- 구체적인 타입을 넘길 수도 있고(여기서는 String), 타입 파라미터로 받은 타입을 넘길 수도 있다(T)
- StringList 같은 경우 String 기반 타입의 타입 인자를 지정하였으므로 하위 클래스에서 상위 클래스에 정의된 함수를 오버라이드 하거나 사용하려면 타입 인자 T를 구체적 타입 String으로 치환해야 한다.
- ArrayList 클래스는 자신만의 타입 파라미터 T를 정의하면서 그 T를 기반 클래스의 타입 인자로 사용한다.
- ArrayList<T>의 T와 List<T>의 T와 전혀 다른 타입 파라미터이며, 실제로는 T가 아니라 다른 이름을 사용해도 의미에는 아무 차이가 없다.


- String 클래스는 제네릭 Comparable 인터페이스를 구현하면서 그 인터페이스의 타입 파라미터 T로 String 자신을 지정한다
타입 파라미터 제약
타입 파라미터 제약은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다. SUM 같은 경우 Int, Double에는 적용돼야 하지만, String에는 적용되면 안 되기 때문에 제약을 거는 것이다.
어떤 타입을 제네릭 타입의 타입 파라미터에 대한 상한으로 지정하면 그 제네릭 타입을 인스턴스화 할 때 사용하는 타입 인자는 반드시 그 상한 타입의 하위 타입이어야 한다.
제약을 가하기 위해서는 타입 파라미터 이름 뒤에 콜론(:)을 표시하고 그 뒤에 상한 타입을 적으면 된다(자바에서는 extents 키워드를 써서 제약을 걸었다)
//T:타입파라미터, Number:상한
fun <T : Number> List<T>.sum(): T {
//logic
}
fun main(args: Array<String>) {
println(oneHalf(3))
}
fun<T: Number> oneHalf(value: T): Double{ //Number를 타입 파라미터 상한으로 정한다
return value.toDouble() / 2.0 //Number 클래스에 정의된 메서드를 호출
}
- 타입 파라미터 T에 대한 상한을 정하고 나면 T 타입의 값을 그 상한 타입의 값으로 취급할 수 있다.
fun main(args: Array<String>) {
println(max("kotlin", "java"))//문자열은 알파벳순으로 비교된다
}
fun <T: Comparable<T>> max(first: T, second: T): T { //이 함수의 인자들은 비교 가능해야 한다
return if(first > second) first else second
}
- 두 파라미터 사이에서 더 큰 값을 찾는 제네릭 함수를 작성했다.
- T의 상한 타입은 Comparable<T>이다.
- max를 비교할 수 없는 값 사이에 호출하면 컴파일 오류가 난다
- first > second는 관례상 first.compareTo(secone) > 0으로 컴파일된다.
- max 함수에서 first의 타입 T는 Comparable<T>를 확장하므로 first를 다른 T 타입 값인 second와 비교할 수 있다
fun main(args: Array<String>) {
val helloWorld = StringBuilder("Hello World")
ensureTrailingPeriod(helloWorld)
println(helloWorld)
}
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable{ //타입 파라미터 제약 목록
if(!seq.endsWith('.')){//CharSequence 인터페이스의 확장 함수를 호출
seq.append('.')//Appendable 인터페이스의 메서드를 호출
}
}
- 타입 파라미터에 둘 이상의 제약을 가해야 하는 경우 위와 같은 문법을 사용한다.
- 이 예제는 타입 인자가 CharSequence와 Appendable 인터페이스를 반드시 구현해야 한다는 사실을 표현
- 데이터에 접근하는 연산, 데이터를 변환하는 연산을 T 타입 값에 수행할 수 있다는 의미
타입 파라미터를 널이 될 수 없는 타입으로 한정
제네릭 클래스나 함수를 정의하고 그 타입을 인스턴스화 할 때는 널이 될 수 있는 타입을 포함하는 어떤 타입으로 타입 인자를 지정해도 타입 파라미터를 치환할 수 있다. 아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?를 상한으로 정한 파라미터와 같다.
class Processor<T> {
fun process(value: T){
value?.hashCode() //value는 널이 될 수 있다.따라서 안전한 호출을 해야한다
}
}
fun main(args: Array<String>) {
val nullableStringProcessor = Processor<String?>()//널이 될수 있는 타입인 String?이 T를 대신한다
nullableStringProcessor.process(null)//null이 value 인자로 지정된다
}
- process 함수에서 value 파라미터의 타입 T에는 물음표(?)가 붙어 있지 않지만 실제로는 T에 해당하는 타입 인자로 널이 될 수 있는 타입을 넘길 수도 있다
- processor 클래스를 널이 될 수 있는 타입을 사용해 인스턴스화 하였다
class Processor<T : Any> { //null 이 될 수 없는 타입 상한을 지정한다
fun process(value: T){
value.hashCode() //T 타입의 value는 null 이 될 수 없다
}
}
- 항상 널이 될 수 없는 타입만 인자로 받게 만들려면 타입 파라미터에 제약을 가하면 된다.
- 널 가능성을 제외한 아무런 제약도 필요 없다면 Any? 대신 Any를 상한으로 사용하면 된다.
- <T: Any> 제약은 T 타입이 항상 널이 될 수 없는 타입이 되게 보장한다
- 컴파일러는 타입 인자인 String?가 Any의 자손 타입이 아니므로 Processor<String?> 와 같은 코드에 오류를 발생시킨다.
'KotlinInAction > 제네릭스' 카테고리의 다른 글
| 변성: 제네릭과 하위 타입 (0) | 2022.08.15 |
|---|---|
| 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터 (0) | 2022.08.14 |