1. 개요
List<? extends Number> 와 List<? super Number>, 처음 보면 둘 다 그냥 “숫자 리스트” 처럼 보이지만 하나는 읽기 전용이고 하나는 쓰기가 된다. 이 글은 제네릭 와일드카드가 왜 필요한지, PECS 원칙으로 언제 뭘 쓰는지 정리한다.
2. 핵심 내용
2-1. 제네릭이 없던 시절
Java 5 이전에는 컬렉션이 전부 Object 를 담았다. 꺼낼 때마다 캐스팅이 필요했고, 엉뚱한 타입을 넣어도 컴파일 시점엔 안 걸렸다. 제네릭은 이 문제를 컴파일 시점으로 끌어온다. List<String> 에 Integer 를 넣으려 하면 그 자리에서 컴파일 에러가 난다.
2-2. 제네릭은 불변(invariant)이다
Integer 는 Number 의 하위 타입이지만, List<Integer> 는 List<Number> 의 하위 타입이 아니다.
List<Number> numbers = new ArrayList<Integer>(); // 컴파일 에러이유는 만약 허용된다면 numbers.add(3.14) 로 Double 을 넣을 수 있게 되는데, 실제로는 Integer 리스트이므로 런타임에 깨진다. 이걸 막으려고 제네릭은 기본적으로 불변이다. 하지만 “숫자 종류라면 뭐든 읽기만 할 건데” 같은 상황에서는 이 불변성이 너무 빡빡하다. 여기서 와일드카드가 필요해진다.
2-3. PECS: Producer Extends, Consumer Super
Joshua Bloch 가 정리한 원칙이다. 해당 타입 파라미터가 데이터를 꺼내기만(생산) 한다면 extends, 데이터를 넣기만(소비) 한다면 super 를 쓴다.
// Producer: 꺼내서 합산만 함 -> extends
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
// Consumer: 값을 넣기만 함 -> super
static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
sum(List.of(1, 2, 3)); // List<Integer> 도 허용
sum(List.of(1.1, 2.2)); // List<Double> 도 허용
List<Number> nums = new ArrayList<>();
addIntegers(nums); // List<Number> 는 List<? super Integer> 를 만족sum 은 리스트 안 값을 읽기만 하므로 ? extends Number 로 Integer, Double 리스트를 모두 받을 수 있다. addIntegers 는 Integer 를 넣기만 하므로 ? super Integer 로 Integer, Number, Object 리스트를 모두 받을 수 있다.
2-4. ? extends T 에는 왜 add 가 안 되나
List<? extends Number> list = new ArrayList<Integer>();
list.add(1); // 컴파일 에러컴파일러 입장에서 list 의 실제 타입은 Integer 일 수도 Double 일 수도 있다. 어떤 타입인지 모르니 아무 값도 안전하게 넣을 수 없다(null 만 예외). 그래서 extends 는 사실상 읽기 전용이 된다. 이 원칙은 컬렉션 프레임워크 의 Collections.copy() 같은 유틸리티 메서드 시그니처에서도 그대로 쓰인다.
3. 마무리
요약
- 제네릭은 불변이다.
List<Integer>는List<Number>의 하위 타입이 아니다.- 값을 꺼내서 쓰기만 하면(Producer)
? extends T, 값을 넣기만 하면(Consumer)? super T.? extends T는 실제 타입을 알 수 없어null외의 값을 넣을 수 없다(사실상 읽기 전용).- PECS 원칙은 라이브러리 유틸리티 메서드 시그니처를 이해하는 데도 그대로 적용된다.