-
[kotlin in action] 제네릭스프로그래밍 언어/kotlin 2021. 8. 25. 02:17728x90반응형
타입 파라미터 간에는 상/하위 관계가 없고 raw-type 간에만 상/하위 관계가 존재한다.
Integer -> Number (O)
List<Number> -> List<Integer>List<Integer> -> ArrayList<Integer> (O)
실체화한 타입 파라미터
실체화한 타입 파라미터를 사용하면 인라인 함수 호출에서 타입 인자로 쓰인 구체적인 타입을 실행 시점에 알 수 있다.
선언 지점 변성
기저 타입(=제네릭 타입에서 타입 파라미터를 제외한 부분)은 같지만 타입 인자가 다른 두 제네릭타입 Type<A> 와 Type<B>가 있을 때 타입 인자 A와 타입 인자 B의 상위/하위 타입 관계에 따라 두 제네릭 타입의 상위/하위 타입 관계가 어떻게 되는지 지정할 수 있다.
사용 지점 변성
제네릭 타입 값 사이의 상위/하위 타입 관계 지정을 제네릭 타입 값을 사용하는 위치에서 파라미터 타입에 대한 제약을 표시하는 방식으로 달성한다. 자바의 와일드카드, 코틀린 선언 지점 변성과 같은 역할.
9.1 제네릭 타입 파라미터
val authors = listOf("Dmitry", "Svetlana") // 타입 추론 val readers: MutableList<String> = mutableListOf() val readers2 = mutableListOf<String>()
코틀린에서는 자바와 달리 제네릭 타입의 타입 인자를 프로그래머가 명시하거나 컴파일러가 추론할 수 있어야 한다.
제네릭 함수와 프로퍼티
- 제네릭 함수 선언
fun <T> List<T>.slice(indices: IntRange) : List<T>
위 함수를 구체적인 리스트에 대해 호출할 때 타입을 명시할 수 있다. 하지만 대부분 컴파일러가 추론할 수 있어서 명시할 필요가 없는 경우가 많다.
fun <T> List<T>.filter(predicate: (T) -> Boolean) : List<T> >>> readers.filter { it !in authors } // 컴파일러는 it 가 String 이라는 사실을 추론한다.
클래스나 인터페이스 안에 정의된 메소드, 확장 함수 또는 최상위 함수에서 타입 파라미터를 선언할 수 있다.
확장 함수에서는 수신 객체나 파라미ㅓ 타입에 타입 파라미터를 사용할 수 있다.
- 제네릭 확장 프로퍼티 선언
val <T> List<T>.penultimate: T // 모든 리스트 타입이 이 제네릭 확장 프로퍼티를 사용할 수 있다. get() = this[size - 2] >>> println(listOf(1, 2, 3, 4).penultimate)
일반 프로퍼티는 타입 파라미터를 가질 수 없다.
제네릭 클래스 선언
제네릭 클래스를 확장하는 클래스 정의 or 제네릭 인터페이스 구현하는 클래스 정의 하려면 구체적인 타입 넘길수도 있고, 타입 파라미터로 받은 타입을 넘길 수도 있다.
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 = ... }
하위클래스에서 상위 클래스에 정의된 함수를 오버라이드하거나 사용하려면 타입인자 T를 구체적인 타입으로 치환해야한다.
타입 파라미터 제약
타입 파라미터 뒤에 상한을 지정함으로써 제약을 정의할 수 있다.
fun <T : Number> List<T>.sum() : T
타입 파라미터를 널이 될 수 없는 타입으로 한정
아무런 상한도 지정하지 않은 타입 파라미터 -> Any? 가 상환
항상 널이 될 수 없는 타입만 인자로 받으려면 Any 를 상한으로 지정
9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터
JVM의 제네릭스는 보통 타입 소거를 사용해 구현된다. (
'보통' 이면 보통이 아닌 경우도 있다는 건가?)실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻이다.
함수를 inline으로 선언하므로써 이런 제약을 우회할 수 있다. 타입 인자가 지워지지 않게 할 수 있음(=실체화)
실행 시점의 제네릭: 타입 검사와 캐스트
제네릭 클래스 인스턴스가 그 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않음.
실행 시점에 해당 인스턴스가 어떤 타입의 원소를 저장하는지 알 수 없다.
인자를 알 수 없는 제네릭을 사용할 때 스타 프로젝션을 사용한다.
if (value is List<*>) { ... }
as 나 as? 캐스팅에도 제네릭 타입을 사용할 수 있지만, 기저 클래스가 같고 타입 인자가 다른 타입으로 캐스팅해도 여전히 캐스팅에 성공한다. 컴파일러가 경고는 하지만, 예상대로 작동하긴 한다.
fun printSum(c: Collection<*>) { val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected") println(intList.sum()) } fun main(args: Array<String>) { printSum(listOf(1, 2, 3)) }
만약, 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 ClassCastException이 발생한다.
fun main(args: Array<String>) { printSum(listOf("a", "b", "c")) // ClassCastException: String cannot be cast to Number }
정수타입인자를 가진 List에 문자열 리스트를 전달하면 타입 인자를 알수가 없으므로 IllegalArgumentException이 발생하지는 않는다. 따라서 as? 캐스트는 성공하고, 실행 도중 예외가 발생한다.
다음과 같이 컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행하게 허용할 수 있다.
fun printSum(c: Collection<Int>) { // 컴파일 시점에 c컬렉션이 Int값을 저장한다는걸 안다. if (c is List<Int>) { println(c.sum()) } } fun main(args: Array<String>) { printSum(listOf(1, 2, 3)) }
코틀린 컴파일러는 안전하지 못한 is 검사를 금지하고 위험한 as 캐스팅은 경고를 출력한다.
실체화한 타입 파라미터를 사용한 함수 선언
제네릭 함수가 호출되어도 그 함수의 본문에서는 호출 시 쓰인 타입 인자를 알 수가 없다.
하지만 inline 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있다.
inline 함수로 만들고 타입 파라미터를 reified로 지정하면 value의 타입이 T의 인스턴스인지를 실행 시점에 검사할 수 있다.
inline fun <reified T> isA(value: Any) = value is T >>> println(isA<String>("abc")) // true
코틀린 표준 라이브러리 함수의 대표적인 예로는 filterIsInstance 가 있다. 인자로 받은 컬렉션의 원소 중에서 타입 인자로 지정한 클래스의 인스턴스만을 모아서 만든 리스트를 반환한다.
fun main(args: Array<String>) { val items = listOf("one", 2, "three") println(items.filterIsInstance<String>()) }
public 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 }
이는, 인라인 함수의 본문을 컴파일러는 함수가 호출되는 지점에 삽입한다. 타입 파라미터가 아닌 구체적인 타입을 사용하므로 만들어진 바이트코드는 실행 시점에 벌어지는 타입 소거의 영향을 받지 않는다.
실체화한 타입 파라미터로 클래스 참조 대신
java.lang.Class 타입 인자를 파라미터로 받는 API에 대한 코틀린 어뎁터를 구축하는 경우
ServiceLoader를 예로 들어보자. 이는 어떤 추상 클래스나 인터페이스를 표현하는 java.lang.Class를 받아서 그 클래스나 인스턴스를 구현한 인스턴스를 반환한다.
val serviceImpl = ServiceLoader.load(Service::class.java)
::class.java 는 코틀린 클래스에 대응하는 java.lang.Class 참조를 얻는 방법이다. (자바에서 쓰는 클래스와 코틀린에서 쓰는 클래스가 다르기 때문에, 자바 클래스에 대한 참조를 얻는 것이다. 리플렉션)
구체화한 타입 파라미터를 사용해서 다시 작성해보면,
val serviceImpl = loadService<Service>()
loadService 함수는, 읽어들일 서비스 클래스를 타입 인자로 가진다.
inline fun <reified T> loadService() { return ServiceLoader.load(T::class.java) }
안드로이드의 startActivity 함수 -> 액티비티의 클래스를 java.lang.Class로 전달하는 대신, 실체화한 타입 파라미터 사용 가능
inline fun <reified T : Activity> Context.startActivity() { val intent = Intent(this, T::class.java) startActivity(intent) } startActicity<DetailActivity>()
실체화한 타입 파라미터의 제약
실체화의 개념으로 인해 생기는 제약 / 코틀린이 실체화를 구현하는 방식에 의해 생기는 제약 존재.
다음과 같은 경우에 실체화된 타입 파라미터를 사용한다.
- 타입 검사와 캐스팅
- 코틀린 리플렉션 API
- 코틀린 타입에 대응하는 java.lang.Class를 얻기
- 다른 함수를 호출할 때 타입 인자로 사용
다음은 불가능 하다.
- 타입 파라미터의 클래스 의 인스턴스 생성
- 타입 파라미터의 클래스 의 동반 객체 메소드 호출
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정
728x90'프로그래밍 언어 > kotlin' 카테고리의 다른 글
[kotlin in action] 제네릭스와 변성 (0) 2021.08.25 [kotlin in action] 연산자 오버로딩과 기타 관례 (0) 2021.08.13 [Kotlin In Action] 코틀린 타입 시스템 (0) 2021.08.06 [Kotlin in Action] 람다로 프로그래밍 (0) 2021.07.26 [Kotlin in action] 클래스, 객체, 인터페이스 (0) 2021.07.25