Chapter 9. 제네릭스

9.1 제네릭 타입 파라미터

9.1.1 제네릭 함수와 프로퍼티

fun main(args: Array<String>) {
    val letters = ('a'..'z').toList()
    println(letters.slice<Char>(0..2)) // 타입 인자를 명시적으로 지정
    println(letters.slice(10..13)) // 컴파일러는 여기서 T가 Char라는 사실을 추론


    val authors = listOf("Dmitry", "Svetlana")
    val readers = mutableListOf<String>("Dmitry", "AA", "CC")
    println(readers.filter { it !in authors }) // [AA, CC]
}
val <T> List<T>.penultimate: T // 모든 리스트 타입에 이 제네릭 확장 프로퍼티를 사용할 수 있음
    get() = this[size - 2]

fun main(args: Array<String>) {
    println(listOf(1, 2, 3, 4).penultimate) // 이 호출에서 타입 파라미터 T는 Int로 추론됨
}



9.1.2 제네릭 클래스 선언

interface List<T> {  // List 인터페이스에 T라는 타입 파라미터를 정의함
    operator fun get(index: Int) : T // 인터페이스 안에서 T를 일반 타입처럼 사용가능
}

class StringList: List<String> { // 이 클래스는 구체적인 타입 인자로 String을 지정해 List를 구현함
    override fun get(index: Int) : String = this[index]
}

class ArrayList<T> : List<T> { // ArrayList의 제네릭 타입 파라미터 T를 List의 타입 인자로 넘김
    override fun get(index: Int) : T = this[index]
}


interface Comparable<T> {
    fun compareTo(other: T) : Int
}

class String : Comparable<String> {
    override fun compareTo(other: String) : Int {
        return if (this === other) 1
        else 0
    }
}



9.1.3 타입 파라미터 제약

타입 파라미터 제약 : 클래스타 함수에 사용할 수 있는 타입 인자를 제한하는 기능

fun <T : Number> oneHalf(value: T) : Double { // Number를 타입 파라미터 상한으로 정함
    return value.toDouble() / 2.0 // Number 클래스에 정의된 메소드를 호출함
}

// 타입 파라미터를 제약하는 함수 선언하기
fun <T: Comparable<T>> max(first: T, second: T) : T { // 이 함수의 인자들은 비교 가능해야 함
    return if(first > second) first else second
}

fun main(args: Array<String>) {
    println(oneHalf(3)) // 1.5


    // 문자열은 알파벳순으로 비교됨
    println(max("kotlin", "java")) // kotlin
    // println(max("kotlin", 42)) 컴파일 오류
}
fun <T> ensureTrailingPeriod(seq: T)
    where T : CharSequence, T : Appendable { // 타입 파라미터 제약 목록
        if (!seq.endsWith('.')) { // CharSequence 인터페이스의 확장 함수를 호출
            seq.append('.') // Appendable 인터페이스의 메소드를 호출
        }
    }


fun main(args: Array<String>) {
    val helloWorld = StringBuilder("Hello World")
    ensureTrailingPeriod(helloWorld)
    println(helloWorld) // Hello World.
}



9.1.4 타입 파라미터를 널이 될 수 없는 타입으로 한정

class Processor<T: Any> { // "null"이 될 수 없는 타입 상한을 지정
    fun process(value: T) {
        value.hashCode() // T 타입의 "value"는 "null"이 될 수 없음
    }
}

fun main(args: Array<String>) {
    val notNullStringProcessor = Processor<String>()
    // notNullStringProcessor.process(null) 컴파일 오류
}




9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터

9.2.1 실행 시점의 제네릭: 타입 검사와 캐스트

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> // Unchecked cast 경고 발생
        ?: throw IllegalArgumentException("Array is expected")
    println(intList)
}

fun main(args: Array<String>) {
    printSum(listOf(1, 2, 3))
    printSum(setOf(1, 2, 3)) // IllegalArgumentException 예외 발생
    printSum(listOf("a", "b", "c")) // classCastException 발생
}



9.2.2 실체화한 타입 파라미터를 사용한 함수 선언

인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있음

inline fun <reified T> isA(value: Any) = value is T

/* filterIsInstance 간단하게 정리한 코드
inline fun <reified T>
    Iterable<*>.filterIsInstance() : List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
    return destination
}

 */

fun main(args: Array<String>) {
    println(isA<String>("abc")) // true
    println(isA<String>(123)) // false


    val items = listOf("one", 2, "three")
    println(items.filterIsInstance<String>()) // [one, three]
}



9.2.3 실체화한 타입 파라미터로 클래스 참조 대신

inline fun <reified T> loadService(): ServiceLoader<T>? {
    return ServiceLoader.load(T::class.java)
}

fun main(args: Array<String>) {
    val serviceImpl = loadService<Service>()
}



9.2.4 실체화한 타입 파라미터의 제약

  • 타입 파라미터 클래스의 인스턴스 생성하기
  • 타입 파라미터 클래스의 동반 객체 메소드 호출하기
  • 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
  • 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기




9.3 변성: 제네릭과 하위 타입

9.3.1 변성이 있는 이유: 인자를 함수에 넘기기

어떤 함수가 리스트의 원소를 추가하거나 변경한다면 <Any> 대신 <String> 등을 넘길 수 x



