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

Glen·2023년 11월 5일
3

배운것

목록 보기
26/37

서론

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개의 댓글