[코틀린 기초] 코틀린 제네릭 (Generics)
제네릭
제네릭은 컴파일 시점에 해당 타입을 확정짓지않고 런타임으로 유예시켜 다양한 타입을 받아들일 수 있도록 제공되는 문법입니다. 자바에서 Collection(List, Map, Set 등)을 활용해보셨다면 코틀린에서도 충분히 쉽게 다가올 수 있을 것입니다.
코틀린의 클래스는 자바와 마찬가지로 타입 파라미터를 가질 수 있습니다.
class BlackdogGenerics<T>(val t: T) {
}
타입 파라미터란 위의 제네릭 클래스의 T 타입입니다.
위의 제네릭 클래스를 실행로직에서 활용해보겠습니다. 타입 아규먼트를 제공해주면 됩니다.
fun main() {
val blackdogGenerics = BlackdogGenerics<String>("제네릭");
}
여기서 타입 아규먼트 선언은 생략할 수 있습니다. (타입추론)
fun main() {
val blackdogGenerics = BlackdogGenerics("제네릭");
}
타입추론이 가능하기 때문에 변수의 타입에 타입 아규먼트를 추가하거나 타입 아규먼트를 생성자에 표시할 수 있습니다.
fun main() {
val list1: MutableList<String> = mutableListOf()
val list2 = mutableListOf<String>()
}
어떠한 타입이 들어올 지 예상할 수 없는 경우, 안전하게 사용하기 위하여 스타 프로젝션 구문(*)을 제공합니다.
fun main() {
val list: List<*> = listOf<String>("테스트")
val listInt: List<*> = listOf<Int>(1, 2, 3);
val listDouble: List<*> = listOf<Double>(1.1, 2.2, 3.3);
println(list)
println(listInt)
println(listDouble)
}
변성
- 제네릭에서 파라미터화된 타입이 서로 어떤 관계에 있는지 설명하는 개념.
- 공변성, 반공변성, 무공변성으로 나뉜다.
- 이펙티브 자바에서는 공변성과 반공변성에 대해서 이야기할 때, PECS라는 규칙을 언급한다.
- PECS 는 Producer - Extends / Consumer - Super 의 약자입니다.
- 공변성은 자바의 extends 키워드이고 코틀린에서는 out
- 반공변성은 자바의 super 키워드이고 코틀린에서는 in
공변성
공변성을 활용하지 않아서 문제가 되는 케이스
class BlackdogGenerics<T>(val t: T) { }
fun main() {
val generics = BlackdogGenerics("테스트");
val charGenerics: BlackdogGenerics<CharSequence> = generics; // 컴파일 오류
}
- 현재 코드에서는 실수로 String이 아닌 CharSequence 타입을 대입하게 되는 경우 컴파일 오류가 발생할 수 있다. 이에 대해서 허용을 해주기 위해서는 공변성 키워드인 out을 활용해야한다.
class BlackdogGenerics<out T>(val t: T) { }
fun main() {
val generics = BlackdogGenerics("테스트");
val charGenerics: BlackdogGenerics<CharSequence> = generics;// 컴파일 성공.
}
<out T> 키워드는 자바에서 <? extends T>와 같은 문법으로 T가 CharSequence이기 때문에 그 하위타입인 제네릭 String 타입이 컴파일 될 수 있도록 코틀린 컴파일러가 이에 대한 거부를 하지 않습니다.
반공변성
반공변성을 활용하지 않아서 문제가 되는 케이스
class Bag<T> {
fun saveAll(to: MutableList<T>, from: MutableList<T>) {
to.addAll(from)
}
}
fun main() {
val bag = Bag<String>()
// in이 없다면 컴파일 오류
bag.saveAll(mutableListOf<CharSequence>("CharSequence"), mutableListOf<String>("String"))
}
현재 Bag<T> 타입의 클래스는 파라미터로 전달된 타입은 to, from 양쪽 모두 String입니다. 하지만 to 파라미터에 전달된 아규먼트는 CharSequence타입을 인자로 받으려고 하기 때문에 컴파일러는 이를 거부합니다. (위의 공변성 예와 동일한 케이스입니다.)
class Bag<T> {
fun saveAll(to: MutableList<in T>, from: MutableList<T>) {
to.addAll(from)
}
}
fun main() {
val bag = Bag<String>()
// in이 없다면 컴파일 오류
bag.saveAll(mutableListOf<CharSequence>("CharSequence"), mutableListOf<String>("String"))
}
이렇게 반공변성 키워드인 in을 사용해주면 상위 타입도 전달받을 수 있도록 처리됩니다. in 키워드는 자바에서 <? super T>와 같은 의미입니다.
제네릭의 경우 기본은 무공변성으로써 in, out 어떤 것도 지정하지 않은 경우입니다. 이는 List<String>과 List<CharSequence>는 그 어떤 관계또 없음을 나타냅니다.