String이 Any의 하위 타입이어도 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을 붙이면 아무 데나 쓸 수 있다" → 인자 위치에는 못 쓴다. 컴파일러가 막는다.