티스토리 뷰
728x90
제네릭
- 제네릭 선언
- 제네릭 선언을 위해서는 하나 이상의 타입 파라미터를 추가해야 한다.
- 선언을 사용할 때는 실제 타입을 지정한다.
open class P<T>(val t: T)
class Box<T>(t: T): P<T>(t) // 생성자 위임 호출 시 타입 인자 추론 x. 생략할 수 없음
fun main() {
val box1 = Box<Int>(1)
val box2 = Box(1) // 생성자 호출 시 타입 인자 생략 o
}
- 컴파일러가 타입을 추론할 수 있으면 타입 인자 생략 가능 (자바와 달리 <>까지 생략)
- 부모 생성자를 위임 호출할 때는 타입 인자를 추론해주지 못하므로 타입 인자를 꼭 명시해야 한다.
- 클래스 안에서는 타입 파라미터를 프로퍼티/함수의 타입, 다른 제네릭 선언의 타입으로 사용할 수 있다.
2. 바운드
fun <T : Comparable<T>> sort(list: List<T>) { ... }
fun main() {
sort(listOf(1, 2, 3)) // ok
sort(listOf(HashMap<Int, String>()) // error
}
- 타입 인자에 들어갈 수 있는 타입에 아무런 제약이 없다면, Any?와 동일하다.
- 특정 타입과 그 하위 타입으로만 타입 인자에 사용될 수 있도록 제한 → 상위 바운드(upper bound)
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
- where 키워드를 통해 상위 바운드를 여러개 지정할 수 있다.
3. 타입 구체화
- 타입 소거(type easer): 제네릭 타입 인자의 정보는 런타임에 알 수 없다. is/as 연산자를 사용할 수 없다.
- 예를 들어 Foo<Bar>나 Foo<Baz?> 등이 Foo<*>로 변경됨.
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) { // 타입 소거되지 않으므로 is 연산자를 통해 타입 검사 가능
destination.add(element)
}
}
return destination
}
fun main() {
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>()) // [one, three]
}
- 구체화: 제네릭 타입 인자의 정보를 런타임까지 유지한다.
- 인라인 함수에 대해서만 사용할 수 있다.
- 함수 본문을 호출 위치로 인라인시키기 때문에 실제 타입을 항상 알 수 있다.
- reified 키워드로 타입 파라미터를 지정한다.
4. 변성
- 무공변(invariant)
- 타입 파라미터의 하위 타입 관계와 상관없이, 제네릭 타입 사이에는 하위 타입 관계가 존재하지 않음.
- in/out 키워드 없이 제네릭 타입을 선언하면 모두 무공변.
// 타입 파라미터 T가 항상 out 위치에서만 사용됨
interface LazyList<out T> {
fun get(index: Int): T
fun subList<range: IntRange): LazyList<T>
fun getUpTo(index: Int): () -> List<T>
}
- 공변성(covariant): 하위 타입 관계를 유지
- A가 B의 하위 타입일 때, Foo<A>가 Foo<B>의 하위 타입이면 Foo는 공변적.
- 타입 파라미터를 out으로 선언 → 생산자 역할
- T 타입의 값을 반환하기만 하고 T 타입의 값을 입력으로 받지는 않음.
// 타입 파라미터 T가 항상 in 위치에서만 사용됨
interface Writer<in T> {
fun write(value: T)
fun writeList(values: Iterable<T>)
}
- 반공변성(contravariant): 뒤집한 하위 타입 관계
- A가 B의 하위 타입일 때, Foo<B>가 Foo<A>의 하위 타입이면 Foo는 반공변적.
- 타입 파라미터를 in으로 선언 → 소비자 역할
- T 타입의 값을 입력으로 받기만 하고 T 타입의 값을 반환하지는 않음.
- 사용 지점 변성
- 제네릭 타입을 사용하는 위치에서 in/out을 사용 → 프로젝션
fun copy(from: Array<Any>, to: Array<Any>) {
for (i in from.indices)
to[i] = from[i]
}
fun main() {
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
}
- Array가 무공변이므로 Array<Any>에 Array<Int>의 값을 대입할 수 없음
fun copy(from: Array<out Any>, to: Array<Any>) {
for (i in from.indices)
to[i] = from[i]
}
fun main() {
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
}
- 사용 지점 변성을 통해 해결
- from의 타입 파라미터를 out으로 변경
- 스타 프로젝션
- *로 표시
- 타입 파라미터의 바운드 안에서 아무 타입이나 될 수 있다는 의미.
- Foo<Any?>: 아무 타입의 값이 들어갈 수 있음 ↔ Foo<*>: 어떤 타입인지 알려져 있지 않음
interface Producer<out T> { ... }
interface Consumer<in T> { ... }
fun main() {
val producer: Producer<*> // Producer<Any?>와 동일
val consumer: Consumer<*> // Consumer<Nothing>과 동일
}
-
- 선언 지점 변성이 붙은 타입 파라미터를 대신하여 쓸 수 있다.
728x90
'kotlin' 카테고리의 다른 글
[kotlin/코틀린] 리플렉션 (0) | 2023.07.19 |
---|---|
[kotlin/코틀린] 어노테이션 (0) | 2023.07.19 |
[kotlin/코틀린] 컬렉션 (0) | 2023.07.19 |
[kotlin/코틀린] 클래스 계층 (0) | 2023.07.19 |
[kotlin/코틀린] 클래스 종류 (이넘 클래스, 데이터 클래스, 인라인 클래스) (0) | 2023.07.19 |