2023 01 16 코틀린 공부 - 제네릭
9장 제네릭스
제네릭 타입의 인스턴스에는 구체적인 타입 인자가 필요하다. 사용할 때 빈 제네릭을 만들 때 타입 인자를 추론할 근거가 없기 때문에 타입인자를 직접 명시해야 한다. 아래의 두 코드는 같은 의미이며 둘 모두 타입인자를 지정한 모습이다.
val readers : MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
모든 제네릭을 다룰 수 있는 함수인 제네릭 함수를 호출할 때 구체적인 타입으로 타입 인자를 넘겨야 한다. 아래의 코드의 경우 slice함수를 사용할 때 컴파일러가 타입 인자를 추론할 수 있으므로 타입인자를 지정하지 않아도 된다.
val letters = ('a'..'z').toList()
println(letters.slice(0..2))
제네릭 확장 프로퍼티도 선언할 수 있다. 아래의 코드는 리스트의 끝에서 두번째 원소를 반환하는 확장 프로퍼티이다.
val <T> List<T>.penultimate: T
get() = this[size - 2]
println(listOf(3,4,5,6).penultimate)
클래스의 이름 뒤에 타입 파라미터를 넣은 <>를 붙이면 제네릭 클래스를 제네릭하게 만들 수 있다. 아래의 코드의 경우 List<T>를 상속받은 두 클래스 중 stringList는 T가 String이므로 get의 return 타입 T가 String으로 치환되어야 한다. ArrayList<T>의 T는 List<T>와는 다른 자신만의 T을 정의하며 사용한다.
interface List<T>{
operator fun get(index: Int): T
}
class stringList: List<String>{
override fun get(index: Int): String {
return this[index]
}
}
class ArrayList<T>: List<T>{
override fun get(index: Int): T {
return this[index]
}
}
클래스나 함수에 사용할 수 있는 타입을 타입인자를 제한하는 기능으로 타입 파라미터 제약이 있다. 타입 파라미터 뒤에 :을 쓰고 뒤에 상한 타입을 적으면 상한 타입만 사용할 수 있게 된다. 아래의 코드는 Number로 들어온 T를 Double타입으로 변환한 후 2.0으로 나눈 값을 return한다. String타입등을 사용할 수 없는 함수이다.
fun <T : Number> oneHalf(value: T): Double {
return value.toDouble() / 2.0
}
println(oneHalf(3L))
제네릭 크래스나 함수의 타입 파라미터는 null이 될 수 있으며 파라미터를 Any로 제약하면 null이 될 수 없게 된다. 아래의 Processor 클래스는 Any?타입을 받을 수 없게되어 null이 될 수 없다.
class Processor<T: Any>{
fun process(value: T){
value.hashCode()
}
}
코틀린은 자바와 마찬가지로 런타임에 제네릭 타입 인자 정보가 지워지기 때문에 List<String> 객체를을 그냥 List객체로만 볼 수 있다. 아래의 두 리스트는 런타임에서 같은 타입의 객체로 본다.
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)
타입 인자를 따로 저장하지않기 때문에 실행 시점에서 타입 인자를 검사할 수 없다. 그래서 리스트의 타입인자의 타입을 검사할 수 없다. 타입 파라미터가 2개 이상이면 모든 타입 파라미터에 *을 포함시키는 스타 프로젝션을 사용하면 집합이 리스트인지 맵인지 확인할 수 있다.
제네릭 타입으로 타입 캐스팅을 하면 타입인자가 달라도 캐스팅이 되며 List로 캐스팅하는 함수에 집합을 넣으면 IllegalAgumentException이 발생하고 다른 타입의 원소가 들어있는 List에는 실행 시점에 ClassCastException이 발생한다.
기본적으로 제네릭 함수가 호출되도 호출할때 쓰인 타입 인자를 알 수 없으나 inline 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입인자를 알 수 있다. 타입 파라미터를 reified로 지정하면 타입이 T의 인스턴스인지 실행시점에 검사할 수 있다.
inline fun <reified T> isA (value: Any) = value is T
println(isA<String>("ABC"))
println(isA<String>(123))
이렇게 inline으로 실체화한 타입 파라미터에는 제약이 있다. 아래 4가지는 할 수 없다.
타입 파라미터 클래스의 인스턴스 생성
타입 파라미터 클래스의 동반 객체 메서드 호출
실체화한 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입 인자로 넘기기
클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기
변성(variance)란 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념이다. List<Any>타입의 타입파라미터를 받는 함수에 List<String>을 넘길 수 없는데 이유는 Any가 Int, Double도 될 수 있어 다른 타입의 원소가 들어와서 ClassCastException 등이 발생 할 수 있다. 그러나 원소 추가나 변경이 없는 경우에는 안전하기 때문에 넘겨도 안전하다.
타입 A의 값이 필요한 모든 곳에 타입 B의 값을 넣어도 아무 문제 없다면 타입 B는 타입 A의 하위 타입이다. Int는 Number의 하위 타입이며 String의 하위 타입은 아니다. 또한 null이 될 수 없는 타입은 null이 될 수 있는 타입의 하위 타입이다. 클래스도 동일하게 적용된다.
A가 B의 하위 타입이면 List<A>가 List<B>의 하위 타입이다. 이런 클래스나 인터페이스를 공변적이라고 말한다.
코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하기 위해 out을 타입 파라미터 이름 앞에 넣어야 한다.
interface Producer<out T>{
fun produce(): T
}
반환타입에 쓰이면 아웃위치, 파라미터 타입에 쓰이면 인 위차라 하는데 out키워드는 T의 사용법을 아웃 위치로 제한하며 하위 타입 관계의 타입 안전성을 보장한다.
반공변성은 타입 B가 타입 A의 하위 타입이고 Consumer<A>가 Comsumber<B>의 하위 타입일 때 Consumer<T>가 T에 반공변이다. 즉 제네릭 타입에서 하위 타입 관계가 뒤집힌다. 그래서 T를 인 위치에서만 사용가능하다.
클래스를 선언하면서 변성을 지정하는 방식을 선언 지점 변성이라고 한다. 코틀린도 이를 지원한다.
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>){
for (item in source){
destination.add(item)
}
}
스타 프로젝션: 타입 인자 대신 사용한다. MutableList<*>는 구체적인 타입의 원소를 저장하기 위해 만들어진 것이라는 뜻이다. 데이터를 읽기만 하거나 타입 인자 정보가 중요하지 않을때도 사용한다.