KotlinInAction/제네릭스

제네릭 타입 파라미터

webmaster 2022. 8. 12. 14:52
728x90

제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있다. 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야 한다.

코틀린 컴파일러는 보통 타입과 마찬가지로, 타입 인자 추론이 가능하며, 빈 리스트를 만들어야 할 경우, 타입 인자를 타입 인자를 추론할 근거가 없기 때문에 직접 타입 인자를 명시해야 한다.

fun main(args: Array<String>) {
    val authors = listOf("Dmitry", "Svetlana") //타입인자추론
    //val readers: MutableList<String> = mutableListOf() //변수 타입을 지정
    val readers = mutableListOf<String>()//변수를 만드는 함수의 인자를 지정
}

제네릭 함수와 프로퍼티

제네릭 함수를 호출할 때는 반드시 구체적인 타입으로 타입 인자를 넘겨야 한다. 컬렉션을 다루는 라이브러리 함수의 대부분은 제네릭 함수이다.

List의 slice 메서드

public fun <T> List<T>.slice(indices: IntRange): List<T> {
    if (indices.isEmpty()) return listOf()
    return this.subList(indices.start, indices.endInclusive + 1).toList()
}
  • 함수의 타입 파라미터 T가 수신 객체와 반환 타입에 쓰인다
fun main(args: Array<String>) {
    val letters = ('a'..'z').toList()
    println(letters.slice<Char>(0..2))//타입인자를 명시적으로 지정한다
    println(letters.slice(10..13))//컴파일러는 여기서 T가 Char라는 사실을 추론
}
  • 두 호출 결과 타입 모두 List<Char>이며 컴파일러는 반환 타입 List<T>의 T를 자신이 추론한 Char로 치환한다.

클래스나 인터페이스 안에 정의된 메서드, 확장 함수 또는 최상위 함수에서는 수신 객체나 파라미터 타입에 타입 파라미터를 사용할 수 있다. 

fun main(args: Array<String>) {
    println(listOf(1, 2, 3, 4).penultimate)//이 호출에서 T는 Int로 추론
}
val <T> List<T>.penultimate: T //모든 리스트 타입에 이 제네릭 확장 프로퍼티를 사용할 수 있다
    get() = this[size - 2]
  • 제네릭 함수를 정의할 때와 마찬가지 방법으로 제네릭 확장 프로퍼티를 선언할 수 있다.

제네릭 클래스 선언

코틀린에서도 자바와 마찬가지로 타입 파라미터를 넣은 <>를 클래스 이름 뒤에 붙이면 클래스를 제네릭하게 만들 수 있다.

타입 파라미터를 이름 뒤에 붙이고 나면, 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있다

자바 표준 인터페이스인 List

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

class ArrayList<T> : List<T> { //ArrayList의 제네릭 타입 파라미터 T를 List의 타입인자로 넘긴다
    override fun get(index: Int): T {
        TODO("Not yet implemented")
    }
}
  • 제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다
    • 구체적인 타입을 넘길 수도 있고(여기서는 String), 타입 파라미터로 받은 타입을 넘길 수도 있다(T)
  • StringList 같은 경우 String 기반 타입의 타입 인자를 지정하였으므로 하위 클래스에서 상위 클래스에 정의된 함수를 오버라이드 하거나 사용하려면 타입 인자 T를 구체적 타입 String으로 치환해야 한다.
  • ArrayList 클래스는 자신만의 타입 파라미터 T를 정의하면서 그 T를 기반 클래스의 타입 인자로 사용한다.
    • ArrayList<T>의 T와 List<T>의 T와 전혀 다른 타입 파라미터이며, 실제로는 T가 아니라 다른 이름을 사용해도 의미에는 아무 차이가 없다.

Comparable 인터페이스

 

Comparable을 구현하 String클래스

  • String 클래스는 제네릭 Comparable 인터페이스를 구현하면서 그 인터페이스의 타입 파라미터 T로 String 자신을 지정한다

타입 파라미터 제약

타입 파라미터 제약은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다. SUM 같은 경우 Int, Double에는 적용돼야 하지만, String에는 적용되면 안 되기 때문에 제약을 거는 것이다.

어떤 타입을 제네릭 타입의 타입 파라미터에 대한 상한으로 지정하면 그 제네릭 타입을 인스턴스화 할 때 사용하는 타입 인자는 반드시 그 상한 타입의 하위 타입이어야 한다.

