• 실체화한 타입 파라미터(reified type parameter)를 사용하면 인라인 함수 호출에서 타입 인자로 쓰인 구체적인 타입을 실행 시점에 알 수 있다.
  • 일반 클래스나 함수의 경우 타입 인자 정보가 실행 시점에 사라지기 때문에 구체적인 타입을 알 수 없다.
  • 선언 지점 변성(declaration-site variance)을 사용하면 기저 타입은 같지만 타입 인자가 다른 두 제네릭 타입이 있을 때, 타입 인자의 상위/하위 타입 관계에 따라 두 제네릭 타입의 상위/하위 타입 관계가 어떻게 되는지 지정할 수 있다.

제네릭 타입 파라미터

  • 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야 한다.
  • 클래스에 타입 파라미터가 여럿 있을 수도 있다.
  • 코틀린 컴파일러는 보통 타입과 마찬가지로 타입 인자도 추론할 수 있다.

제네릭 함수와 프로퍼티

  • 제네릭 함수를 호출할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야 한다.
  • 대부분 컴파일러가 타입 인자를 추론할 수 있다.

  • 제네릭 함수를 정의할 때와 마찬가지 방법으로 제네릭 확장 프로퍼티를 선언할 수 있다.
  • 일반 프로퍼티는 타입 파라미터를 가질 수 없다.

제네릭 클래스 선언

  • 자바와 마찬가지로 타입 파라미터를 넣은 꺽쇠 기호를 클래스 이름 뒤에 붙이면 클래스를 제네릭하게 만들 수 있다.
  • 제네릭 클래스를 확장하는 클래스를 정의하려면, 구체적인 타입을 넘길 수도 있고 타입 파라미터로 받은 타입을 넘길 수도 있다.

타입 파라미터 제약

  • 타입 파라미터 제약: 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능
  • 상한(upper bound): 그 제네릭 타입을 인스턴스화할 때 사용하는 타입 인자는 그 상한 타입이거나 그 상한 타입의 하위 타입이어야 한다.
    • 자바에서는 <T extends Number> T sum(List<T> list)
fun <T: Number> List<T>.sum : T
  • 타임 파라미터에 둘 이상의 제약을 가해야 되는 경우는 약간 다른 구문을 사용한다.
fun <T> ensureTrailingPeriod(seq: T) where T : CharSequence, T : Appendable {  
   // ...  
}

타입 파라미터를 널이 될 수 없는 타입으로 한정

  • 아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?를 상한으로 정한 파라미터와 같다.
  • <T : Any>라는 제약은 T 타입이 항상 널이 될 수 없는 타입이 되게 보장한다.
  • Any를 사용하지 않고 다른 널이 될 수 없는 타입을 사용해 상한을 정해도 된다.

실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터

  • JVM 제네릭스는 타입 소거(type erasure)를 사용해서 구현된다.
  • 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다.

실행 시점의 제네릭: 타입 검사와 캐스트

  • 타입 소거로 인해 코틀린 제네릭 타입 인자 정보는 런타임에 지워진다.
    • 하지만 타입 소거로 전반적인 메모리 사용량이 줄어든다.
  • 이는 제네릭 클래스 인스턴스가 그 인스턴슬르 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않는다는 뜻이다.
  • List<String>List<Int>의 경우, 컴파일러는 두 리스트를 서로 다른 차입으로 인식하지만 실행 시점에 그 둘은 완전히 같은 객체다.
    • 하지만 컴파일러가 타입 인자를 알고 올바른 타입의 값만 각 리스트에 넣도록 보장해준다.
  • 타입 소거로 인해 실행 시점에 타입 인자를 검사할 수 없다.

  • 스타 프로젝션을 통해서 타입 인자를 명시하지 않고 리스트 타입이라는 사실은 확인할 수 있다.

  • is 뿐만 아니라 as 도 마찬가지다.
  • 코틀린 컴파일러는 컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행하게 허용한다.

실체화한 타입 파라미터를 사용한 함수 선언

  • 인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 확인할 수 있다.
  • 인라인 함수에 타입 파라미터를 reified로 지정하면 실행시점에 타입 인자를 확인할 수 있다.
inline fun <reified T> isA(value: Any) = value is T
  • 인라인 함수에는 실체화한 타입 파라미터가 여럿 있거나 실체화한 타입 파라미터와 실체화하지 않은 타입 파라미터가 함께 있을 수도 있다.
  • 인라인 함수는 본문을 구현한 바이트코드를 그 함수가 호출되는 모든 지점에 삽입한다.
    • 따라서 컴파일러는 인라인 함수를 호출하는 각 부분의 정확한 타입 인자를 알 수 있어서 바이트 코드를 삽입할 수 있다.
  • 자바 코드에서는 reified 타입 파라미터를 사용하는 inline 함수를 호출할 수 없다.
  • 성능을 좋게 하려면 인라인 함수의 크기를 계속 관찰해야한다. 함수가 커지면 실체화한 타입에 의존하지 않는 부분을 별도의 일반 함수로 뽑아내는 편이 낫다.

실체화한 타입 파라미터로 클래스 참조 대신

  • 타입 인자를 파라미터로 받는 API에 대한 코틀린 어댑터를 구축하는 경우 실체화한 타입 파라미터를 자주 사용한다.

실체화한 타입 파라미터의 제약

  • 다음과 같은 경우에 실체화한 타입 파라미터를 사용할 수 있다.
    • 타입 검사와 캐스팅(is, !is, as, as?)
    • 코틀린 리플렉션 API(::class)
    • 코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)
    • 다른 함수를 호출할 때 타입 인자로 사용
  • 실체화한 타입 파라미터로 다음과 같은 할 수 없다.
    • 타입 파라미터 클래스의 인스턴스 생성하기
    • 타입 파라미터 클래스의 동반 객체 메소드 호출하기
    • 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
    • 클래스, 프로퍼티, 또는 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기

