제네릭 (Generics)
타입을 명시하지 않고 함수나 클래스를 정의할 수 있는 기술
타입 파라미터
타입 파라미터를 통해 타입을 제너릭하게 선언하고 나중에 인스턴스화될 때 구체적인 타입으로 치환
<> 안에 선언
타입의 상한 (upper)
- 타입 파라미터는 그냥 선언하면 Any?의 하위 타입이 올 수 있는 것으로 생성
- 그러나 nullable하지 않은 값을 넣고 싶거나 Comparable한 클래스만 치환되어서 함수나 클래스 내부에서 비교를 하고 싶은 경우 등, 타입의 파라미터의 상한을 설정하여 해당 클래스의 하위 클래스만 치환될 수 있도록 선언할 수 있다.
fun <T: Comparable<T>> max(first:T, second:T):T
- 상한은 where을 이용해서 여러 개 설정할 수 있다.
fun <T> maxSerializable(first: T, second: T): T where T: Comparable<T>, T: Serializable
타입 소거
- 타입 파라미터는 런타임에 소거된다.
- < T > 가 < Int > 로 치환되었어도 이 <> 자체가 사라진다.
- List< Int > 가 컴파일 후에는 List 로 되는 것
- 왜 소거되는 것일까?
- 컴파일 타임에 이미 타입 검사를 했기에 런타임에는 할 필요가 없어서 삭제
- 타입 저장 메모리 사용량을 줄일 수 있다는 장점
- 제너릭이 없던 자바의 하위 버전 호환을 위해 런타임에 삭제
- 지워버리면 나중에 Int로 선언된 리스트에 String을 넣어도 모르는 거 아닌가요?
reified와 inline
- 타입 파라미터는 소거되기 때문에 함수 본문에서 is T 처럼 타입 검사를 할 수가 없다.
- 하지만, 타입 검사를 하고 싶을 때 실체화한 타입 파라미터를 살려놓을 수 있다.
- 방법은 바로 inline!
- inline은 함수와 전달받은 람다의 바이트코드를 호출한 함수의 본문에 복사해주는 키워드
- 원래는 람다는 무명객체가 계속 생성되는 것을 줄이기 위해서 사용
- 그러나 여기서는 타입을 살려놓기 위해 사용
- inline으로 가능한 이유
- inline 키워드로 바이트코드를 복사하고자 할 때 어떤 타입을 사용하는 지 알기 때문에 reified 키워드가 붙어있으면 그 타입 자체를 지우기 않고 같이 복사해 넣는다.
변성 (variance)
List< Int >, List< String > 처럼 base type은 같고 타입 인자만 다른 타입의 관계를 설명하는 개념
무공변성 (invariant)
List< Int >, List< String >, 등등 List< T > 에서 파생된 여러 타입들은 서로 관계가 없다.
공변성 (covariant)
그러나 타입 인자의 상하 관계를 제네릭 클래스에서도 유지되었으면 하는 경우도 존재.
ex. Int -> Number일 때, List< Int > -> List< Number > 관계 유지
예를 들어
open class Animal
class Dog : Animal()
class Cat : Animal()
구조로 이루어져 있고
fun main() {
val dogs = mutableListOf<Dog>()
processAnimal(dogs)
}
- 다음과 같이 Dog를 가지는 list를 만들었을 때
fun processAnimal(animals: MutableList<Animal>) {
animals.add(Cat())
}
- 다음과 같이 다른 하위 클래스를 add하게 되면서 list의 타입 안정성이 깨지는 문제가 발생할 수 있다.
- 따라서, 함수 매개변수처럼 소비할 수 있는 부분에 타입 파라미터가 오지 못하도록 제한하는 것
- 소비하는 부분에는 Dog보다 상위 타입만 올 수 있음
반공변성 (contravariance)
- in 위치에서 타입 파라미터를 쓰는 경우
- 타입 인자간의 상하 관계가 제너릭 클래스에 대해서는 뒤집히는 것처럼 보이기 때문에 반공변성이라고 한다.
- 즉, 해당 클래스는 타입 파라미터를 소비(consume)만 할 수 있는 것
- MutableList< Int >에 MutableList< Number > 만 들어갈 수 있는 것
- 위 Animal 예시를 다시 가져와 보면
fun processAnimal(animals: MutableList<Animal>) {
animals.add(Animals())
}
fun getAnimal(): Dog {
return animals.last()
}
- 위 상황에서 List< Dog >에 상위타입인 Animal()가 중간에 들어갔을 때
- getAnimal로 Dog로 반환했지만 Animal이 나오면서 타입이 맞지 않아서 에러가 발생할 수 있다.
- 따라서, in 키워드가 붙으면 반환 타입처럼 생산되는 위치에는 타입 파라미터가 올 수 없다.
선언 지점 변성, 사용지점 변성
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
for(item in source) {
destination.add(item)
}
}
in, out 위치
- 아웃 위치
- 인 위치
- 인도 아웃도 아님
- 생성자 파라미터, private 메서드 파라미터
- in, out을 나누는 이유가 외부 클래스 사용자가 잘못 사용하는 경우를 막기 위해서이기 때문에 나중에 호출될 수 없는 생성자나 private으로 외부 사용자가 접근 못하는 경우는 인도 아웃도 아니다.
- 그러나 생성자에서 var, val 로 getter, setter를 만드는 경우 var는 in,out 모두에 해당, val 은 out 위치에 해당한다.
스타 프로젝션 (star projection)
- 구체적인 원소의 타입을 알 수 없을 때 사용
- List<*> 로 사용한다.
- 어떤 타입인지 모른다는 것일 뿐 아무거나 다 담아도 된다는 건 아니다 (이건 List<Any?>)
- 타입 인자는 별로 중요하지 않고 base type, 즉 List, Set같은 부분을 비교할 때 사용
출처