Java의 공변(Covariant)과 불공변(Invariant)

Glen·2023년 11월 5일
3

배운것

목록 보기
26/38

서론

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 공식을 기억해 둬서 제네릭을 효과적으로 사용하는 법을 익히자.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글

관련 채용 정보