제약을 가하기 위해서는 타입 파라미터 이름 뒤에 콜론(:)을 표시하고 그 뒤에 상한 타입을 적으면 된다(자바에서는 extents 키워드를 써서 제약을 걸었다)

//T:타입파라미터, Number:상한
fun <T : Number> List<T>.sum(): T {
    //logic
}
fun main(args: Array<String>) {
    println(oneHalf(3))
}
fun<T: Number> oneHalf(value: T): Double{ //Number를 타입 파라미터 상한으로 정한다
    return value.toDouble() / 2.0 //Number 클래스에 정의된 메서드를 호출
}
  • 타입 파라미터 T에 대한 상한을 정하고 나면 T 타입의 값을 그 상한 타입의 값으로 취급할 수 있다.
fun main(args: Array<String>) {
    println(max("kotlin", "java"))//문자열은 알파벳순으로 비교된다
}
fun <T: Comparable<T>> max(first: T, second: T): T { //이 함수의 인자들은 비교 가능해야 한다
    return if(first > second) first else second
}
  • 두 파라미터 사이에서 더 큰 값을 찾는 제네릭 함수를 작성했다. 
  • T의 상한 타입은 Comparable<T>이다. 
    • max를 비교할 수 없는 값 사이에 호출하면 컴파일 오류가 난다
  • first > second는 관례상 first.compareTo(secone) > 0으로 컴파일된다. 
  • max 함수에서 first의 타입 T는 Comparable<T>를 확장하므로 first를 다른 T 타입 값인 second와 비교할 수 있다
fun main(args: Array<String>) {
    val helloWorld = StringBuilder("Hello World")
    ensureTrailingPeriod(helloWorld)
    println(helloWorld)
}

fun <T> ensureTrailingPeriod(seq: T)
    where T : CharSequence, T : Appendable{ //타입 파라미터 제약 목록
        if(!seq.endsWith('.')){//CharSequence 인터페이스의 확장 함수를 호출
            seq.append('.')//Appendable 인터페이스의 메서드를 호출
        }
    }
  • 타입 파라미터에 둘 이상의 제약을 가해야 하는 경우 위와 같은 문법을 사용한다.
  • 이 예제는 타입 인자가 CharSequence와 Appendable 인터페이스를 반드시 구현해야 한다는 사실을 표현
    • 데이터에 접근하는 연산, 데이터를 변환하는 연산을 T 타입 값에 수행할 수 있다는 의미

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

제네릭 클래스나 함수를 정의하고 그 타입을 인스턴스화 할 때는 널이 될 수 있는 타입을 포함하는 어떤 타입으로 타입 인자를 지정해도 타입 파라미터를 치환할 수 있다. 아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?를 상한으로 정한 파라미터와 같다.

class Processor<T> {
    fun process(value: T){
        value?.hashCode() //value는 널이 될 수 있다.따라서 안전한 호출을 해야한다
    }
}
fun main(args: Array<String>) {
    val nullableStringProcessor = Processor<String?>()//널이 될수 있는 타입인 String?이 T를 대신한다
    nullableStringProcessor.process(null)//null이 value 인자로 지정된다
}
  • process 함수에서 value 파라미터의 타입 T에는 물음표(?)가 붙어 있지 않지만 실제로는 T에 해당하는 타입 인자로 널이 될 수 있는 타입을 넘길 수도 있다
  • processor 클래스를 널이 될 수 있는 타입을 사용해 인스턴스화 하였다
class Processor<T : Any> { //null 이 될 수 없는 타입 상한을 지정한다
    fun process(value: T){
        value.hashCode() //T 타입의 value는 null 이 될 수 없다
    }
}
  • 항상 널이 될 수 없는 타입만 인자로 받게 만들려면 타입 파라미터에 제약을 가하면 된다.
    • 널 가능성을 제외한 아무런 제약도 필요 없다면 Any? 대신 Any를 상한으로 사용하면 된다.
  • <T: Any> 제약은 T 타입이 항상 널이 될 수 없는 타입이 되게 보장한다
  • 컴파일러는 타입 인자인 String?가 Any의 자손 타입이 아니므로 Processor<String?> 와 같은 코드에 오류를 발생시킨다.
728x90