자바가 겪는 제네릭의 타입 시스템 문제를 코틀린은 겪지 않는다.
자바의 제네릭의 타입 시스템은 invariant 하다. 이 말은 ‘List<String>‘ 은 ‘List<Object>‘ 의 서브 타입이 아니라는 뜻이다.
만약에 List 가 Invariant 하지 않다면 무슨 일이 일어날까? 다음 자바 예를 보면 알 수 있다.
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! A compile-time error here saves us from a runtime exception later.
objs.add(1); // Put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
List 가 invariant 하지 않으므로 List<Object>
변수인 objs 에 List<String>
타입 변수인 strs 를 대입하는게 가능해질 것이다.
그리고 실제 런타임 시점에 String s = strs.get(0) 을 하면 ClassCastException 이 발생할 것이다.
이런 invariant 는 문제를 예방해주지만 유연함을 제공해주지는 않는다. 다음 예시를 보자.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
// 와일드카드 타입을 사용하지 않은 pushAll 메소드는 결함이 있다.
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
Stack<Number>
로 선언하고 pushAll() 메소드에 Integer 타입을 넘기는 경우.자바는 이런 상황을 해결하기 위해서 한정적 와일드 카드 타입 (? extends E) 이라는 걸 제공해준다. 위 예시를 해결하기 위해서 pushAll() 의 파라미터는 다음과 같이 변경될 것이다.
public void pushAll(Iterable<? extends E> src {
for (E e : src) {
push(e);
}
}
List<A>
가 List<B>
의 하위 타입이 되는 것을 Covariant 라고 부른다.자 이번에는 반대의 경우를 한번 보자. Stack 에서 pushAll() 과 짝을 이루는 popAll() 메소드를 작성할 차례다.
public void popAll(Collection<E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
이번에도 와일드 타입을 사용해야한다. Stack 의 popAll() 메소드에 인자로 자신의 상위 타입은 허용해줘야한다. 즉 다음과 같이 될 것이다.
public void popAll(Collection<? super E> dst {
while(!isEmpty() {
dst.add(pop());
}
}
Collection<A>
가 Collection<B>
의 상위 타입인 경우를 contravariance 라고 한다.자바에서 다음과 같은 Source 인터페이스가 있다고 보자. 그냥 단순히 자신의 타입 객체를 리턴해주는 메소드가 있다.
// Java
interface Source<T> {
T nextT();
}
Source<Object>
에 Source<String>
을 넣어도 문제 없다.하지만 자바에서는 다음과 같은 작업은 허용되지 않는다.
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
Source<Object>
타입을 Source<? extends Object>
로 선언을 해줘야한다.Source<? extends Object>
로 만들어줘야한다.코틀린에서는 이 문제를 Declaration-site variance 를 통해서 해결한다. 이걸 이용하면 클래스 선언부에 타입을 선언해 놓는 것만으로도 invariant 가 아닌 Covariant 와 contravariance 를 제공해준다.
바로 예시로 보자. 다음은 Declaration-site 를 Producer 로서 사용하는 예제다.
interface Source <out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
다음은 Declartion-site 를 Consumer 로서 사용하는 예제다.
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, you can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
정리하면 틀린에서는 제네릭 타입 시스템에서 Declaration-site 를 통해 Contravaraicne 와 Covariant 를 제공해준다. 자바에서는 invariant 밖에 되지 않는데.
Use-site variance 는 Declartion-site 를 메소드 레벨로 줄인 것이다.
바로 실제 예시로보자.
class Array<T>(val size: Int) {
operator fun get(index: Int): T { ... }
operator fun set(index: Int, value: T) { ... }
}
이 경우에는 다음과 같은 copy() 작업에서 유연함을 제공해주지 못한다.
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ type is Array<Int> but Array<Any> was expected
하지만 다음과 같이 선언 해놓으면 딱 이 메소드의 경우에만 타입 변환 기능을 제공해줄 수 있다.
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
제네릭에서 아직은 어떤 타입이 올 지 모르는 경우, 하지만 타입의 관계는 명확히 정의해놓고 싶은 경우에 Star-projection 을 사용하면 된다.
바로 예시를 보자.
class Other
open class TUpper
class TChild : TUpper()
class Foo<out T : TUpper>
fun foo(bar: Foo<*>) {..}
fun demo() {
foo(Foo<TChild>()) // compile success
foo(Foo<Other>()) // compile error
}
*
를 통해서 사용할 수 있다.*
는 구체적인 타입이 정해지기 전에는 Any? 로 취급된다.*
를 선언해놨다. 하지만 그렇다고 해서 아무 타입의 Foo 객체를 다 받는 건 아니다. TUpper 의 하위 클래스만 들어올 수 있다.다음은 Star-projection 만 이용한 경우다.
fun acceptStarList(list: ArrayList<*>) {
list.add("문자열") // error!
}