프로그래밍

[Java] 제너릭(Generic) - 무공변성, 공변성, 반공변성

도둑탈을 쓴 애쉬 2023. 4. 16. 22:12

최범균님의 프로그래밍 초식 - 지네릭 변성(java) 를 보고 정리한 글입니다.

(출처: https://youtu.be/PtM44sO-A6g)

 

제너릭 예시 - List

자바에서 우리가 가장 쉽게 접하고 있는 제너릭 예시는 List 이다.

다음은 List 내부 구현이다.

public interface List<E> extends Collection<E> {

    boolean add(E e);
    
    E get(int index);

	/***/
}

 

List는 제너릭을 활용하기 때문에, 우리는 다양한 타입의 List를 선언할 수 있다.

List<String> messages = new ArrayList<>();
List<User> users = new ArrayList<>();

 

제너릭을 활용하면, 잘못된 타입의 값을 넣는 실수를 컴파일 타임에 예방을 해준다.

아래는 List에 잘못된 타입의 값 추가하는 예시이다. (컴파일 에러가 발생한다.)

List<String> codes = new ArrayList<>();
codes.add("1"); // OK
codes.add(1); // 컴파일 에러

 

public class User { }
public class Player extends User { }
public class Dealer extends User { }

User user = new Player(); // OK !

List<User> users = new ArrayList<>();
 users.add(new User());
 users.add(new Player()); // OK!
 users.add(new Dealer()); // OK!

 

List<Parent>는 List<Child>의 상위타입이 아니다.

다음과 같은 코드가 있다고 가정하자.

public class User { }
public class Player extends User { }
public class Dealer extends User { }

위 코드에서 User는 Player과 Dealer의 상위 타입이다.

하지만, List<User>는 List<Player>나 List<Dealer>의 상위 타입이 아니다.

 

기본적으로 자식 타입은 부모 타입으로 타입 변환이 가능하다.

따라서, Player 인스턴스를 User 타입 변수에 할당할 수있지만 
List<Player> 인스턴스를 List<User> 타입 변수에 할당할 수 없다. (상위 타입이 아니므로)

User user = new Player(); // OK
List<User> = new ArrayList<Player>(); // 컴파일 에러

( List는 인터페이스이므로, 예시에선 ArrayList를 활용했다. )

 

왜 List<User>는 List<Player>의 상위 타입이 아닌걸까?

아래의 예시를 보자.

List<User> users = new ArrayList<Player>(); // 1. 이게 된다면
Dealer dealer = new Dealer();
users.add(dealer); // 2. Dealer는 User이므로 가능하다
Player player = users.get(0); // 3. Dealer인데 Player ..?

List<User>가 List<Player>의 상위타입이라 가정하자.

1. 그렇다면 List<User> users 에 List<Player> 인스턴스를 할당할 수 있다.

2. Dealer는 User의 하위 타입이므로, dealer는 users에 추가될 수 있다.

3. users.get(0)연산의 값으로 dealer가 나오는데, users는 Player 리스트이다..

=> Dealer는 Player 다? (모순 발생)

 

List<User>가 List<Player>의 상위타입이라 가정하면 위와 같은 모순이 발생한다.

즉, List<User>는 List<Player>의 상위타입이 아니다.

 

무변성 (invariant)

A가 B의 상위 타입일 때,

T<A>가 T<B>의 상위 타입이 아니면

변성이 없음을 뜻한다.

 

즉, 위에서 본 List 예시가 무변성의 예시이다.

(List<User>는 List<Player>의 상위 타입이 아니므로.)

// User는 Player의 상위 타입이다.
User user = new Player(); // OK

// List<User>는 List<Player>의 상위 타입이 아니다.
List<User> users = new ArrayList<Player>(); // 컴파일 에러

 

무변성일 때 문제

유저들에게 돈을 나눠주는 Bank 클래스가 새롭게 추가되었다고 가정하자.

public class Bank {

    public void giveMoney(List<User> users) {
        /***/
    }
}

 

Player들에게 상금을 지급하고싶다고 하면, 다음과 같이 코드를 작성할 수 있을 것이다.

Bank bank = new Bank();
List<Player> players = new ArrayList<>();
bank.giveMoney(players); // 컴파일 에러

이 때, 위에서 말했듯 List<User>는 List<Player>의 상위 타입이 아니므로 컴파일 에러가 발생한다.

이 문제는 공변으로 해결할 수 있다.

 

공변 (covariant)

A가 B의 상위 타입이고

T<A>가 T<B>의 상위 타입이면

공변이다.

 

java에서는 extends 키워드를 사용해 공변 처리할 수 있다.

 

public class Bank {

    public void giveMoney(List<? extends User> users) {
        /***/
    }
}

위와 같이 extends 키워드를 사용해 공변으로 만들어줬다.

List<? extends User> 의 뜻은 List의 요소가 User의 하위타입이란 뜻이다.

 

Bank bank = new Bank();

List<User> users = new ArrayList<>();
bank.giveMoney(users); //OK

List<Player> players = new ArrayList<>();
bank.giveMoney(players); // OK

List<Dealer> dealers = new ArrayList<>();
bank.giveMoney(dealers); // OK

따라서, bank의 giveMoney 파라미터로 User 리스트 / Player 리스트 / Dealer 리스트 모두 전달할 수 있다.

 

공변에서 값 사용

공변에서 제너릭 타입을 사용하는 메서드에 값을 전달할 수 없다.

무슨 말인지 이해가 잘 가지 않으니 예시를 보면서 알아보자.

 

아래는 List 내부 구현을 간략화 한 코드이다.

public interface List<T> {

    boolean add(T t);
}

List의 add 메서드는 제너릭 타입을 사용한다.

즉, List가 공변일 경우 add에 값을 전달할 수 없다.

List<? extends User> users = new ArrayList<>();
users.add(new Player()); // 컴파일 에러

List<? extends User> 에 Player 객체를 추가할 시 컴파일 에러가 발생한다.

 

이는, List의 실제 타입을 특정할 수 없기 때문이다.

(Player인지, Lion인지 알 수 없다.)

 

이 문제는 반공변으로 해결할 수 있다.

 

반공변 (contravariant)

A가 B의 상위 타입이고

T<A>가 T<B>의 하위 타입이면

반공변이다.

 

java에서는 super 키워드를 사용해 반공변 처리할 수 있다.

 

즉, Parent가 Child의 상위 타입이면

List<Parent>는 List<? super Child>의 하위 타입이다.

// List<Player>는 List<? super Player>의 하위 타입이다.
List<? super Player> players = new ArrayList<>();
players.add(new Player());

// List<User>는 List<? super Player>의 하위 타입이다.
List<? super Player> players2 = new ArrayList<User>();
players.add(new Player());

 

PECS

어떨 때 공변 혹은 반공변을 사용하면 되는지 선택의 기준이다.

이는, producer - extends , consumer - super 의 줄임말이다.

 

값을 제공하면(produce) - extends 사용 (공변)

값을 사용하면(consume) - super 사용 (반공변)