변성(variance) 개념은 List<String>와 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념이다. 일반적으로 이런 관계가 왜 중요한지 먼저 설명한 다음에 코틀린에서 변성을 어떻게 표시하는지 살펴본다. 직접 제네릭 클래스나 함수를 정의하는 경우 변성을 꼭 이해해야한다. 변성을 잘 활용하면 사용에 불편하지 않으면서 타입 안정성을 보장하는 API를 만들 수 있다.
9.3.1 변성이 있는 이유: 인자를 함수에 넘기기
List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까? String 클래스는 Any를 확장하므로 Any 타입 값을 파라미터로 받는 함수에 String 값을 넘겨도 절대로 안전하다. 하지만 Any와 String이 List 인터페이스의 타입 인자로 들어가는 경우 그렇게 자신 있게 안전성을 말할 수 없다.
fun printContents(list: List<Any>) {
println(list.joinToString())
}
>>> printContents(listOf("abc", "bac"))
abc, bac
이 경우에는 문자열 리스트도 잘 작동한다. 이 함수는 각 원소를 Any로 취급하며 모든 문자열은 Any 타입이기도 하므로 안전하다.
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
이 함수에 문자열 리스트를 넘기면 좋지 않다.
>>> val Strings = mutableListOf("abc", "bac")
>>> addAnswer(strings) // 이 줄이 컴파일 된다면,
>>> println(strings.maxBy { it.length }) // 실행 시점에 예외가 발생할 것이다.
ClassCastException: Integer cannot be cast to String
Mutable<String> 타입의 strings 변수를 선언해서 함수에 넘긴다. 컴파일러가 이 식을 받아들인다면 정수를 문자열 리스트 뒤에 추가할 수 있다. 따라서 이 함수 호출은 컴파일 될 수 없다. 이 예제는 MutableList<Any>가 필요한 곳에 MutableList<String>을 넘기면 안 된다는 사실을 보여준다. 코틀린 컴파일러는 실제로 이런 함수 호출을 금지한다.
이제 List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전한가라는 질문에는 no라고 답할 수 있다. 어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어서 List<Any> 대신 List<String>을 넘길 수 없다. 하지만 원소 추가나 변경이 없는 경우에는 List<String>을 List<Any> 대신 넘겨도 안전하다. 코틀린에서는 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전하지 못한 함수 호출을 막을 수 있다. 함수가 읽기 전용 리스트를 받는다면 더 구체적인 타입의 원소를 갖는 리스트를 그 함수에 넘길 수 있다. 하지만 리스트가 변경 가능하다면 그럴 수 없다.
9.3.2 클래스, 타입, 하위 타입
지금까지는 타입과 클래스라는 용어를 자주 혼용해 왔다. 하지만 실제로 그 둘은 같지 않다.
제네릭 클래스가 아닌 클래스에서는 클래스 이름을 바로 타입으로 쓸 수 있다. 예를 들어 var x: String이라고 쓰면 String 클래스의 인스턴스를 저장하는 변수를 정의할 수 있다. 하지만 var x: String?처럼 같은 클래스 이름을 null이 될 수 있는 타입에도 쓸 수 있다는 점을 기억하자. 이는 모든 코틀린 클래스가 적어도 둘 이상의 타입을 구성할 수 있다는 뜻이다.
제네릭 클래스에서는 상황이 더 복잡하다. 올바른 타입을 얻으려면 제네릭 타입의 타입 파라미터를 구체적인 타입 인자로 바꿔줘야 한다. 예를 들어,List는 타입이 아니다(하지만 클래스다). 하지만 타입 인자를 치환한 List<Int>, List<String?>, List<List<String>> 등은 모두 제대로 된 타입이다. 각각의 제네릭 클래스는 무수히 많은 타입을 만들어낼 수 있다.
타입 사이의 관계를 논하기 위해 하위 타입(subtype)이라는 개념을 잘 알아야한다. 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이다. 예를 들어, Int는 Number의 하위 타입이지만 String의 하위 타입은 아니다. 이 정의는 모든 타입이 자신의 하위 타입이라는 뜻이기도 하다.
상위 타입(supertype)은 하위 타입의 반대다. A 타입이 B 타입의 하위 타입이라면 B는 A의 상위 타입이다. A 타입이 B 타입의 하위 타입이라면 B 타입은 A의 상위 타입이다.
// 어떤 타입이 다른 타입의 하위 타입인지 검사하기
fun test(i: Int) {
val n: Number = i // Int가 Number의 하위 타입이어서 컴파일된다.
fun f(s: String) { /* ... */ }
f(i) // Int가 String의 하위 타입이어서 컴파일되지 않는다.
}
간단한 경우 하위 타입은 하위 클래스(subclass)와 근본적으로 같다. 예를 들어, Int 클래스는 Number의 하위 클래스이므로 Int는 Number의 하위 타입이다. String은 CharSequence의 하위 타입인 것처럼 어떤 인터페이스를 구현하는 클래스의 타입은 그 인터페이스 타입의 하위 타입이다.
아래는 null이 될 수 있는 타입은 하위 타입과 하위 클래스가 같지 않은 경우를 보여주는 예다.
제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어가고, 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변(invariant)이라고 한다. MutableList를 예로 들면 A와 B가 서로 다르기만 하면 MutableList<A>는 항상 MutableList<B>의 하위 타입이 아니다. 자바에서는 모든 클래스가 무공변이다.
코틀린의 List 인터페이스는 읽기 전용 컬렉션을 표현한다. A가 B의 하위 타입이면 List<A>는 List<B>의 하위 타입이다. 그런 클래스나 인터페이스를 공변적(covariant)이라 한다.
9.3.3 공변성: 하위 타입 관계를 유지
Producer<T>를 예로 공변성 클래스를 설명해보자. A가 B의 하위 타입일 때 Producer<A>가 Producer<B>의 하위 타입이면 Producer는 공변적이다. 이를 하위 타입 관계가 유지된다고 말한다.
코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 넣어야 한다.
interface Producer<out T> { // 클래스가 T에 대해 공변적이라고 선언한다.
fun produce(): T
}
클래스의 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스으 인스턴스를 함수 인자나 반환 값으로 사용할 수 있다. 예를 들어, Herd 클래스로 표현되는 동물 무리의 사육을 담당하는 함수가 있다고 생각해보자. Herd 클래스의 타입 파라미터는 그 떼가 어떤 동물 무리인지 알려준다.
// 무공변 컬렉션 역할을 하는 클래스 정의하기
open Class Animal {
fun feed() { ... }
}
class Herd<T : Animal> { // 타입 파라미터를 무공변성으로 지정한다.
val size: Int get() = ...
operator fun get(i: Int): T { ... }
}
fun feedAll(animals: Herd<Animal>) {
for (i in 0 until animals.size) {
animals[i].feed()
}
}
사용자 코드가 고양이 무리를 만들어서 관리한다고 하자.
// 무공변 컬렉션 역할을 하는 클래스 사용하기
class Cat : Animal() { // Cat은 Animal이다.
fun cleanLitter() { ... }
}
fun takeCareOfCats(cats: Herd<Cat>) {
for (i in 0 until cats.size) {
cats[i].cleanLitter()
// feedAll(cats) // Error: inferred type is Herd<Cat>, but Herd<Animal> was expected라는 오류가 발생한다.
}
}
불행히도 고양이들은 여전히 배고플 것이다. feedAll 함수에게 고양이 무리를 넘기면 타입 불일치 오류를 볼 수 있다. Herd 클래스의 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 때문에 고양이 무리는 동물 무리의 하위 클래스가 아니다. 명시적으로 타입 캐스팅을 사용하면 이 문제를 풀 수 있긴 하지만 그런 식으로 처리하면 코드가 장황해지고 실수를 하기 쉽다. 게다가 타입 불일치를 해결하기 위해 강제 캐스팅을 하는 것은 옳지 않다.
Herd 클래스는 List와 비슷한 API를 제공하며 동물을 그 클래스에 추가하거나 무리 안의 동물을 다른 동물로 바꿀 수는 없다. 따라서 Herd를 공변적인 클래스로 만들고 호출 코드를 적절히 바꿀 수 있다.
// 공변적 컬렉션 역할을 하는 클래스 사용하기
class Herd<out T : Animal> { // T는 이제 공변적이다.
...
}
fun takeCareOfCats(cats: Herd<Cat>) {
for (i in 0 until cats.size) {
cats[i].cleanLitter()
}
feedAll() // 캐스팅을 할 필요가 없다.
}
모든 클래스를 공변적으로 만들 수는 없다. 공변적으로 만들면 안전하지 못한 클래스도 있다. 타입 파라미터를 공변적으로 지정하면 클래스 내부에서 그 파라미터를 사용하는 방법을 제한한다. 타입 안정성을 보장하기 위해 공변적 파라미터는 항상 out 위치에만 있어야한다. 이는 클래스가 T 타입의 값을 생산할 수는 있지만 T 타입의 값을 소비할 수는 없다는 뜻이다.
클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은 모두 in과 out 위치로 나뉜다. T라는 타입 파라미터를 선언하고 T를 사용하는 함수가 멤버로 있는 클래스를 생각해보자. T가 함수의 반환 타입에 쓰인다면 T는 out 위치에 있다. 그 함수는 T 타입의 값을 생산한다. T가 함수의 파라미터 타입에 쓰인다면 T는 in 위치에 있다. 그런 함수는 T 타입의 값을 소비한다.
클래스 타입 파라미터 T 앞에 out 키워드를 붙이면 클래스 안에서 T를 사용하는 메소드가 out 위치에서만 T를 사용하게 허용하고 in 위치에서는 T를 사용하지 못하게 막는다. out 키워드는 T의 사용법을 제한하며 T로 인해 생기는 하위 타입 관계의 타입 안정성을 보장한다.
예를 들어, Herd 클래스를 생각해보자. Herd에서 타입 파라미터 T를 사용하는 장소는 오직 get 메소드의 반환 타입 뿐이다.
class Herd<out T : Animal> {
val size: Int get() = ...
operator fun get(i: Int): T { ... } // T를 반환 타입으로 사용한다.
}
함수의 반환 타입 위치는 out 위치다. 따라서 이 클래스를 공변적으로 선언해도 안전하다. Cat이 Animal의 하위 타입이기 때문에 Herd<Animal>의 get을 호출하는 모든 코드는 get이 Cat을 반환해도 아무 문제없이 작동한다.
- 공변성: 하위 타입 관계가 유지된다(Producer<Cat>은 Producer<Animal>의 하위 타입이다).
- 사용 제한: T를 out 위치에서만 사용할 수 있다.
이제 List<T> 인터페이스를 보자. 코틀린 List는 읽기 전용이다. 따라서 그 안에는 T 타입의 원소를 반환하는 get 메소드는 있지만 리스트에 T 타입의 값을 추가하거나 리스트에 있는 기존 값을 변경하는 메소드는 없다. 따라서 List는 T에 대해 공변적이다.
interface List<out T> : Collection<T> {
operator fun get(index: Int): T {
// ...
}
}
타입 파라미터를 함수의 파라미터 타입이나 반환 타입에만 쓸 수 있는 것은 아니다. 타입 파라미터를 다른 타입의 타입 인자로 사용할 수 있다. 예를 들어, List 인터페이스에는 List<T>를 반환하는 subList라는 메소드가 있다.
interface List<out T> : Collection<T> {
fun subList(fromIndex: Int, toIndex: Int): List<T> // 여기서도 T는 out 위치에 있다.
}
이 경우 subList 함수에 쓰인 T는 out 위치에 있다.
MutableList<T>를 타입 파라미터 T에 공변적인 클래스로 선언할 수는 없다는 점에 유의하자. MutableList<T>에는 T를 인자로 받아서 그 타입의 값을 반환하는 메소드가 있다(따라서 T가 in과 out 위치에 동시에 쓰인다).
interface MutableList<T> : List<T>, MutableCollection<T> { // MutableList는 T에 대해 공변적일 수 없다.
override fun add(element: T): Boolean // 왜냐하면 T가 in 위치에 쓰이기 때문이다.
}
컴파일러는 타입 파라미터가 쓰이는 위치를 제한한다. 클래스가 공변적으로 선언된 경우 "Type parameter T is declared as 'out' but occurs in 'in' position"이라는 오류를 보고한다.
생성자 파라미터는 in이나 out 어느 쪽도 아니라는 사실에 유의하자. 타입 파라미터가 out이라 해도 그 타입을 여전히 생성자 파라미터 선언에 사용할 수 있다.
변성은 코드에서 위험할 여지가 있는 메소드를 호출할 수 없게 만듦으로써 제네릭 타입의 인스턴스 역할을 하는 클래스 인스턴스를 잘못 사용하는 일이 없게 방지하는 역할을 한다. 생성자는 나중에 호출할 수 있는 메소드가 아니기 때문에 위험할 여지가 없다.
생성자 파라미터에 val이나 var 키워드 중 어떤 것을 쓰느냐에 따라 다른 점은 주의하자. 읽기 전용 프로퍼티는 out 위치, 변경 가능 프로퍼티는 out과 in 위치 모두에 해당한다.
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { ... }
여기서는 T 타입인 leadAnimal 프로퍼티가 in 위치에 있기 때문에 T를 out으로 표시할 수 없다.
또한 이런 위치 규칙은 오직 외부에서 볼 수 있는(public, protected, internal) 클래스 API에만 적용할 수 있다. 비공개 메소드의 파라미터는 in도 아니고, out도 아닌 위치다. 변성 규칙은 클래스 외부의 사용자가 클래스를 잘못 사용하는 일을 막기 위한 것이므로 클래스 내부 구현에는 적용되지 않는다.
9.3.4 반공변성: 뒤집힌 하위 타입 관계
반공변성(contravariance)은 공변성을 거울에 비친 상이라 할 수 있다. 반공변 클래스의 하위 타입 관계는 공변 클래스의 경우와 반대다. 예를 들어, Comparator 인터페이스를 살펴보면, 이 인터페이스에는 compare라는 메소드가 있다. 이 메소드는 주어진 두 객체를 비교한다.
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int { ... } // T를 in 위치에 사용한다.
}
이 인터페이스의 메소드는 T 타입의 값을 소비하기만 한다. 이는 T가 in 위치에서만 쓰인다는 뜻이다. 따라서 T 앞에는 in 키워드를 붙여야만 한다.
물론 어떤 타입에 대해 Comparator를 구현하면 그 타입의 하위 타입에 속하는 모든 값을 비교할 수 있다. 예를 들어, Comparator<Any>가 있다면 이를 사용해 모든 타입의 값을 비교할 수 있다.
>>> val anyComparator = Comparator<Any> {
... e1, e2 -> e1.hashCode() - e2.hashCode()
... }
>>> val strings: List<String> = ...
>>> strings.sortedWith(anyComparator) // 문자열과 같은 구체적인 타입의 객체를 비교하기 위해 모든 객체를 비교하는 Comparator를 사용할 수 있다.
sortedWith 함수는 Comparator<String>을 요구하므로, String보다 더 일반적인 타입을 비교할 수 있는 Comparator를 sortedWith에 넘기는 것은 안전하다. 어떤 타입의 객체를 Comparator로 비교해야 한다면 그 타입이나 그 타입의 조상 타입을 비교할 수 있는 Comparator를 사용할 수 있다. 이는 Comparator<Any>가 Comparator<String>의 하위 타입이라는 뜻이다. 그런데 여기서 Any는 String의 상위 타입이다. 따라서 서로 다른 타입 인자에 대해 Comparator의 하위 타입 관계는 타입 인자의 하위 타입 관계와는 정반대 방향이다.
이제 반공변성에 대한 정의를 설명할 준비가 됐다. Consumer<T>를 예로 들어 설명하자. 타입 B가 타입 A의 하위 타입인 경우 Consumer<A>가 Consumer<B>의 하위 타입인 관계가 성립하면 제네릭 클래스 Consumer<T>는 타입 인자 T에 대해 반공변이다. 여기서 A와 B의 위치가 서로 뒤바뀐다는 점에 유의하자. 따라서 하위 타입 관계가 뒤집힌다고 말한다. 예를 들어, Consumer<Animal>은 Consumer<Cat>의 하위 타입이다.
아래는 타입 파라미터에 대해 공변성인 클래스와 반공변성인 클래스의 하위 타입 관계를 보여준다. Producer 클래스는 타입 인자의 하위 타입 관계를 그대로 따르지만 Consumer 클래스에서는 타입 인자의 하위 타입 관계와는 반대라는 점을 확인할 수 있다.
9.3.5 사용 지점 변성 : 타입이 언급되는 지점에서 변성 지점
클래스를 선언하면서 변성을 지정하면 그 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치므로 편리하다. 이런 방식을 선언 지점 변성(declaration site variance)이라 부른다. 자바의 와일드카드 타입(? extends나 ? super)에 익숙하다면 자바는 변성을 다른 방식으로 다룬다는 점을 깨달았을 것이다. 자바에서는 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시해야 한다. 이런 방식을 사용 지점 변성(use-site variance)이라 부른다.
코틀린도 사용 지점 변성을 지원한다. 따라서 클래스 안에서 어떤 타입 파라미터가 공변적이거나 반공변적인지 선언할 수 없는 경우에도 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있다.
MutableList와 같은 상당수의 인터페이스는 타입 파라미터로 지정된 타입을 소비하는 동시에 생산할 수 있기 때문에 일반적으로 공변적이지도 반공변적이지도 않다. 하지만 그런 인터페이스 타입의 변수가 한 함수 안에서 생산자나 소비자 중 단 한가지 역할만을 담당하는 경우가 자주 있다. 예를 들어 다음 함수를 살펴보자.
// 무공변 파라미터 타입을 사용하는 데이터 복사 함수
fun <T> copyData(source: Mutable<T>, destination: MutableList<T> {
for (item in source) {
destination.add(item)
}
}
이 함수는 컬렉션의 원소를 다른 컬렉션으로 복사한다. 두 컬렉션 모두 무공변 타입이지만 원본 컬렉션에서는 읽기만 하고 대상 컬렉션에는 쓰기만 한다. 이 경우 두 컬렉션의 원소 타입이 정확하게 일치할 필요가 없다. 예를 들어 문자열이 원소인 컬렉션에서 객체의 컬렉션으로 원소를 복사해도 아무 문제가 없다.
이 함수가 여러 다른 리스트 타입에 대해 작동하게 만들려면 두 번째 제네릭 타입 파라미터를 도입할 수 있다.
// 타입 파라미터가 둘인 데이터 복사 함수
fun <T: R, R> copyData(source: MutableList<T>, // source 원소 타입은 destination 원소 타입의 하위 타입이어야 한다.
destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems) // Int가 Any의 하위 타입이므로 이 함수를 호출할 수 있다.
>>> println(anyItems)
[1, 2, 3]
두 타입 파라미터는 원본과 대상 리스트의 원소 타입을 표현한다. 한 리스트에서 다른 리스트로 원소를 복사할 수 있으려면 원본 리스트 원소 타입은 대상 리스트 원소 타입의 하위 타입이어야 한다. 예를 들어 위 코드에서 Int는 Any의 하위 타입이다.
코틀린에는 이를 더 우아하게 표현할 수 있는 방법이 있다. 함수 구현이 out이나 in 위치에 있는 타입 파라미터를 사용하는 메소드만 호출한다면 그런 정보를 바탕으로 함수 정의 시 타입 파라미터에 변성 변경자를 추가할 수 있다.
// out 프로젝션 타입 파라미터를 사용하는 데이터 복사 함수
fun <T> copyData(source: MutableList<out T>, // out 키워드를 타입을 사용하는 위치 앞에 붙이면
destination: MutableList<T>) { // T 타입을 in 위치에 사용하는 메소드를 호출하지 않는다는 뜻이다.
for (item in source) {
destination.add(item)
}
}
타입 선언에서 타입 파라미터를 사용하는 위치라면 어디에나 변성 변경자를 붙일 수 있다. 따라서 파라미터 타입, 로컬 변수 타입, 함수 반환 타입 등에 타입 파라미터가 쓰이는 경우 in이나 out 변경자를 붙일 수 있다. 이때 타입 프로젝션(type projection)이 일어난다. 즉 source를 일반적인 MutableList가 아니라 MutableList를 프로젝션을 한(제약을 가한) 타입으로 만든다. 이 경우 copyData 함수는 MutableList의 메소드 중에서 반환 타입으로 타입 파라미터 T를 사용하는 메소드만 호출할 수 있다.(더 정확하게는 타입 파라미터 T를 아웃 위치에서 사용하는 메소드만 호출할 수 있다). 컴파일러는 타입 파라미터 T를 함수 인자 타입(더 정확하게는 in 위치에 있는 타입)로 사용하지 못하게 막는다.
>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits
the use of 'fun add(element: E): Boolean'
프로젝션 타입의 메소드 중 일부를 호출하지 못하더라도 놀라지 말자. 그런 메소드를 호출하고 싶으면 프로젝션 타입 대신 일반 타입을 사용하면 된다. 일반 타입을 사용하려면 경우에 따라 다른 타입과 연관이 있는 새 타입 파라미터를 추가해야할 수도 있다.
9.3.6 스타 프로젝션: 타입 인자 대신 * 사용
코틀린에서 제네릭을 사용할 때, 어떤 타입 인자가 들어올지 전혀 모르는 경우가 종종 발생한다. 이때 스타 프로젝션(star projection)을 사용하면, 타입 인자 대신 *를 써서 "모든 타입"을 의미하는 안전한 형태로 다룰 수 있다.
제네릭 클래스를 사용할 때, 특정 타입 인자에 의존하지 않고 단순히 내부 구조나 메서드만 활용하고 싶을 수 있다. 예를 들어, 어떤 함수가 여러 종류의 List<T>를 받지만, List<T> 내부에는 관심이 없는 경우다. 이럴 때 스타 프로젝션을 사용하면, 해당 제네릭 타입의 구체적인 타입 인자를 신경쓰지 않고 안전하게 다룰 수 있다.
List<*>와 같이 *를 사용하면, 어떤 타입이 들어오더라도 상관없다는 의미다. 내부적으로 코틀린은 해당 타입 매개변수의 상한 타입(upper bound)을 사용한다. 만약 타입 매개변수에 별도의 상한이 없다면 기본적으로 Any?가 상한이 된다.
for printList(list: List<*>) {
for (item in list) {
println(item) // 읽기는 사용 가능
}
// list.add(42) // 컴파일 에러! 쓰기는 불가능
}
리스트의 요소를 읽어올 때는 동작한다. 반환되는 타입은 상한 타입으로 취급되어, 일반적으로 Any? 타입이 된다. 쓰기는, 즉 요소를 추가하거나 변경하는 것은 제한된다. 왜냐하면 실제 리스트의 타입이 무엇인지 알 수 없기 때문에, 잘못된 타입의 값을 넣으면 타입 안정성이 깨질 가능성이 있다.
여러 종류의 제네릭 리스트를 받아서 그 내용을 출력하고 싶을 때, 스타 프로젝션을 사용하면 다음과 같이 작성할 수 있다.
fun printElements(coolection: Collection<*>) {
for (element in collection) {
println(element)
}
}
>>> val stringList: List<String> = listOf("코틀린", "제네릭")
코틀린, 제네릭
>>> intList: List<Int> = listOf(1, 2, 3)
1, 2, 3
'Kotlin' 카테고리의 다른 글
[Kotlin] 스타 프로젝션(*)과 Any의 차이 (0) | 2025.04.03 |
---|---|
[Kotlin] 코틀린 선언 지점 변성과 자바 와일드카드 비교 (0) | 2025.04.03 |
[Kotlin in Action] 9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터 (0) | 2025.04.02 |
[Kotlin in Action] 9.1 제네릭 타입 파라미터 (0) | 2025.04.02 |
[Kotlin in Action] 8.3 고차 함수 안에서 흐름 제어 (0) | 2025.04.02 |
댓글