참조 : Kotlin in action
9.1. Generic type parameters
-
Java 와 기본적으로 generic 사용법은 동일하다.
-
Type Inference 는 가능하지만 명시적으로 써야 하는 케이스도 있다.
val authors = listOf(“Dmitry”, “Svetlana”) // type inference 가능
val readers : MutableList<String> = mutableListOf() // type inference 불가능, 명시적 선언
val readers = mutableListOf<String>()
-
Kotlin 에서는 raw type generic 을 사용할 수 없다. ( Java 로 치자면 그냥 List )
9.1.1. Generic functions and properties
-
extension property 에도 generic 을 사용할 수 있다.
val <T> List<T>.penultimate: T // penultimate 는 끝에서 2번째라는 뜻
get() = this[size - 2]
println( listOf(1,2,3,4).penultimate ) // 3
-
regular property 는 type parameter 를 가질 수 없다.
( generic non-extension property 는 만들 수 없다. )
val <T> x:T = TODO() // compile error
9.1.2. Declaring generic classes
9.1.3. Type parameter constraints
-
Type parameter 의 constraint 를 주려면 : 을 이용하면 된다.
fun <T : Number> List<T>.sum() : T // Number 를 upper bound 라고 한다.
subtype 이 subclass 와 같다고 생각할 수 있으나 엄밀히 이야기하면 다르다.
이는 나중에 다룬다.
-
다음과 같은 복잡한 generic constraint 도 정의 가능하다.
fun <T: Comparable<T>> max(first:T, second:T): T{
return if (first > second) first else second
}
-
여러 개의 다른 constraint 를 주려면 다음과 같이..
fun <T> ensureTrailingPeriod(seq: T) where T : CharSequence, T : Appendable {
if (!seq.endsWith(‘.’)){
seq.append(‘.’)
}
}
여기서 T 는 CharSequence 와 Appendable interface 둘 다를 구현해야 한다.
9.1.4. Making type parameters non-null
-
class Processor<T>{
fun process(value: T){
value?.hashCode()
}
}
아무 upper bound 도 명시하지 않으면 Any? 가 upper bound 이다.
null 불가능하게 하려면 : Any 를 upper bound 로 명시해주면 된다.
9.2. Generics at runtime: Erases and reified type parameters
-
JVM 에서 type 은 지워진다.
그러나 Kotlin 에서 inline 을 사용하면 type erasure 를 막을 수 있다. ( Kotlin 에서는 이를 reified 라고 부른다. )
9.2.1. Generics at runtime: type checks and casts
-
Kotlin 의 generic 도 runtime 에 마찬가지로 지워진다.
List<String> 도 runtime 에는 List 인 것이다.
그래서 다음 코드는 compile 되지 않는다.
if ( value is List<String> ) { … } // ex) value 가 Any type 일 경우?
대신 이렇게 써야 한다.
if ( value is List<*>) { … } // 그냥 List 가 아닌 이유는 Kotlin 이 raw type 을 인정하지 않기에, Java 의 List<?> 와 비슷
여기서 * 은 projection 이라 불리는데 이유는 나중에 알 수 있다.
-
type erase 의 장점은 memory 절약에 있다.
-
as 나 as? 로 generic collection 을 casting 하면 unchecked cast warning 을 볼 수 있다.
cast operation 이 실패하지 않기 때문에 (type param 이 없어지니깐) runtime 에서 ClassCastException 이 발생할 수 있다.
fun printSum(c: Collection<*>){
val intList = c as? List<Int> ?: throw IllegalArgumentException(“List is expected”)
println(intList.sum())
}
-
만약 type parameter 가 명시되어 있다면 is check 는 잘 동작한다.
fun printSum(c: Collection<Int>){
if ( c is List<Int> ){
println(c.sum())
}
}
9.2.2. Declaring functions with reified type parameters ( reify : 구체화하다 )
-
fun <T> isA(value: Any) = value is T
// compile error “Error: Cannot check for instance of erased type: T”
위의 에러를 피하는 한가지 방법은, inline function 을 사용하는 것이다.
이 경우에는 reified 될 수 있다.
( reified 라는 것은 runtime 에 type argument 를 알 수 있다는 것 )
( inline 은 함수 call 을 생략할 수 있고, lambda 와 함께 사용되는 경우 anonymous class 를 안 만드는 등 성능 향상이 있다. )
inline fun <reified T> isA(Value: Any) = value is T // no error
-
reified type parameter 가 작동하는 예 중 하나는 filterIsInstance function 이다.
이 함수는 collection 을 받아서 특정 class 에 해당하는 element 들만 가진 collection 을 return 해준다.
val items = listOf(“one”, 2, “three”)
println(items.filterIsInstance<String>())
// 결과는 [“one”, “three”]
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T>{
val destination = mutableListOf<T>()
for (element in this){
if (element is T){
destination.add(element)
}
}
}
-
reified type parameter 을 가진 inline function 은 Java 에서 호출할 수 없다.
일반적인 inline function 은 호출할 수 있다. ( 실제로 inline 되지는 않는다. )
9.2.3. Replacing class references with reified type parameters
-
reified type parameter 가 쓰이는 대표적인 곳은 Class type 을 param 으로 받는 adapter 스타일 API 를 만드는 것이다.
대표적인 예는 JDK 의 ServiceLoader이다.
val serviceImpl = ServiceLoader.load(Service::class.java)
// ::class.java 는 java.lang.Class 를 Kotlin 에서 얻는 방법이다. Java 에서는 단순히 Service.class 이다.
reified type parameter 를 사용한다면..
val serviceImpl = loadService<Service>()
inline fun <reified T> loadService(){
return ServiceLoader.load(T::class.java)
}
-
Android 에서 activity 띄울 때도 reified type parameter 를 쓰면 좋다.
inline fun <reified T: Activity> Context.startActivity(){
val intent = Intent(this, T::class.java)
startActivity(intent)
}
startActivity<DetailActivity>()
9.2.4. Restrictions on reified type parameters
-
몇 가지 제약사항이 있는데, 언어적인 것도 있고 현재 버전적인 것도 있다.
다음은 reified type parameter 사용되는 곳(방법)이다.
type check 의 is, !is, as, as?
Kotlin reflection API 에서 ::class 관련하여 ( 나중에 다룬다 )
Java 의 class 를 접근하기 위한 ::class.java
다른 함수 호출을 위한 type argument
다음은 할 수 없다.
type parameter 를 통해 new instance 를 만들 수 없다.
type parameter class 의 companion object 를 호출할 수 없다.
non-reified type parameter 를 reified type parameter 를 가진 함수 호출하는 데 사용할 수 없다.
class 의 type parameter, property, non-inline function 에는 reified marking 을 할 수 없다.
9.3. Variance: Generics and subtyping
-
“variance” 는 base type 과 다른 type argument 가 얼마나 관계가 있느냐를 나타낸다.
예를 들어 List<String> 과 List<Any> 를 비교하는 것을 말한다.
9.3.1. Why variance exists: passing an argument to a function
-
fun printContents(list: List<Any>){
println(list.joinToString())
}
compile 이 된다.
fun addAnswer(list: MutableList<Any>){
list.add(42)
}
compile error 가 난다, list 에 List<String> 형태가 들어올 경우 type mismatch 가 날 수 있기 때문이다.
9.3.2. Classes, types, and subtypes
-
generic 에서는 class 와 type 이 더 명확히 구분된다.
List<Int> 에서 List 는 type 이 아닌 class 이다.
List<Int> 가 type 이다.
-
Int 는 Int 의 subtype.
Int 는 Int? 의 subtype 이지만,
Int? 는 Int 의 subtype 이 아니다.
( class 와 type 의 구분 )
-
MutableList 는 type parameter 에 invariant 하다. (나중에 다시 다룬다.)
-
A 가 B 의 subtype 이라면, List<A> 가 List<B> 의 subtype 이다.
이런형태의 것은 covariant 라고 부른다.
9.3.3. Covariance: preserved subtyping relation
-
covariant class 는 A 가 B 의 subtype 라면 AClass<A> 가 AClass<B> 의 subtype 이 되는 generic class 를 말한다.
-
Kotlin 에서 covariant 로 만들려면 “out” keyword 를 사용해야 한다.
interface Producer<out T> {
fun produce(): T
}
T 에 대해 covariant 한 class Producer 정의
-
open class Animal{
fun feed(){ … }
}
class Herd<T : Animal> { // Herd 는 목동, covariant 하지 않다
val size: Int get() = …
operator fun get(i: Int): T { … }
}
fun feedAll(animals: Herd<Animal>){
for( i in 0 until animals.size){
animals[i].feed()
}
}
class Cat: Animal(){
fun cleanLitter(){ … }
}
fun takeCareOfCats(cats: Herd<Cat>){
for (i in 0 until cats.size){
cats[i].cleanLitter()
feedAll(cats) // compile error, it’s not covariant. Herd 클래스 정의에 out 을 넣어줘야 한다.
}
}
-
모든 것을 다 covariant 로 만들 수는 없다.
function 이 “out” 이라는 키워드에 맞게, T 에 대한 produce 를 해야 하고, T 를 consume 하면 안된다.
interface Transformer<T> {
fun transform(t: T): T // 앞의 T 는 in position (consume), 뒤의 T는 out position (produce)
}
generic 에서의 out 은 위의 정의에서와 마찬가지로 T 가 out position 에 오도록만 사용되야 한다는 것.
class Herd<out T: Animal> {
val size: Int get() = …
operator fun get(i: Int): T { … } // T 가 out position 에 있다.
}
interface List<out T> : Collection<T>{
operator fun get(index: Int): T
}
MutableList 는 in, out position 모두 positioning 되기 때문에 covariant 가 될 수 없다.
-
constructor val parameter 는 in, out 모두 아니다. ( 고려대상이 아니다 )
( Immutable 이라도 최초 값을 설정되어야 하니 당연히 이리 되어야징? )
class Herd<out T: Animal>(vararg animals: T){ … }
그러나 constructor 가 val 과 var 모두 사용한다면 getter 와 setter 가 다 있어서 immutable 하지 않기 때문에 covariant 로 만들 수 없다.
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T){ … } // out 정의 불가
-
추가로 position rule 은 visible(public, protected, internal) API 에만 적용된다.
private 은 in, out position 전혀 상관없다.
class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T){ … }
9.3.4. Contravariance: reversed subtyping relation
-
contravariance 는 covariance 의 mirror 라고 볼 수 있다.
오직 in position 에만 사용된다.
interface Comparator<in T>{
fun compare(e1: T, e2: T): Int{ … }
}
in 의 경우는 반대로 Comparator<Any> 는 Comparator<String> 의 subtype 이다.
이 경우 마찬가지로 Any 는 String 의 subtype 이다.
-
-
한 개의 class 또는 interface 에 covariant 와 contravariant 모두 가질 수 있다.
interface Function1<in P, out R>{
operator fun invoke(p: P): R
}
(P) -> R 은 Function1 을 lambda form 으로 쓴 것이다.
이 형태를 보면 in 과 out 이 좀 더 명확하게 보인다.
9.3.5. Use-site variance: specifying variance for type occurrences
-
variance modifier 를 클래스 정의에 사용하는 것을 declaration-site variance 라고 불린다. ( in, out )
Java 의 경우는 type parameter 를 사용할 때마다 정의를 한다. 이 경우는 use-site variance 라고 부른다. ( ? extends T, ? super T )
-
Kotlin 은 기본적으로 declaration-site variance 를 지원하지만, use-site variance 를 사용할 수도 있다.
예를 들면 MutableList 는 invariant 지만 실제로는 producer 로만 사용되거나 consumer 로만 사용하는 경우 use-site variance 로 지원할 수 있다.
fun<T: R, R> copyData(source: MutableList<T>, destination: MutableList<R>){
for (item in source){
destination.add(item)
}
}
val ints = mutableListOf(1, 2, 3)
val anyItems = mutbleListOf<Any>()
copyData(ints, anyItems)
println(anyItems)
위의 코드는 아래와 같이 정의해서 사용될 수 있다.
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>){
for (item in source){
destination.add(item)
}
}
이 때 source 에 매핑되는 MutableList 는 type projection 된, 다시 말해 projected 된 MutableList 이다.
이 경우 out position 으로만 사용될 수 있으며, 당연히 in position 으로 사용되는 함수들은 부를 수 없다.
in position 으로 사용되는 경우 compile error 가 난다.
val list: MutableList<out Number> = …
list.add(42) // compile error
-
List<out T> 와 같은 경우는 redundant 이기 때문에 compiler 가 warning 을 표시할 것이다.
다시 말해 List 자체가 원래 List<out T> 로 정의되어 있다.
-
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>){
for(item in source){
destination.add(item)
}
}
이렇게 함으로써 destination 은 T 의 subType( Java 기준으로는 superType ) 이 될 수 있다.
T 가 String 이라면 in T 는 CharSequence
-
Kotlin 에서의 Use-site variance 선언은 Java 의 bounded wildcard 와 정확하게 매칭된다.
MutableList<out T> 는 MutableList<? extends T> 와 같고,
MutableList<in T> 는 MutableList<? super T> 와 같다.
9.3.6. Star projection: using * instead of a type argument
-
star projection syntax 라 불리는 "*“ 는 generic argument 에 대한 정보가 없음을 이야기한다.
-
MutableList<*> 는 MutableList<Any?> 와는 다르다.
MutableList<Any?> 는 어떤 type 이던 담을 수 있지만, MutableList<*> 는 한가지 type 만 담을 수 있다.
-
val list: MutableList<Any?> = mutableListOf(‘a’, 1, “qwe”)
val chars = mutableListOf(‘a’,’b’, ‘c’)
val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
unknownElements.add(42) // compile error
println(unknownElements.first()) // ok!
compile error 는 “Error: Out-projected type ‘MutableList<*>’ prohibits the use of ‘fun add(element: E): Boolean’”
이 경우에 compiler 가 MutableList<*> 를 MutableList<out Any?> 로 project 한 것이다.
Java 의 List<?> 와 동일하다고 보면 된다.
-
contravariant 가 Consumer<in T> 라면 star projection 은 <in Nothing> 이라고 보면 된다.
즉 consumer 로만 사용할 수 있고, 무엇을 consume 하는지는 모른다.
-
star projection 은 type 이 중요하지 않은 경우에 사용할 수 있다.
fun printFirst(list: List<*>){
if (list.isNotEmpty()){
println(list.first()) // list.first() 가 return 하는 것은 Any? 이다.
}
}
-
interface FieldValidator<in T>{
fun validate(input: T): Boolean
}
object DefaultStringValidator: FieldValidator<String>{
override fun validate(input: String) = input.isNotEmpty()
}
object DefaultIntValidator: FieldValidator<Int>{
override fun validate(input: Int) = input >= 0
}
val validators = mutableMapOf<KClass<*>, FieldValidator<*>>() // KClass 는 나중에 배워용
validators[String::class] = DefaultStringValidator
validators[Int::class] = DefaultIntValidator
validators[String::class]!!.validate(“”) // compile error
“Error:Out-projected type ‘FieldValidator<*>’ prohibits the use of ‘fun validate(input: T): Boolean’
-
고쳐보자..
object Validators{
private val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
fun <T: Any> registerValidator(kClass: kClass<T>, fieldValidator: FieldValidator<T>){
validators[kClass] = fieldValidator
}
@Suppress(“UNCHECKED_CAST”)
operator fun <T: Any> get(kClass: KClass<T>): FieldValidator<T> = validators[kClass] as? FieldValidator<T>?: throw IllegalArgumentException(“No validator for ${kclass.simpleName}”)
}
Validators.registerValidator(String::class, DefaultStringValidator)
Validators.registerValidator(Int::class, DefaultIntValidator)
println(Validators[String::class].validate(“Kotlin”))
println(Validators[Int::class].validate(42))
9.4. Summary
-
Kotlin 의 generic 은 Java 의 것과 매우 비슷하다.
generic function, generic class 를 동일한 방식으로 정의한다.
-
Java 에서는 compile time 에만 generic type 이 존재한다.
-
is operator 를 통해서 type argument 를 함께 사용할 수 없다. 왜냐하면 runtime 에 type argument 는 제거되기 때문.
-
inline function 의 Type parameter 는 reified 로 mark 된다.
이 말은 runtime 에 is check 를 가능하게 하고, java.lang.Class 를 얻을 수도 있다.
-
Variance 는 같은 base class 를 갖는 2개의 generic type 중 혹은 2개의 다른 type argument 중에 어떤 녀석이 subtype 이고 어떤 것이 super type 인지를 정하는 것이다.
-
type parameter 가 out position 에만 사용된다면 class 를 covariant 로 선언할 수 있다.
-
반대로 type parameter 가 in position 에만 사용된다면 class 를 contravariant 로 선언할 수 있다.
-
List 와 같은 read-only interface 는 covariant 이다.
이 말은 List<String> 은 List<Any> 의 subtype 이다.
-
first type parameter 가 contravariant 이고, second type parameter 가 covariant 인 function interface 의 경우에는
(Animal) -> Int 가 (Cat) -> Number 의 subtype 이다.
-
Kotlin 은 generic class 전체에 명시할 수도 있고 ( 이를 declaration-site variance 라고 한다. ),
특정 generic type 사용에만 명시할 수도 있다. ( 이를 use-site variance 라고 한다. )
-
star projection syntax 는 type argument 를 알 수 있거나 중요하지 않은 경우에 사용할 수 있다.
댓글