본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Kotlin Tutorial] Generics - Chap9. Generics

by 돼지왕 왕돼지 2017. 9. 5.
반응형


참조 : Kotlin in action

!is, *, ::class, ::class.java, ?, ? extends t, ? super t, adapter, android activity reified type parameter, any, any?, any? * difference, API, as, as?, base type, both contravariant and covariant, bounded wildcard, Cast, cast operation, CLASS, class type, classcastexception, classes, companion object, compiler warning, constructor parameter, CONSUME, Consumer, contravariance, covariance, covariance mirror, covariant, declaration-site variance, Declaring functions with reified type parameters, Declaring generic classes, default upper bound, Erased and reified type parameters, extends, filterisinstance, Function1, Generic, generic at runtime, Generic functions and properties, Generic type parameters, generic where, generics, Generics at runtime, Generics at runtime: type checks and casts, getter, immutable, immutable generic class, in, in nothing, in out, in out position, in position, inline function, inline function reified, Internal, invariant, is, java class, java.lang.class, jdk, jvm, kclass, Kotlin, kotlin basics, kotlin generics, kotlin reflection api, kotlin tutorial, lambda form, list, Making type parameters non-null, Memory, mutablelist, mutablelistof, new instance, non-extension property, non-reified type parameter, object, out, out keyword, out position, preserved subtyping relation, Private, Private Method, produce, Producer, Project, projected, Projection, Protected, Public, raw type, raw type generic, redundant, regular property, reified, reified type parameter, reified type parameter inline function in java, Replacing class references with reified type parameters, Restrictions on reified type parameters, reversed subtyping relation, Runtime, serviceloader, Setter, specifying variance for type occurrences, star projection, Star projection: using * instead of a type argument, subclass, SubType, subtypes, Super, type, type argument, type check, type erase, type erasure, type inference, type param, type parameter, type parameter class, Type parameter constraints, type parameter erase, type parameter reified, type projection, types, unchecked cast warning, upper bound, use-site variance, Use-site variance: specifying variance for type occurrences, Val, Var, vararg, VARIANCE, Variance: Generics and subtyping, Visible, visible api, Where, [Kotlin Tutorial] Generics - Chap9. Generics, 상관 관계, 코틀린, 코틀린 강좌, 코틀린 기초 강좌


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 이다.



-

!is, *, ::class, ::class.java, ?, ? extends t, ? super t, adapter, android activity reified type parameter, any, any?, any? * difference, API, as, as?, base type, both contravariant and covariant, bounded wildcard, Cast, cast operation, CLASS, class type, classcastexception, classes, companion object, compiler warning, constructor parameter, CONSUME, Consumer, contravariance, covariance, covariance mirror, covariant, declaration-site variance, Declaring functions with reified type parameters, Declaring generic classes, default upper bound, Erased and reified type parameters, extends, filterisinstance, Function1, Generic, generic at runtime, Generic functions and properties, Generic type parameters, generic where, generics, Generics at runtime, Generics at runtime: type checks and casts, getter, immutable, immutable generic class, in, in nothing, in out, in out position, in position, inline function, inline function reified, Internal, invariant, is, java class, java.lang.class, jdk, jvm, kclass, Kotlin, kotlin basics, kotlin generics, kotlin reflection api, kotlin tutorial, lambda form, list, Making type parameters non-null, Memory, mutablelist, mutablelistof, new instance, non-extension property, non-reified type parameter, object, out, out keyword, out position, preserved subtyping relation, Private, Private Method, produce, Producer, Project, projected, Projection, Protected, Public, raw type, raw type generic, redundant, regular property, reified, reified type parameter, reified type parameter inline function in java, Replacing class references with reified type parameters, Restrictions on reified type parameters, reversed subtyping relation, Runtime, serviceloader, Setter, specifying variance for type occurrences, star projection, Star projection: using * instead of a type argument, subclass, SubType, subtypes, Super, type, type argument, type check, type erase, type erasure, type inference, type param, type parameter, type parameter class, Type parameter constraints, type parameter erase, type parameter reified, type projection, types, unchecked cast warning, upper bound, use-site variance, Use-site variance: specifying variance for type occurrences, Val, Var, vararg, VARIANCE, Variance: Generics and subtyping, Visible, visible api, Where, [Kotlin Tutorial] Generics - Chap9. Generics, 상관 관계, 코틀린, 코틀린 강좌, 코틀린 기초 강좌



-

한 개의 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 를 알 수 있거나 중요하지 않은 경우에 사용할 수 있다.






반응형

댓글