9.3.2 클래스, 타입, 하위 타입

fun test(i: Int) {
    val n: Number = i // Int가 Number의 하위 타입이어서 컴파일 됨

    fun f(s: String) {
        println(s)
    }
    // f(i) // Int가 String의 하위 타입이 아니어서 컴파일 되지 x
}



9.3.3 공변성: 하위 타입 관계를 유지

out 키워드 사용

class Herd<out T : Animal>(vararg animals: T) { // T는 이제 공변적이다.
    val size: Int get() = this.size
    operator fun get(i: Int) : T = this[i]
}



9.3.4 반공변성: 뒤집힌 하위 타입 관계

공변성반공변성무공변성
Producer<out T>Consumer<in T>MutableList<T>
타입 인자의 하위 타입 관계까 제네릭 타입에서도 유지타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힘하위 타입 관계가 성립하지 않음
Producer<Cat>은 Producer<Animal>의 하위 타입Consumer<Animal>은 Consumer<Cat>의 하위 타입
T를 아웃 위치에서만 사용 가능T를 인 위치에서만 사용 가능T를 아무 위치에서나 사용 가능



9.3.5 사용 지점 변성: 타입이 언급되는 지점에서 변성 지정

사용 지점 변성 : 타입 파라미터가 있는 타입을 사용할 때 마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시하는 방식

/* 무공변 파라미터 타입을 사용하는 데이터 복사 함수
fun <T> copyData (source: MutableList<T>, destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}
 */

/*
// 타입 파라미터가 둘인 데이터 복사 함수
fun <T: R, R> copyData(source: MutableList<T>, destination: MutableList<R>) { // source 원소 타입은 destination 원소 타입의 하위 타입이어야 함
    for (item in source) {
        destination.add(item)
    }
}*/

/*
// 아웃-프로젝션 타입 파라미터를 사용하는 데이터 복사 함수
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
// out 키워드를 타입을 사용하는 위치 앞에 붙이면 in 위치에 사용하는 메소드를 호출하지 않는다는 뜻
    for (item in source) {
        destination.add(item)
    }
}
 */

// in 프로젝션 타입 파라미터를 사용하는 데이터 복사 함수
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
    // 원본 리스트 원소 타입의 상위 타입을 대상 리스트 원소 타입으로 허용
    for (item in source) {
        destination.add(item)
    }
}

fun main(args: Array<String>) {
    val ints = mutableListOf(1, 2, 3)
    val anyItems = mutableListOf<Any>()
    copyData(ints, anyItems) // Int가 Any의 하위 타입이므로 이 함수를 호출할 수 있음
    println(anyItems) // [1, 2, 3]


    val list: MutableList<out Number> = mutableListOf(1, 2, 3)
    //list.add(42) // 컴파일 오류
}



9.3.6 스타 프로젝션: 타입 인자 대신 * 사용

interface FieldValidator<in T> { // T에 대해 반공변인 인터페이스를 선언
    fun validate(input: T): Boolean // T를 "인" 위치에만 사용함(이 메소드는 T 타입의 값을 소비함)
}

object DefaultStringValidator : FieldValidator<String> {
    override fun validate(input: String) = input.isNotEmpty()
}

object DefaultIntValidator : FieldValidator<Int> {
    override fun validate(input: Int) = input >= 0
}

// 검증기 컬렉션에 대한 접근 캡슐화하기
object Validators {
    private val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
    fun <T: Any> registerValidator(
        kClass: KClass<T>, fieldValidator: FieldValidator<T>) {
        validators[kClass] = fieldValidator // 어떤 클래스와 검증기가 타입이 맞아 떨어지는 경우에만 그 클래스와 검증기 정보를 맵에 키/값 쌍으로 넣음
    }

    @Suppress("UNCHECKED_CAST") // FieldValidator<T> 캐스팅이 안전하지 않다는 경고를 무시하게 만듦
    operator fun <T: Any> get (kClass: KClass<T>) : FieldValidator<T> =
        validators[kClass] as? FieldValidator<T>
            ?: throw IllegalArgumentException(
                "No Validator for ${kClass.simpleName}"
            )
}

fun main(args: Array<String>) {
    val validators = mutableMapOf<KClass<*>, FieldValidator<*>>()
    validators[String::class] = DefaultStringValidator
    validators[Int::class] = DefaultIntValidator

    //validators[String::class]!!.validate("") // 컴파일 오류

    // 검증기를 가져오면서 명시적 타입 캐스팅 사용하기
    val stringValidator = validators[String::class] as FieldValidator<String>
    println(stringValidator.validate("")) // false


    // 검증기를 잘못 가져온 경우
    val stringValidator2 = validators[Int::class] as FieldValidator<String> // 검증기를 잘못 가져왔지만 컴파일과 타입 캐스팅시 아무 문제 없음
    //stringValidator2.validate("") // 검증기를 사용해야 비로소 오류 발생



    // 검증기 컬렉션에 대한 접근 캡슐화 테스트
    Validators.registerValidator(String::class, DefaultStringValidator)
    Validators.registerValidator(Int::class, DefaultIntValidator)
    println(Validators[String::class].validate("Kotlin")) // true
    println(Validators[Int::class].validate(42)) // true
}

좋은 웹페이지 즐겨찾기