1. 상속 고찰
class Animal
class Bird
class Eagle
fun main(args: Array<String>) {
val animal: Animal = Bird() // Type mismatch.
val bird: Bird = Bird()
val eagle: Eagle = Bird() // Type mismatch.
}
main 함수의 첫 줄을 한국어로 번역해 보자.
val animal: Animal = Bird() |
|||
val animal: |
Animal |
= |
Bird() |
새가 한마리 태어났다. |
|||
동물 역할을 한다. |
|||
animal 이라고 이름 지었다. |
|||
새(Bird)가 한 마리 태어났는데 동물(Animal) 역할을 하는 animal 이라고 이름 지었다. |
main 함수 내부 모두를 번역한 결과는 아래와 같다.
val animal: Animal = Bird() // Type mismatch.
// 새가 한마리 태어났는데 동물의 역할을 하는 animal 이라고 이름 지었다.
val bird: Bird = Bird()
// 새가 한마리 태어났는데 새의 역할을 하는 bird 라고 이름 지었다.
val eagle: Eagle = Bird() // Type mismatch.
// 새가 한마리 태어났는데 독수리의 역할을 하는 bird 라고 이름 지었다.
첫 번째 줄에서 Type mismatch 컴파일 오류가 발생했다.
val animal: Animal = Bird() // Type mismatch.
"새가 한마리 태어났는데 동물의 역할을 하는 animal 이라고 이름 지었다."
새가 동물의 역할을 못할 이유가 없음에도 Type mismatch 컴파일 에러가 발생한다.
개발자로서 우리가 창조하는 세상에서 새가 동물의 일종(상속, 분류)이라는 것을 선언해 주지 않았기 때문이다.
두 번째 줄과 세 번째 줄에 대해서는 논리적으로 맞는지 스스로 생각해 보시길….
2. 상속으로 상위 분류, 하위 분류 선언하기
상속을 통해 "새"가 "동물"의 하위 분류(상속)임을 알려주도록 하자.
open class Animal
open class Bird: Animal()
class Eagle: Bird()
fun main(args: Array<String>) {
val animal: Animal = Bird()
val bird: Bird = Bird()
val eagle: Eagle = Bird() // Type mismatch.
}
상속 구조를 정의해 주었다.
class Bird: Animal() // 새는 짐승의 하위 분류이다.
class Eagle: Bird() // 독수리는 새의 하위 분류이다.
또한 상속 구조에 의해 독수리는 짐승의 하위 분류이다.
main 함수의 세 번째 줄에서 Type mismatch 컴파일 오류가 발생했다.
val eagle: Eagle = Bird() // Type mismatch.
// 새가 한마리 태어났는데 독수리의 역할을 하는 bird 라고 이름 지었다.
위 문장을 자세히 보자.
과연 태어난 새가 독수리라고 하는 것은 맞는 말일까?
참새나 앵무새일지도 모른다.
따라서 논리적으로 태어난 새에게 독수리 역할을 시킬 수는 없다.
독수리는 반드시 새이지만, 새가 반드시 독수리는 아니기 때문이다.
3. 제네릭 고찰
제네릭도 위의 상황과 정확히 같은 논리를 펼칠 수 있다.
open class Animal
open class Bird: Animal()
class Eagle: Bird()
class Box<T>
fun main(args: Array<String>) {
val animal: Animal = Bird()
val bird: Bird = Bird()
val animalBox: Box<Animal> = Box<Bird>() // Type mismatch.
val birdBox: Box<Bird> = Box<Bird>()
val eagleBox: Box<Eagle> = Box<Bird>() // Type mismatch.
}
Tip
|
Box<T> 를 "T 의 Box" 또는 "T Box" 라고 읽는다. List<Int> 를 소리내에 읽을면 "Int (의) List" 라고 읽는다. |
제네릭에서 주의할 것은 Box<Animal>, Box<Bird>, Box<Eagle> 를 각각 독립적이고 원자적인 단위로 봐야한다는 것이다.
Box<Animal> 를 Box 와 Animal 로 분리해서 생각하지 말고 하나의 원자적 단위로 봐야한다는 것이다.
INFORMATION: 아재개그 + 알파: Box<T> 를 Box 와 T 로 나누어 생각하는 것은, 소금을 나누어 소와 금으로 생각하는 오류와 같다. 그렇게 나눌 수 있다면 소와 금을 팔아 부자가 될 수 있을텐데…
원자적 단위로 보는데 어려움이 있다면 typealias 를 사용해서 코드를 수정하고 이해하도록 보도록 하자.
open class Animal
open class Bird : Animal()
class Eagle : Bird()
class Box<T>
typealias AnimalBox = Box<Animal>
typealias BirdBox = Box<Bird>
typealias EagleBox = Box<Eagle>
fun main(args: Array<String>) {
val animal: Animal = Bird()
val bird: Bird = Bird()
val animalBox: AnimalBox = BirdBox() // Type mismatch.
val birdBox: BirdBox = BirdBox()
val eagleBox: EagleBox = BirdBox() // Type mismatch.
}
Type mismatch 가 일어나는 것은 AnimalBox, BirdBox, EagleBox 사이에 상속 관계를 선언해 준 적이 없기 때문이다.
4. 공변(covariance)
타입 파라미터 T 를 갖는 Box 가 상속 관계를 가지고 있고, T 의 상속 관계를 같이 공유하여 변화한다면 이를 공변이라고 한다.
타입 파라미터 T 의 대상이 되는 Animal ← Bird ← Eagle 의 상속를 관계를 공유하여
Box 를 실체화한 Box<Animal> ← Box<Bird> ← Box<Eagle> 의 상속 관계가 된다며 이를 공변이라고 한다.
이를 컴파일러에게 알려주는 키워드가 out 이다.
out 을 타입 파라미터 T 앞에 적어 주면 된다.
open class Animal
open class Bird: Animal()
class Eagle: Bird()
class OutBox<out T>
fun main(args: Array<String>) {
val animal: Animal = Bird()
val bird: Bird = Bird()
val animalOutBox: OutBox<Animal> = OutBox<Bird>()
val birdOutBox: OutBox<Bird> = OutBox<Bird>()
val eagleOutBox: OutBox<Eagle> = OutBox<Bird>() // Type mismatch.
}
기존에 무공변(invariance)이였던 Box 와의 구분을 위해 OutBox 라는 이름을 사용했다.
5. 반공변(contravariance)
위에서는 T 의 상속 관계를 공유하도록 out 키워드를 통해 OutBox 를 공변으로 선언해 주었다.
-
T: Animal ← Bird ← Eagle
-
OutBox<out T>: OutBox<Animal> ← OutBox<Bird> ← OutBox<Eagle>
그런데 타입 파라미터가 실체화된 Animal, Bird, Eagle 의 관계와 반대인 InBox 관계를 정의할 수 있을까?
이를 반공변이라 하고 타입 파라미터 T 에 in 키워드를 적용해서 표시할 수 있다.
-
T: Animal ← Bird ← Eagle
-
InBox<in T>: InBox<Eagle> ← InBox<Bird> ← InBox<Animal>
open class Animal
open class Bird: Animal()
class Eagle: Bird()
class InBox<in T>
companion object {
@JvmStatic
fun main(args: Array<String>) {
val animal: Animal = Bird()
val bird: Bird = Bird()
val animalInBox: InBox<Animal> = InBox<Bird>() // Type mismatch.
val birdInBox: InBox<Bird> = InBox<Bird>()
val eagleInBox: InBox<Eagle> = InBox<Bird>()
}
}
6. 중간 정리
백문이 불여일견!!!
T 의 대상 | T 에 대해 Box 는 무공변 | T 에 대해 OutBox 는 공변 | T 에 대해 InBox 는 반공변 |
---|---|---|---|
open class Animal |
class Box<T> |
class OutBox< out T> |
class OutBox< in T> |
Box<T> 는 T 의 상속과 무공변(공유하여 변화하는 것이 없음) |
Box< out T> 는 T 의 상속과 공변(공유하여 변화) |
OutBox< in T> 는 T 의 상속과 반공변(반대로 공유하여 변화) |
7. in, out 키워드와 인자, 반환값
JAVA 제네릭스에는 PECS(Producer Extends, Consumer Super) 라는 규칙이 있다.
관심있다면 찾아보자(Kotlin 공부하면서 JAVA 와 대조해 보는 것은 때로는 좋은 선택이지만 난 반댈세… 특히, 이 경우는…).
Kotlin 은 JAVA PECS 와 같은 것을
제네릭스 타입 파라미터 T 에 in 을 붙이면 제네릭스 안에 정의된 함수의 인자로 T 를 사용 가능하지만 반환값으로 사용할 수는 없다.
제네릭스 타입 파라미터 T 에 out 을 붙이면 제네릭스 안에 정의된 함수의 반환값으로 T 를 사용 가능하지만 인자로 사용할 수는 없다.
이를 위배하면 컴파일 에러가 발생한다.
이를 외워두어야 한다.
그리고 더해서 이해해 보자.
먼저 OutBox<out T> 를 살펴보자.
val animalOutBox: OutBox<Animal> = OutBox<Bird>()
val birdOutBox: OutBox<Bird> = OutBox<Bird>()
// val eagleOutBox: OutBox<Eagle> = OutBox<Bird>() // Type mismatch.
위의 소스를 그림으로 이해해 보자.
새 취급의 위험성: 어이 거기 동그라미 3번 좌측 빨간 동물! 날아봐!!! (저 고래인데요!!!)
컴파일러는 3 의 위험성 때문에 out T 에 대해 T 를 in 의 위치, 즉 함수의 인자로 사용하지 못 하도록 한다.
그럼에도 자신 있다면 @UnsafeVariance 를 사용하면 된다.
이번에는 InBox<in T> 를 살펴보자.
// val animalInBox: InBox<Animal> = InBox<Bird>() // Type mismatch.
val birdInBox: InBox<Bird> = InBox<Bird>()
val eagleInBox: InBox<Eagle> = InBox<Bird>()
위의 소스를 그림으로 이해해 보자.
독수리 취급의 위험성: 어이 거기 동그라미 1번 오른쪽 검정새! 토끼 사냥해봐!!! (저 참새인데요!!!)
컴파일러는 1 의 위험성 때문에 in T 에 대해 T 를 out 의 위치, 즉 함수의 반환값으로 사용하지 못 하도록 한다.
그럼에도 자신 있다면 @UnsafeVariance 를 사용하면 된다.
8. 선언 지점 변성 vs. 사용 지점 변성
// Declaration-site variance
// 선언 지점 변성 (in, out 키워드 등판)
class OutBox<out T>
class InBox<in T>
fun main(args: Array<String>) {
// 사용 지점에서는 in / out 키워드가 없다.
val animalBox: OutBox<Animal> = OutBox<Bird>()
val eagleBox: InBox<in Eagle> = InBox<Bird>()
...
}
// 선언 지점에서는 in / out 키워드가 없다.
class Box<T>
fun main(args: Array<String>) {
// Use-site variance
// 사용 지점 변셩 (in, out 키워드 등판)
val animalBox: Box<out Animal> = Box<Bird>()
val eagleBox: Box<in Eagle> = Box<Bird>()
...
}