[Kotlin] 3-7. 지네릭

지네릭

자바와 마찬가지로, 코틀린 클래스는 타입 파라미터를 가질 수 있다

class Box<t>(t: T) {
  var value = t
}

일반적으로 지네릭 클래스의 인스턴스를 생성할 때 타입 인자를 전달해야 한다

val box: Box<Int> = Box<Int>(1)

하지만 파라미터를 추론할 수 있다면 타입 인자를 생략할 수 있다

val box = Box(1)  // 1은 Int 타입이므로 컴파일러는 Box<Int>를 말한다는 것을 알아낼 수 있다

변성

코틀린은 자바의 와일드카드 타입 대신 선언 위치 변성과 타입 프로젝션을 제공

선언 위치 변성

지네릭 인터페이스 Source가 파라미터로 T를 갖는 메서드가 없고 오직 T를 리턴하는 메서드만 있다고 하자

// Java
interface Source<T> {
  T nextT();
}

Source 인스턴스에 대한 레퍼런스를 Source 타입 변수에 저장하는 것은 완전히 안전하다 Consumer 메서드 호출이 없기 때문이다. 하지만 자바는 이를 알지 못하기에 여전히 이를 금지한다

// 자바
void demo(Source<String> strs) {
  Source<Object> objects = strs;  // 자바는 허용하지 않음
  // ...
}

이를 고치려면 Source<? extends Object> 타입의 객체를 선언해야 하는데 이는 의미가 없다
왜냐면 전처럼 변수에 대해 동일 메서드를 호출할 수 있고 이득없이 타입만 더 복잡해지기 때문이다
하지만 컴파일러는 이를 모른다

코틀린에서는 이를 컴파일러에 알려주는 방법을 제공. 이를 선언 위치 변성이라고 부른다
Source의 타입 파라미터 T에 대해 Source의 멤버에서 T를 리턴만 하고 소비하지 않는다는 것을 명시할 수 있다 이를 위해 out수식어 제공

abstract class Source<out T> {
  abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
  val objects: Source<Any> = strs  // 괜찮다. T가 out 파라미터이기 때문이다
  // ...
}

클래스 C의 타입 파라미터 T를 out으로 선언하면, C의 멤버에서 out위치에만 T가 위치할 수 있지만
리턴에서 C가 안전하게 C의 상위타입이 될 수 있다

out수식어를 애노테이션이라 부른다

out외에 코틀린은 추가 공변 애노테이션 in을 제공한다. 이는 파라미터를 반공변으로 만든다
반공변 파라미터는 오직 소비만 할 수 있고 생산할 수는 없다

abstract class Comparable<in T> {
  abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
  x.compareTo(1.0)  // 1.0은 Double타입인데 이는 Number의 하위타입이다
  // 따라서 Comparable<Double> 타입의 변수에 x를 할당할 수 있다
  val y: Comparable<Double>= x // OK

지네릭 함수

클래스뿐만 아니라 함수도 타입 파라미터를 가질 수 있다. 함수 이름 앞에 타입 파라미터를 위치시킴

fun <T> singletonList(item: T): List<T> {
  // ...
}

fun <T> T.basicToString() : String {  // 확장 함수
  // ...
}

지네릭 함수를 호출하려면 호출 위치에서 함수 이름 뒤에 타입 인자를 지정한다

val l = singletonList<Int>(1)