•
Java 코드를 읽다보면 다음과 같은 다양한 제네릭 용법들이 보인다.
◦
List<String>
◦
List<E>
◦
List<? extends E>
◦
List<? super E>
•
변성은 상속 과정에서 제네릭 변수로 인해 이슈가 발생할 때 접하곤 한다.
변성이란?
•
변성이란 매개변수의 타입 파라미터에 타입 경계를 명시하여 상위 경계, 하위 경계를 정해주는 것이다.
•
변성에 대한 의미는 아래와 같이 설명이 가능하다.
의미 | |
공변성 | A가 B를 상속받을 때, C<B>는 C<A>의 하위 타입이다. |
반공변성 | A가 B를 상속받을 때, C<A>는 C<B>의 하위 타입이다. |
무공변성 | A가 B를 상속받을 때, C와 C<B>는 아무 관계가 없다. |
공변성
•
공변성은 타입 생성자에게 LSP를 허용하여 유연한 설계를 가능하게 해준다.
•
즉 제네릭을 사용할 때, 공변성을 사용하면 해당 타입이거나 서브 타입만 사용할 수 있게하는 것이다.
•
다음과 같이 <? extends E>로 사용한다.
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
Java
복사
반공변성
•
공변성의 반대 개념이다.
•
타입 생성자에게 자기 자신의 타입과 부모 타입만 허용하는 것이다.
•
다음과 같이 <? super E>로 사용한다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
Java
복사
불공변성
•
기본적으로 Kotlin, Java의 제네릭은 불공변성을 가지고 있다.
•
따라서 다음과 같이 작성한 코드는 오류가 발생한다.
◦
즉 자기 자신 타입을 제외한 모든 타입에서 오류가 발생한다.
public class Stack {
...
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
}
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers); // Error 발생
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects); // Error 발생
Java
복사
공변 / 반공변은 왜 쓸까?
•
위 코드만 보더라도 제네릭을 더욱 유연하게 사용할 수 있게 해주는 걸 확인할 수 있다.
•
기본 제네릭 만으로는 하나의 타입밖에 표현할 수가 없고, 이는 리스코프 치환 원칙에도 어긋나게 된다.
언제 어떤걸 사용해야 할까?
펙스 (PECS) : producer-extends, consumer-super
•
위 공식을 외우게 되면, 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.
•
즉, 생산자(producer)는 extends를 사용하고 소비자(consumer)는 super를 사용하면 된다.
•
반대로 사용할 경우 어차피 컴파일에서 에러를 잡아준다.
주의할 점
•
한정적 와일드카드는 반환 타입으로 설정하면 안 된다.
•
유연성을 높여주기커녕 클라이언트 코드에서도 와일드카드 타입을 사용해야 하기 때문이다.