본문 바로가기

IT/kotlin언어

[kotlin] 제너릭 변성(variance) 정리

안녕하세요 남갯입니다


오늘은 제너릭 타입에대해 정리해보려고합니다



SubType 이란?


subType이란 어떤 class가 다른클래스를 상속받은것을 의미한다. 


즉 타입 A가 필요한 곳에 타입 B 값을 넣어도 문제가 없다면 B는 A의 하위타입인 것이다.


예를들어 


open class A() {
val x = 0
val y = 1
}

class B : A(){
val z = 2
}


와 같이 B는 A를 상속받았으므로 A의 subType이다.


var a = A()
var b = B()
a = b

는 가능하지만 


var a = A()
var b = B()
b = a

는 mismatch가 난다.


즉 A타입이 필요한곳에 B를 넣었는데 문제가 없으므로 B 는 A의 SubType인것이다.








무공변성(invariance)


제너릭타입에서 인스턴스화 할때 다른인자가 들어가는 경우와 인스턴스간의 하위관계가 성립하지 않으면 그 제너릭 타입을 무공변이라고 합니다. 


* 자바와 코틀린에서의 제너릭 타입은 무공변입니다.


예를들어 Number - Int, Double은 공변성을 가지지만 Number - String 은 무공변성이다.


List<String> str = new ArrayList<String>();
List<Object> obj = str; // error

위의 코드와 같이 str에 obj를 넣으려고 하면 에러가 난다.


왜일까요?


자바에서의 제너릭타입은 무공변성을 가집니다. 즉 List<String>은 List<Object>의 subtype이 아니라는 의미인것입니다.

왜냐하면 List가 무공변성을 가지지 않는다면 


List<String> str = new ArrayList<String>();
List<Object> obj = str;
obj.add(1);
String s = str.get(0); // class cast exception 이 발생함


위에서 에러가 발생합니다. 그래서 자바는 런타임에 안전을 보장하기 위해 금지하고 있습니다.


List<Object> obj = new ArrayList<>();
List<String> str = new ArrayList<String>();
obj.addAll(str);


위의 코드는 동작합니다. 왜 인가 봤더니 


boolean addAll(Collection<? extends E> c);


자바에서의 addAll은 와일드 카드를 사용하고있습니다. Object클래스에 String은 SubType이기 때문에 E타입이 Object가 되게되고


Object의 SubType은 저장이 가능하도록 하는 방법이죠









공변성 (convariance) out


위에서 자바에서는 무공변성을 가진다고 말했습니다. 코틀린에서의 와일드 카드처럼 out은 비슷한 용도를 갖고있습니다.


해당의 SubType일경우 


open class Parent {
fun hi() {
TODO()
}
}

class Child : Parent()

class Children<T : Parent> {
val name = ""
fun get() : T{
TODO()
}
}

fun getChild(children : Children<Parent>){
children.get()
}

위의 코드가 있을때 Child는 Parent의 SubType이지만 


val child = Children<Child>()
getChild(child)


이렇게 작성을 할경우 에러가 납니다.


에러의 이유는 type mismatch 저런 형태로 작성을 하게되면 무공변이여서 불가능하다 하지만


class Children<out T : Parent> {
val name = ""
fun get() : T{
TODO()
}
}

out을 붙여주게되면 무공변에서 설명한 자바의 와일드 카드와 동일한 동작을 하게됩니다.


Parent의 SubType인 Child를 넣어도 동작하게 되는것이죠









반공변성 (contravariance) in


공변성을 가지지만 SubType이 뒤집힌것을 반공변성이라고 부른다. 



val child = Children<Child>()
getChild(child)


**

Java <? extends Parent> == kotlin <out T : Parent>

Java <? super Parent> == kotlin <int T : Parent>

**



in과 out의 가장 큰 차이점


in과 out의 가장 큰 차이점은 데이터에 대한 수정/읽기 제한의 의미가 크다.


class Array<T>(val size: Int) {
fun get(index: Int): T { }
fun set(index: Int, value: T) { }
}

위와 같은 소스에서 T타입을 리턴하는것을 제한할 수 가 없는데,


아래와 같이 out 으로 지정할경우 수정의 제한


fun copy(from: Array<out Any>, to: Array<Any>) {  }


아래와 같이 in 으로 지정할경우 읽기의 제한


fun fill(dest: Array<in String>, value: String) { }


이 가능하다.



* 공부하면서 작성한 글이라 따끔한충고는 달게 받겠습니다.



글을 작성하기 위해 아래를 참조했습니다.

https://kotlinlang.org/docs/reference/generics.html

https://umbum.tistory.com/612

https://stackoverflow.com/questions/9151461/what-is-the-difference-between-java-subtype-and-true-subtype