변성: 제네릭과 하위 타입

  • 변성(variance): List<String>List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념

변성이 있는 이유: 인자를 함수에 넘기기

  • 어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어서 List<Any>대신 List<String>을 넘길 수 없다.
  • 하지만 원소 추가나 변경이 없는 경우에는 List<String>List<Any> 대신 넘겨도 안전하다.
  • 코틀린에서는 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전하지 못한 함수 호출을 막을 수 있다.

클래스, 타입, 하위 타입

  • 타입과 클래스의 차이
    • String이라는 클래스는 String?String 이라는 두 가지 타입을 만들어 낸다.
    • 제네릭 클래스에서는 하나의 클래스가 무수히 많은 타입을 만들어 낼 수 있다.
      • List 클래스는 List<Int>, List<String?>, List<List<String>> 등의 타입을 만들어낼 수 있다.
  • 하위 타입: 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이다.
  • 상위 타입: 하위 타입의 반대
  • 간단한 경우는 하위 타입과 하위 클랙스가 근본적으로 같다.
  • 널이 될 수 있는 타입은 하위 타입과 하위 클래스가 같지 않을 수 있다.
    • 널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입이다.
  • 제네릭 타입
    • 무공변: 제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이에 하위 타입 관계가 성립하지 않는 관계
    • 공변: A가 B의 하위 타입이면 List<A>List<B>의 하위 타입이다.
    • 자바의 모든 클래스는 무공변이다.
    • 코틀린의 MutableList는 무공변이다.
    • 코틀린의 List(읽기 전용)는 공변적이다.

공변성: 하위 타입 관계 유지

  • 타입 안정성을 보장하기 위해 공변적 파라미터는 아웃(out) 위치에만 있어야 한다.
    • 이는 클래스가 T 타입의 값을 생산할 수는 있지만 T 타입의 값을 소비할 수는 없다는 뜻이다.

  • 타입 파라미터 T에 붙은 out 키워드는 다음 두 가지를 함께 의미한다.
    • 공변성: 하위 타입 관계가 유지된다.
    • 사용 제한: T를 아웃 위치에서만 사용할 수 있다.

  • 공변적 파라미터는 다른 타입의 타입 인자로 사용할 수도 있다.

  • 생성자 파라미터는 인이나 아웃 어느 쪽도 아니라는 사실에 유의하라.
  • 하지만 val이나 var 키워드를 생성자 파라미터에 적는다면 게터, 세터를 정의하는 것과 같아서 아웃과 인 위치 모두에 해당한다.
  • 또한 이런 위치 규칙은 외부에서 볼 수 있는 (public, protected, internal) 클래스 API에만 적용할 수 있다.
    • 비공개 메소드의 파라미터는 인도 아웃도 아닌 위치다.
    • 변성 규칙은 클래스 외부의 사용자가 클래스를 잘못 사용하는 일을 막기 위한 것이므로 클래스 내부 구현에는 적용되지 않는다.

반공변성: 뒤집한 하위 타입 관계

공변성반공변성무공변성
Producer<out T>Consumer<in T>MutableList<T>
타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지된다.타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힌다.하위 타입 관계가 성립하지 않는다.
T를 아웃 위치에서만 사용할 수 있다.T를 위치에서만 사용할 수 있다.T를 아무 위치에서나 사용할 수 있다.
  • 클래스나 인터페이스가 어떤 타입 파라미터에 대해서는 공변적이면서 다른 타입 파라미터에 대해서는 반공변적일 수도 있다.
interface Function1<in P, out R> {
	operator fun invoke(p: P) : R
}

사용 지점 변성: 타입이 언급되는 지점에서 변성 지정

  • 선언 지점 변성: 클래스를 선언하면서 변성을 지정하면 그 클래스를 사용하는 모든 장소에 변성 지정자가 영항을 끼친다.
  • 사용 지정 변성: 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시한다.
    • 자바의 와일드카드 타입(? extends? super)
  • 코틀린도 사용 지점 변성을 지원한다.

  • 사용 지점 변성을 사용하면 타입 프로젝션이 일어난다.
  • 위의 예시에서 source는 일반적인 MutableList가 아니라 MutableList를 프로젝션을 한(제약을 가한) 타입으로 만든다.
    • 컴파일러는 타입 파라미터 T를 함수 인자 타입으로 사용하지 못하게 막는다.

스타 프로젝션: 타입 인자 대신 * 사용

  • 타입 검사와 캐스트에 대해 설명할 때 제네릭 타입 인자 정보가 없음을 표현하기 위해 스타 프로젝션을 사용한다.
  • MutableList<*>MutableList<out Any?>처럼 아웃 프로젝션 타입으로 인식한다.
    • 그 리스트에 담는 값의 타입에 따라서는 리스트를 만들어서 넘겨준 쪽이 바라는 조건을 깰 수도 있기 때문이다.
  • Counsumer<in T>와 같은 반공변 타입 파라미터에 대한 스타 프로젝션은 <in Nothing>과 동등하다.
  • 타입 파라미터를 시그니처에서 전혀 언급하지 않거나 데이터를 읽기는 하지만 그 타입에는 관심이 없는 경우와 같이 타입 인자 정보가 중요하지 않을 때도 스타 프로젝션 구문을 사용할 수 있다.
  • @Suppress: 애노테이션에 붙은 컴파일 경고를 무시하게 만든다.