코틀린 언어 용어 사전
제네릭out · in · 공변 · 반공변 · 스타 프로젝션

변성

제네릭 타입 사이의 상속 관계를 정하는 규칙. out은 생산자, in은 소비자에 붙인다.

StringAny의 하위 타입이어도 List<String>List<Any>의 하위 타입인지를 정하는 규칙.

왜 기본이 불공변인가

val strs: MutableList<String> = mutableListOf()
val anys: MutableList<Any> = strs     // 만약 허용된다면
anys.add(42)                          // Int 를 넣을 수 있다
val s: String = strs[0]               // 꺼내면 터진다

그래서 MutableList는 불공변이다. 대입 자체를 금지해 타입 안전을 지킨다.

out — 생산자는 공변

interface Producer<out T> {
    fun produce(): T          // T 를 '반환' 만 한다
    // fun consume(item: T)   // 컴파일 에러 — T 를 인자로 못 받는다
}

val p: Producer<Any> = producerOfString    // OK

꺼내기만 한다면 상위 타입으로 받아도 안전하다 Producer<String> 에서 꺼낸 것은 어차피 Any 이기도 하다

코틀린의 List<out E> 가 그래서 공변이다

  • val l: List<Any> = listOf("a") — // 읽기 전용이라 안전하다

in — 소비자는 반공변

interface Consumer<in T> {
    fun consume(item: T)      // T 를 '받기' 만 한다
    // fun produce(): T       // 컴파일 에러
}

val c: Consumer<String> = consumerOfAny     // OK — Any 를 받는 놈은 String 도 받는다
// Comparable<in T> 가 이 구조다
val comp: Comparable<Double> = object : Comparable<Number> {
    override fun compareTo(other: Number) = 0
}

외우는 문장

  • out — → 생산자(Producer) → 반환 위치에만 → 공변
  • in — → 소비자(Consumer) → 인자 위치에만 → 반공변

자바의 ? extends / ? super 와 같은 개념이지만 코틀린은 '선언 지점' 에 한 번 적으면 된다 (자바는 쓸 때마다 적는다)

사용 지점 변성도 있다

fun copy(from: Array<out Any>, to: Array<Any>) { ... }   // 이 자리에서만 공변으로

스타 프로젝션 — 타입을 모를 때

fun printSize(list: List<*>) = println(list.size)   // 타입은 모르지만 크기는 안다

// 읽으면 Any? 로 나온다. 쓰는 것은 불가능하다

타입 소거와 reified

if (list is List<String>) { }        // 컴파일 에러 — 런타임에 타입이 없다
if (list is List<*>) { }             // 이건 가능

inline fun <reified T> isType(x: Any) = x is T   // inline + reified 면 가능

면접 함정

  • "코틀린 List는 불변이라 공변"읽기 전용이라 공변이다. 불변과는 다른 이야기다.
  • "out을 붙이면 아무 데나 쓸 수 있다" → 인자 위치에는 못 쓴다. 컴파일러가 막는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 제네릭·변성 — in/out·스타 프로젝션·타입 소거