제네릭 타입 파라미터
- 제네릭스를 사용하면 타입 파라미터를 받는 타입 정의 가능
- 이 변수는 문자열을 담는 리스트다 라고 말할 수 있는 것
- 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야 함
Map<K, V>
-> Map<String, Person>
listOf("Dmitry", "Svetlana")
: List<String>
임을 추론
제네릭 함수와 프로퍼티
- 모든 타입을 저장하는 리스트(제네릭 리스트)를 다룰 수 있기를 원한다.
- 컬렉션을 다루는 라이브러리 함수는 대부분 제네릭 함수
fun <T> List<T>.slice(indices: IntRange) : List<T>
타입파라미터 | 타입파라미터가 수신 객체와 반환 타입에 사용
val letters = ('a'..'z').toList()
println(letters.slice<Char>(0..2)) //[a, b, c] => 타입 인자를 명시적으로 지정
println(letters.slice(10..13)) //[k, l, m, n] => 컴파일러는 여기서 T가 Char라는 사실 추론
- 함수의 타입 파라미터 T가 수신 객체와 반환 타입에 쓰인다. => 수신&반환 타입 모두
List<T>
val <T> List<T>.penultimate: T
get() = this[size-2]
println(listOf(1, 2, 3, 4).penultimate) //3
- 제네릭 함수를 정의할 때와 마찬가지 방법으로 제네릭 확장 프로퍼티를 선언 할 수 있다.
- 위 예제에서 T는 Int로 추론
제네릭 클래스 선언
interface List<T> {
operator fun get(index: Int) : T
//..
}
- 클래스(인터페이스)도 제네릭하게 만들 수 있다.
- T를 인터페이스 안에서 일반 타입처럼 사용 가능
class StringList: List<String> {
override fun get(index: Int): String = ...
}
class ArrayList<T> : List<T> {
override fun get(index: Int): T = ...
}
- 제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다.
StringList
클래스는 구체적인 타입 인자로 String 을 지정해 List를 구현
- 하위 클래스에서 상위 클래스에 정의된 함수를 오버라이드하거나 사용하려면 타입 인자 T를 구체적 타입 String 으로 치환해야 한다.
Comparable
인터페이스를 구현하는 클래스가 이런 패턴의 예다.
Comparable 인터페이스
interface Comparable<T> {
fun compareTo(other: T): Int
}
class String : Comparable<String> {
override fun compareTo(other: String): Int = /*...*/
}
- String 클래스는 제네릭 Comparable 인터페이스를 구현하면서 그 인터페이스의 타입 파라미터 T로 String 자신을 지정한다.
타입 파라미터 제약
- 타입 인자를 제한하는 기능
- ex) sum 함수 : 숫자 타입만을 허용
- 해당 숫자 타입이 상한 타입
- 상한으로 지정하면 그 제네릭 타입을 인스턴스화할 때 사용하는 타입 인자는 반드시 그 상한or하위타입이어야 함
println(listOf(1, 2, 3).sum()) //6
null이 될 수 없는 타입 파라미터
- 상한 타입 지정 X : Any? 와 같다(가장 큰 타입 개념)
<T:Any>
: not null로 지정됨
소거 타입, 실체 파라미터
- 제네릭스는 보통 타입 소거를 사용해 구현
- 타입 소거(type erasure) : 컴파일시에만 존재하고 실행시 타입 없음
- 타입을 갖고 싶다면? inline + reify(실체화)
실행 시점의 제네릭 : 타입 검사와 캐스트
- 실행 시점에
List<String>
와 List<Int>
는 완전히 같은 타입의 객체
- 타입 소거의 한계
List<String>
, List<Int>
가 아닌 리스트야! 라는 것을 알고 싶다면? 스타 프로젝션(List<*>
)을 사용하자
실체 파라미터 : inline + reified
inline fun <refied T> isA(value: Any) = value is T
println(isA<String>("abc")) //true
println(isA<String>(123)) // false
- 타입 인자를 실행 시점에 알 수 있기 때문에
is
가능
- 각 원소가 타입 인자로 지정한 클래스의 인스턴스인지 검사 가능
- reified 키워드는 이 타입 파라미터가 실행 시점에 지워지지 않음을 표시
왜 인라인 함수에서만 실체화가 가능하쥬?🥸
- (8장 인라인 함수 : 람다의 부가 비용 없애기)
- 컴파일러가 인라인 함수의 본문을 구현한 바이트 코드를 그 함수가 호출되는 모든 지점에 삽입
- 인라이닝함으로써 얻는 이익이 더 큰 경우에만 사용하기
- 함수가 계속 커진다면 비추천
inline fun <reified T> loadService() {
return ServiceLoader.load(T::class.java)
}
reified
로 타입 파라미터 실체화
T::class
로 타입 파라미터의 클래스를 가져온다.
reified 타입 파라미터 제약
- 아래에서 사용 가능
is
, !is
, as
, as?
가능
- 리플렉션 API
::class.java
로 클래스 얻기
- 다른 함수를 호출할 때 타입 인자로 사용
- 아래는 안 됨
- 타입 파라미터 클래스의
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- inline이 아닌 함수에 reified로 지정하기
변성: 제네릭과 하위 타입
- 변성 : 같인 기저 타입에 다른 여러 타입들간의 관계를 설명하는 개념
변성이 있는 이유
- 인자를 함수에 넘기기
- 공변성질 :
List<Any>
를 받는 함수에 List<String>
을 넘기는 것은 절대로 안전!
- 반공변성질 :
MutableList<Any>
를 받는 함수에 MutableList<String>
을 넘기는 것은 컴파일 불가능!
- 상위타입->하위타입
- Any가 상위타입인데 String을 Any에게 보내려 하고 있어서 불가능
하위 타입
fun test(i: Int) {
val n: Number = i
fun f(s:String) { /*...*/ }
f(i)
}
- Number > Int(하위)
- Int가 String의 하위 타입이 아니어서 컴파일 불가능
- Int? > Int
- A? > A
Int < Int?
공변성, 반공변성, 무공변성
- 읽기 전용 List 인터페이스는 공변적이다. 따라서
List<String>
은 List<Any>
의 하위 타입이다.
- 함수 인터페이스에서 함수 타입은 함수 파라미터 타입에 대해서는 반공변적이며 함수 반환 타입에 대해서는 공변적이다.
- MutableList는 읽기 쓰기가 가능하기 때문에 무공변적이다.
스타 프로젝션: 타입 인자 대신 * 사용
- 타입 인자 정보가 없음 or 타입 인자 정보가 중요하지 않을 때를 표현
MutableList<Any?>
!= MutableList<*>
MutableList<Any?>
: 모든 타입의 원소 담기 가능
MutableList<*>
: 어떤 정해진 구체적인 타입의 원소를 담는 리스트, 정확한 원소 타입 모름