최범균님의 프로그래밍 초식 - 지네릭 변성(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 사용 (반공변)
'프로그래밍' 카테고리의 다른 글
[소프트웨어 아키텍처] 계층형 아키텍처에서 헥사고날 아키텍처(Hexagonal Architecture)로 (1) | 2023.05.22 |
---|---|
[소프트웨어 아키텍처] 레이어드 아키텍처(Layered Architecture)란? (5) | 2023.04.25 |
[Java] LinkedHashMap (0) | 2023.03.13 |
[Java] 함수형 인터페이스와 람다 (5) | 2023.03.12 |
[Java] 복사와 불변 (new, unmodifiable, copyOf) (2) | 2023.02.24 |