Search
Duplicate
✂️

공변성, 반공변성, 불공변성

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를 사용하면 된다.
반대로 사용할 경우 어차피 컴파일에서 에러를 잡아준다.

주의할 점

한정적 와일드카드는 반환 타입으로 설정하면 안 된다.
유연성을 높여주기커녕 클라이언트 코드에서도 와일드카드 타입을 사용해야 하기 때문이다.