[kotlin] generics
Generics
설명하기전에 배경지식으로
- class vs type
- subclass vs subtype
에 대한 배경지식을 알아야 한다. 별로 어려운 내용이 아니므로 매우 간략히 정리한다.
class vs type
╔═════════════╦═════════════════╦
║ ║ Class ║ Type ║
╠═════════════╬════════╬════════╬
║ String ║ Yes ║ Yes ║
║ String? ║ No ║ Yes ║
║ List ║ Yes ║ Yes ║
║ List<String>║ No ║ Yes ║
╚═════════════╩════════╩════════╩
subclass vs subtype
subclass가 성립하기 위해서는, 어떤 클래스를 상속해야한다. 예를 들어, Integer, Number가 있따고 가정해보자. Integer은 Number를 상속한다. 그러므로 Integer는 Number의 서브클래스 이다.
val integer: Int = 1
val number: Number = integer
integer은 number의 하위 타입이기 때문에 이것이 성립한다.
subtype의 예를 보자. Int는 Int?의 하위 타입이다. 그래서 Int?에 Int를 할당할 수 있다.
Variance(Invariance, Covariance, Contravariance)
변성이란 타입 간의 범위 지정을 일컫는 말이다.
다시 말해, 변성이란 타입간의 상속 관계를 정의하는 것이다.
설명을 위해
abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Spider(val terrorFactor: Int): Animal(1)
이런 클래스가 존재한다고 가정한다.
- Invariance(무공변)
invariance를 살펴보자. 기본적으로 자바, 코틀린에서 타입 범위를 따로 지정하지 않으면 모든 제네릭 클래스는 무공변이다. 무공변이라는 것은 제네릭타입을 인스턴스화 할 때, 서로 다른 타입 인자가 들어가는 경우 인스턴스 타입 사이의 하위 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라고 한다.
자바코드를 보자
List<Dog> dogList = new ArrayList<>();
List<Animal> animalList = dogList; // compile error
컴파일 에러가 나오는 것을 볼 수 있다. 이렇듯 기본적으로 성립하지 않는다.
즉, List<Dog>
을 List<Animal>
에 할당 할 수 없다. 이를 무공변이라 한다.
이게 된다면, 꺼낼 때 classCastException이 발생할 수 있음
Dog -> Animal
List<Dog>
->x Animal
- Convariance(공변)
설명하기 전에 코틀린의 List는 immutable하다. 즉, 변경(추가, 삭제) 불가능 하다. 아래 코드를 보자.
val dogList: List<Dog> = listOf(Dog(10), Dog(20))
val animalList: List<Animal> = dogList
List가 변경 불가능 하기 때문에, 위의 코드는 컴파일 에러가 나지 않는다.
Dog은 Animal의 하위 타입이고,
List<Dog>
은 List<Animal>
의 하위 타입이다.
이렇게 타입 관계가 보존되는 것을 covariance(공변) 이라고 한다.
Dog -> Animal
List
- Contravariance(반공변)
1)
동물들을 비교하고 싶은 상황이 생겼다고 가정하자.
interface Compare
val dogCompare: Compare<Dog> = object: Compare<Dog> {
override fun compare(first: Dog, second: Dog): Int {
return first.cuteness - second.cuteness
}
}
위와 같은 코드가 있고
val animalCompare: Compare<Animal> = dogCompare // Compiler error
이런 코드를 만들었을 때, 컴파일 에러가 난다. 이 코드가 왜 성립하지 하면 안돼는지에 대한 매우 좋은 이유가 있다. animalCompare에 spiders를 전달한다고 가정해보자. 에러가 날 것이다. dogCompare은 오직 dog의 귀여움을 측정하기 위한 클래스이기 때문이다.
반면, 만약에 동물들을 전부 비교할 수 있는 매커니즘이 있다면, dogs, spiders 모두에게 효과적일 것이다. 그런 것이 있다.
val animalCompare: Compare<Animal> = object: Compare<Animal> {
override fun compare(first: Animal, second: Animal): Int {
return first.size - second.size
}
}
val spiderCompare: Compare<Spider> = animalCompare // Works nicely!
컴파일 에러 없이 잘 돌아간다.
그리고 이를 통해 볼 수 있는 것은
Spider가 Animal의 하위 타입인데,
Compare<Animal>
이 Compare<Spider>
의 하위 타입 이라는 것이다.
즉, 타입 관계가 역전 되었다!
이것을 contravariance(반공변) 이라고 한다.
Spider -> Animal
Compare<Animal>
-> Compare<Spider>
2) 또 다른 예시를 보자.
public static void contravarianceProblem() {
List<? super Number> numberList = getNumberList();
numberList.add(new Long(12));
numberList.add(new Double(12));
Number number = numberList.get(0); // compile error
}
public static List<? super Number> getNumberList() {
List<Object> objects = new ArrayList<>();
objects.add("");
return objects;
}
여기서 compile error를 내지 않는다면, 큰 문제가 생길 수 있다. 첫 번째 index를 꺼내면, Object가 Number에 들어가기 때문이다. 그래서 이런것을 막기 위해 읽기를 금지하고 write only로 제한한다.
자바의 변성
자바에서 제네릭을 covariant, contravariant 하게 만들 수 있다.
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
extends가 타입을 제한함으로써 Animal과 Dog간의 관계를 covariant(공변) 하게 만든다.
Compare<Animal> animalCompare = (first, second) -> first.getSize() — second.getSize();
Compare<? super Spider> spiderCompare = animalCompare;
super로 타입을 제한함으로써 Animal과 Dog간의 관계를 contravariant(반공변) 하게 만들 수 있다.
또한, 이렇게 사용하는 지점에서 변성을 결정하는 것을 use-site variance(사용 지점 변성)이라고 말한다.
코틀린의 변성
코틀린에서 역시 가능하다. List를 보면
interface List<out E> {
fun get(index: Int): E
}
out키워드의 의미는 List안의 메소드들은 오직 E type을 리턴만 하고 E type을 메소드의 인자로 받지 않는 다는 것을 뜻한다(E type 파라미터를 쓸려고 하면 컴파일 에러). 이것이 List를 covariant(공변)하게 만든다.
Compare를 보자.
interface Compare<in T> {
fun compare(first: T, second: T): Int
}
이번에는 in이 있다. in키워드의 의미는 Compare안의 메소드들은 T type을 인자로만 받고 T type을 리턴하지 않는 다는 뜻이다. 이것이 Compare를 contravariant(반공변)하게 만든다.
또한, 코틀린에서는 클래스를 선언하는 시점에 변성을 고려하게 만들었다. 사용하는 지점이 아닌.
이것을 declaration-site variance(선언 지점 변성)이라고 한다.
PE, CS -> out, in -> read only, write only
참고 표 :
╔═════════════╦═════════════════╦══════════════════╦═══════════════════╦
║ ║ Covariance ║ Contravariance ║ Invariance ║
╠═════════════╬═════════════════╬══════════════════╬═══════════════════╬
║ Purpose ║ Producer ║ Consumer ║Producer + Consumer║
║ Example ║ ImmutableList ║ Compare ║MutableList ║
║ Java ║ extends ║ super ║ ║
║ Kotlin ║ out ║ in ║ ║
╚═════════════╩═════════════════╩══════════════════╩═══════════════════╩
이렇게 변성에 대한 개념을 알아봤고, 왜 변성이 필요한지에 대해서도 간략히 살펴봤다.
참고 자료 : https://proandroiddev.com/understanding-generics-and-variance-in-kotlin-714c14564c47 https://medium.com/tompee/idiomatic-kotlin-variance-82355d9a71df https://deep-dive-dev.tistory.com/39