-
[Kotlin in Action, Effective Kotlin] 제네릭과 변성 variance언어/Kotlin 2022. 5. 29. 18:37반응형
1. 공변성 (covariant)
- Set과 List는 공변성이 있다. String은 Any로 업캐스팅 될 수 있으므로, List<String>은 List<Any>로 업캐스팅 될 수 있다. 공변성이란 하위 타입 관계가 유지되는 것이다. List가 공변성이 있다는 것을 나타내기 위해 제네릭 E를 <out E>로 선언한다.
- out이란, 제네릭 E가 out 위치에서 사용되어야 한다는 의미이다. out위치란 public 접근 제한자인 변수, 함수의 return 타입의 위치를 의미한다. E가 반환 타입에 사용된다면, 그 함수는 E타입의 값을 생산(Produce)한다고 정의한다.
public interface List<out E> : kotlin.collections.Collection<E> { . public operator fun get(index: Int): E . . public fun indexOf(element: @UnsafeVariance E): Int . . }
- 위 코드에서 E는 out위치에서 사용되어야한다. 그러므로 get 메서드는 제네릭 E를 out 위치에서 사용하고 있다.
- indexOf 메서드는 제네릭 E를 왜 in 위치에서 사용할 수 있나? List내부의 요소를 추가 혹은 변경, 삭제하지 않는 함수에는 @UnsafeVariance 어노테이션으로 제네릭 E를 in 위치에서 사용할 수 있다. < 명확하지 않다면 사용하지 말자.
2. 무공변성 (invariant)
- MutableSet과 MutableList는 무공변성이 있다. String은 Any로 업캐스팅 될 수 있으나, MutableList<String>은 MutableList<Any>로 업캐스팅 될 수 없다. 무공변성이란 타입 관계가 유지되지 않는 것이다. MutableList가 무공변성이 있다는 것을 나타내기 위해 제네릭 E를 <E>로 선언한다. 기본적인 제네릭은 모두 무공변성이다.
public interface MutableList<E> : List<E>, MutableCollection<E> { . . override fun add(element: E): Boolean . . public operator fun set(index: Int, element: E): E . . }
- 무공변성이 있는 제네릭 타입E는 in 위치와 out 위치 어디에서든지 사용가능하다.
fun addInteger(list: MutableList<Any>) { list.add(43) } val strings: MutableList<String> = mutableListOf("abc", "def") addInteger(strings) // ["abc", "def", 43] println(strings.maxBy { it.length })
- 만약 mutableList가 공변성이라면? 위 코드는 runtime시점에 오류가 발생한다. 이런 문제를 막기위해 mutableList는 무공변성이며, addInteger(strings)를 호출하는 시점에 타입이 일치하지 않는다는 오류가 발생한다.
3. 반공변성 (contravariant)
- Comparator은 반공변성이 있다. String가 Any로 업캐스팅 될 수 있으면, Comparator<Any>가 Comparator<String>으로 업캐스팅 될 수 있다. 즉, String의 상위 객체(부모)가 Any면, Comparator<Any>의 상위객체(부모)는 Comparator<String>이다. 반공변성이란 공변성과 정반대로 타입 관계가 바뀌는 것이다. Comparator가 반공변성이 있다는 것을 나타내기 위해 제네릭 T를 <in T>로 선언한다.
interface Comparator<in T> { fun compare(e1: T, e2: T): Int { ... } }
- in이란, 제네릭 T가 in 위치에서 사용되어야 한다는 의미이다. in 위치란 함수의 파라미터 타입의 위치를 의미한다. T가 파라미터 타입에 사용된다면, 그 함수는 T타입의 값을 소비(consume)한다고 정의한다.
4. 왜?
- 왜 무공변성이 필요할까?
- 컴파일 시점에 타입이 일치하지 않는 문제가 있어 아이템이 변경 및 추가 되는 MutableXXX는 무공변성을 가진다.
- 왜 공변성이 필요할까?
- Int는 Number로 업캐스팅 되므로, Int는 자식이고 Number는 부모 클래스다. 공변성 상황에서, 마찬가지로 List<Int>는 자식이고 List<Number>는 부모 클래스다. 부모 자식 관계에서 부모의 함수는 자식 클래스에서 사용이 가능하다.
- 아래 코드에서 addNumberToList 함수는 List<Number>의 확장함수이다. 자식 클래스에서 부모 클래스의 함수는 사용이 가능하므로 아래 List<Int>와 List<Double>에서도 사용이 가능하다.
fun List<Number>.addNumberToList(number: Int): List<Number> = this.map { it.toDouble + number } val integers: List<Int> = listOf(1, 2, 3, 4) val doubles: List<Double> = listOf(5.0, 6.0, 7.0, 8.0) println(integers.addNumberToList(10)) // [11.0, 12.0, 13.0, 14.0] println(doubles.addNumberToList(10)) // [15.0, 16.0, 17.0, 18.0]
- 반대로 무공변성인 MutableList에 확장함수를 만들면 어떻게 될까? 아래 코드에서 MutableList<Number>는 MutableList<Int>, MutableList<Double>과 연관관계가 없으므로, 확장함수를 사용할 수 없다.
fun MutableList<Number>.addNumberToList(number: Int): List<Number> = this.map { it.toDouble + number } val integers: List<Int> = listOf(1, 2, 3, 4) val doubles: List<Double> = listOf(5.0, 6.0, 7.0, 8.0) val numbers: List<Number> = listOf(5.0, 6.0, 7.0, 8.0) println(integers.addNumberToList(10)) // compile error println(doubles.addNumberToList(10)) // compile error println(numbers.addNumberToList(10)) // [15.0, 16.0, 17.0, 18.0]
같이 보면 좋은글 :
- Java의 무공변성 List와 공변성 Array : https://sungjk.github.io/2017/08/06/effective-java-4-2.html
참고 :
반응형'언어 > Kotlin' 카테고리의 다른 글
[EffectiveKotlin] equals, hashcode 규약과 data class, jpa (0) 2022.06.01 [EffectiveKotlin] 상속보다는 컴포지션을 사용하라 (0) 2022.06.01 [Kotlin] - List의 합집합, 차집합, 교집합 (0) 2022.05.08 [Kotlin In Action] 객체의 동등성: equals()와 해시 컨테이너: hashCode() 그리고 data class (0) 2022.04.26 [Kotlin In Action] 로컬 함수와 확장 (0) 2022.04.25