[Kotlin in Action] part9_제네릭스
제네릭 타입 파라미터
제네릭스를 사용하면 타입 파라미터를 사용하는 타입을 정의할 수 있다.
타입 파라미터를 사용하면 구체적으로 원하는 타입을 정의할 수 있다.
코틀린에서는 타입 인자도 추론할 수 있다.
val authors = listOf("Dmitry", "Svetlana")
두 값이 문자열이기 때문에 컴파일러는 여기서 생기는 리스트가 문자열을 담는 리스트라는 것을 추론할 수 있다.
만약 빈 리스트를 만든다고 하면 타입 인자를 추론할 수 없기 때문에 직접 변수의 타입 또는 타입 인자를 지정해야 한다.
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
ㄴ 이 두 선언은 동등하다.
제네릭 함수와 프로퍼티
제네릭 함수는 모든 리스트를 다루는 함수를 만들고 싶을 때 사용한다. 제네릭 함수를 호출할 때는 반드시 구체적인 타입으로 타입 인자를 넘겨야 한다.
제네릭 함수를 선언할 때와 같이 제네릭 확장 프로퍼티를 선언할 수 있다.
제네릭 클래스 선언
타입 파라미터를 넣은 <>를 클래스(or 인터페이스)의 이름 뒤에 붙이면 클래스(or 인터페이스)를 제네릭하게 만들 수 있다.
interface List<T> { //List 인터페이스에 T라는 타입 파라미터를 정의
operator fun get(index: Int): T
// ...
}
제네릭 클래스를 확장하는 클래스(or 구현하는 인터페이스)를 정의하려면 타입 인자를 지정해야 한다.
-> 구체적인 타입을 넘기거나 타입 파라미터로 받은 타입을 넘길 수 있다.
-> 클래스가 자기 자신을 타입 인자로 참조할 수도 있다. 아래의 코드를 보아라.
interface Comparable<T> {
fun compareTo(other: T): Int
}
class String : Comparable<String> {
override fun compareTo(other: String): Int = /* ... */
}
타입 파라미터 제약
타입 파라미터 제약: 클래스나 하수에 사용할 수 있는 타입 인자를 제한하는 기능
예를 들어 sum함수를 생각할 떄 List<Int>나 List<Double>에 적용할 수 있지만 List<String>에는 적용할 수 없다.
제약을 가하려면 타입 파라미터 이름 뒤에 : 를 표시하고 그 뒤에 상한 타입을 적으면 된다.
-> 상한 타입: 제네릭 타입의 타입 파라미터에 대한 제약 타입
만약 함수가 적용할 수 없는 타입 인자에 대해 호출이 되면 컴파일 오류가 난다.
아주 드물지만 타입 파라미터에 대해 둘 이상의 제약을 가해야 하는 경우도 있다.
타입 파라미터를 널이 될 수 없는 타입으로 한정
항상 널이 될 수 없는 타입만 타입 인자로 받게 만들려면 타입 파라미터에 제약을 가해야 한다.
예를 들어
널이 될 수 없는 타입을 보장하려면 <T:Any?>가 아닌 <T:Any>를 상한으로 사용하면 된다.
이 타입은 T타입이 항상 널이 될 수 없는 타입이 되게 보장하기 때문이다.
실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터
실행 시점의 제네릭: 타입 검사와 캐스트
코틀린 제네릭 타입 인자 정보는 런타임에 지워진다. 정보를 유지하지 않는다.
따라서 실행 시점에 컴파일러는 리스트가 어떤 타입으로 선언됐는지 알 수 없다.
또 다른 문제는 타입 소거로 인해 생긴다. 타입 인자를 저장하지 않기 때문에 실행 시점의 타입 인자는 검사 불가능하다.
ㄴ is 검사에서 타입 인자로 지정한 타입을 검사할 수는 없다.
ㄴ 실행 시점에 어떤 값이 List인지는 알 수 있지만 List의 타입은 알 수 없다는 것이다.
: 근데 List인지는 어떻게 앎? -> 스타 프로젝션을 사용해서 알 수 있다. (9장에서 설명)
실체화한 타입 파라미터를 사용한 함수 선언
대개의 경우에 제네릭 함수의 타입 인자는 알아낼 수 없다. 실행 시점에 지워지기 때문이다.
그러나 하나의 예외가 있다.
-> 인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있다.
>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T
//이 함수를 인라인 함수로 만들고 타입 파라미터를 reified로 지정
-> 이렇게 하면 value의 타입이 T의 인스턴스인지 실행 시점에 검사할 수 있다.
inline fun <reified T> isA(value: Any) = value is T
>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false
실체화한 타입 파라미터의 제약
실체화한 타입 파라미터에는 제약이 있다.
▼ 아래의 경우에 실체화한 타입 파라미터를 사용할 수 있다.
- 타입 검사와 캐스팅(is, !is, as, as?)
- 10장에서 설명할 코틀린 리플렉션 API
- 코틀린 함수에 대응하는 java.lang.Class를 얻기
- 다른 함수를 호출할 때 타입 인자로 사용.
▼ but, 아래의 경우에는 실체화한 타입 파라미터를 사용할 수 없다.
- 타입 파라미터 클래스의 인스턴스 생성하기
- 타입 파라미터 클래스의 동반 객체 메서드 호출하기
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified 로 지정하기
변성: 제네릭과 하위 타입
변성: 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념.
-> 변성을 잘 활용하면 사용하기 편하고 타입 안전성을 보장하는 API를 만들 수 있다.
변성이 있는 이유: 인자를 함수에 넘기기
예를 들어서 String 클래스는 Any를 확장하기 때문에 Any 타입 값을 받는 파라미터에 String값을 넘겨도 안전하다.
하지만 Any와 String이 List 인터페이스의 타입 인자로 들어갈 때는 안전하지 않다.
-> List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘길 수 없다.
리스트의 원소를 추가하거나 변경하면 타입 불일치가 생길 수도 있기 때문.
(그렇지 않은 경우에는 안전할 수도 있다.)
클래스, 타입, 하위 타입
타입과 클래스의 차이.
제네릭 클래스가 아닌 클래스에서는 클래스 이름을 타입으로 쓸 수 있다.
하지만 클래스 이름을 널이 될 수 있는 타입에도 쓸 수 있다. -> 모든 코틀린 클래스가 최소 둘 이상의 타입을 구성 가능
제네릭 클래스의 경우: 올바른 타입을 얻으려면 타입 인자가 구체적이어야 한다.
하위 타입: 타입 a의 값이 필요한 곳에 타입 b를 넣어도 된다면 타입 b는 타입a의 하위 타입.
상위 타입: 그 반대
컴파일러는 변수 대입 또는 함수 인자 전달 시 항상 하위 타입 검사를 한다.
fun test(i: Int) {
val n: Number = i //Int가 Number의 하위 타입이라서 컴파일된다.
fun f(s: String) { /*...*/ }
f(i) //Int가 String의 하위 타입이 아니라서 컴파일 되지 않는다.
}
간단한 경우에 하위 타입은 하위 클래스와 같다.
하지만 널이 될 수 있는 타입의 경우, 하위 타입과 하위 클래스는 같지 않다.
-> 널이 될 수 없는 타입의 값을 널이 될 수 있는 타입의 변수에 저장 : 가능
-> 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 변수에 저장 : 불가능
공변성: 하위 타입 관계를 유지
공변적: a가 b의 하위 타입이면 List<A>는 List<B>의 하위 타입이다. 이러한 클래스나 인터페이스는 공변적.
코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 앞에 out을 넣어야 함.
interface Producer<out T> { //클래스가 T에 대하여 공변적이라고 선언
fun produce(): T
}
-> 그러면 타입 파라미터와 타입 인자의 타입이 정확히 같지 않아도 함수 인자나 반환값으로 사용할 수 있다.
=> 공변적인 클래스로 만들면 코드를 적절히 바꿀 수 있다.
but, 모든 클래스를 공변적으로 만들 수는 없다. 타입 파라미터를 공변적으로 지정하면 파라미터의 사용법이 제한된다.
-> 타입 파라미터를 사용할 수 있는 지점은 모두 in과 out위치로 나눈다.
타입 파라미터 T가 함수의 반환 타입으로 사용된다면 T는 아웃 위치에 있다. -> T타입의 값을 생산
타입 파라미터 T가 함수의 파라미터 타입으로 쓰인다면 T는 인 위치에 있다. -> T타입의 값을 소비
반공변성: 뒤집힌 하위 타입 관계
반공변성: 타입b가 타입a의 하위 타입인 경우 Consumer<a>가 Consumer<b>의 하위 타입이다.
반공변성의 경우에도 공변성과 마찬가지로 in 키워드를 타입인자에 붙이면 그 타입 인자를 오직 인 위치에서만 사용 가능.
-> 어떤 클래스나 인터페이스가 어떤 타입 파라미터에 대해서는 공변적이면서 다른 타입 파라미터에 대해서는 반공변적일 수가 있다.
-> 클래스 정의에 변성을 직접 적으면 그 클래스를 사용하는 모든 곳에 변성이 적용된다.
사용 지점 변성: 타입이 언급되는 지점에서 변성 지정
위에 언급했듯이 클래스 정의에 변성을 직접 적으면 그 클래스를 사용하는 모든 곳에 변성이 적용된다
ㄴ 선언 지점 변성이라 부른다.
-> 자바에서는 타입 파라미터가 있는 타입을 사용할 때마다 어떤 타입으로 대치할 수 있는지 명시해야 한다.
ㄴ 사용 지점 변성이라 부른다.
ㄴ 코틀린도 이를 지원한다.
따라서 변성을 선언할 수 없는 경우에도 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있다.
스타 프로젝션: 타입 인자 대신 * 사용
스타 프로젝션: 제네릭 타입 인자 정보가 없음을 표현
-> 원소 타입이 알려지지 않은 리스트: List<*>
=> 제네릭 클래스의 타입 인자가 어떤 타입인지 정보가 없거나 타입 인자가 어떤 타입인지 중요하지 않을 때 스타 프로젝션 구문을 사용할 수 있다.