Java의 제네릭을 사용하고, 개념에 대해서 배우면 공변
과 불공변
에 관한 얘기가 나온다.
단어가 난해하고, 실생활에 접하기 힘든 단어라 어렵다고 생각할 수 있지만 사실 간단한 용어이다.
그렇다면 자바에서 공변과 불공변이 무엇인지 알아보자.
공변(Covariant)이란 SubType이 SuperType의 하위 타입일 때, SubType은 SuperType이 될 수 있는 것을 말한다.
class Animal {
...
}
class Cat extends Animal {
...
}
Animal animal = new Cat();
불공변(Invariant)이란 SubType이 SuperType의 하위 타입일 때, SubType은 SuperType이 될 수 없고, SuperType도 SubType이 될 수 없다는 것을 말한다.
Java에서는 제네릭이 불공변하다는 특성을 가진다.
List<Animal> animals = new ArrayList<Cat>(); // 컴파일 에러
이러한 제네릭 타입의 불공변한 특징은 한정적인 기능을 제공한다.
제네릭이 불공변하게 제공되는 이유는 이펙티브 자바 - 아이템 28에 나와있다.
다음과 같이 add()
와 addAll()
메서드가 있다.
public class MyCollection<T> {
private Collection<T> collection;
public MyCollection(Collection<T> collection) {
this.collection = collection;
}
public void add(T t) {
collection.add(t);
}
public void addAll(Collection<T> collection) {
this.collection.addAll(collection);
}
}
이때 add()
메서드는 공변이 가능하지만, addAll()
메서드는 불공변하다. (매개변수 타입이 제네릭 이므로)
MyCollection<Number> myCollection = new MyCollection<>(new ArrayList<>());
myCollection.add(1); // 정상
List<Integer> ints = List.of(2, 3, 4, 5);
myCollection.addAll(ints); // 컴파일 에러
하지만 메서드의 역할을 봤을 때 addAll()
은 공변 해야한다.
따라서 이 경우 제네릭의 한정적 타입 매개변수
기능을 활용할 수있다.
public class MyCollection<T> {
...
public void addAll(Collection<? extends T> collection) {
this.collection.addAll(collection);
}
...
}
이렇게 하면 파라미터로 들어오는 제네릭 타입을 공변하게 할 수 있다.
한정적 타입 매개변수에는 super
키워드도 적용할 수 있는데, 해당 키워드는 제네릭 타입을 반공변
하게 한다.
반공변(contravariant)은 공변의 반대로, SubType이 SuperType의 하위 타입일 때, SuperType이 SubType이 될 수 있는 것을 말한다.
공변은 다형성을 제공하여 객체지향적인 프로그래밍을 가능하게 하지만, 반공변은 어디에 사용할 곳이 있는지 싶다.
이펙티브 자바 - 아이템 31
에서는 다음과 같은 공식이 있다.
펙스(PECS): producer-extends, consumer-super
매개변수 T가 생산자이면 \<? extends T>를 사용하고, 소비자이면 \<? super T>를 사용하라는 공식이다.
여기 기존의 컬렉션을 다른 컬렉션으로 옮기는 메서드가 있다.
public class MyCollection<T> {
...
public void transfer(MyCollection<T> other) {
other.addAll(collection);
collection.clear();
}
...
}
여기서 매개변수의 T는 생산자가 아닌, 소비자이다.
따라서 super
와일드카드 타입을 사용해야 한다.
만약 사용하지 않는다면 src의 값을 dst로 옮길 때, 컴파일 에러가 발생한다.
MyCollection<Number> src = new MyCollection<>(new ArrayList<>());
MyCollection<Object> dst = new MyCollection<>(new ArrayList<>());
src.add(2.0);
src.add(new AtomicInteger());
src.transfer(dst); // 컴파일 에러
src는 Number 타입을 가지므로, Number를 상속한 타입을 담을 수 있다.
dst는 Object 타입을 가지므로, Number의 타입도 당연하게 담을 수 있다.
하지만 컴파일 에러가 발생한다. (파라미터가 반공변하지 않으므로)
따라서 매개변수 T가 소비자라면 반공변하게 만들어 주어야 한다.
public class MyCollection<T> {
...
public void transfer(MyCollection<? super T> other) {
other.addAll(collection);
collection.clear();
}
...
}
공변과 불공변의 개념에 대해 간단하게 알아보았다.
단어가 어렵게 들려서 그렇지, 사실 어려운 개념이 아니다.
PECS 공식을 기억해 둬서 제네릭을 효과적으로 사용하는 법을 익히자.