String이 Object의 하위 타입이어도 List<String>은 List<Object>의 하위 타입이 아니다. 이를 불공변(invariant)이라 한다.
왜 막았나
List<String> strs = new ArrayList<>();
List<Object> objs = strs; // 만약 이게 허용된다면
objs.add(42); // Integer 를 넣을 수 있다
String s = strs.get(0); // 꺼내면 ClassCastException
컴파일 시점에 막지 않으면 런타임에 터진다. 그래서 아예 대입을 금지한다.
배열은 공변이라 위험하다
Object[] arr = new String[1]; // 배열은 허용된다 (공변)
arr[0] = 42; // 컴파일은 통과
// 런타임: ArrayStoreException
배열의 이 설계가 실수였기 때문에 제네릭은 불공변으로 만들어졌다. "배열보다 리스트를 쓰라"는 조언의 근거 중 하나다.
PECS — 우회로
Producer Extends, Consumer Super
| ? extends T | 꺼내기만 한다 (생산자) | → 읽기 OK, 쓰기 금지 |
|---|---|---|
| ? super T | 넣기만 한다 (소비자) | → 쓰기 OK, 읽기는 Object 로만 |
// 꺼내 쓴다 → extends
double sum(List<? extends Number> nums) {
double s = 0;
for (Number n : nums) s += n.doubleValue(); // 읽기 OK
// nums.add(1); // 컴파일 에러 — 무엇이 들어갈지 모른다
return s;
}
sum(List.of(1, 2, 3)); // List<Integer> 도 받는다
sum(List.of(1.5, 2.5)); // List<Double> 도 받는다
// 넣는다 → super
void fill(List<? super Integer> dst) {
dst.add(1); // 쓰기 OK — Integer 의 상위면 무엇이든 담을 수 있다
// Integer i = dst.get(0); // 컴파일 에러 — 꺼내면 Object 로만 보인다
}
fill(new ArrayList<Object>());
왜 그 방향인가
? extends Number 는 "Number 이거나 그 하위 중 '무엇인가' 하나"
- 꺼내면 최소한 Number 다 (읽기 안전)
- 넣으려면 그게 Integer 인지 Double 인지 알아야 한다 (쓰기 불가)
? super Integer 는 "Integer 이거나 그 상위 중 '무엇인가' 하나"
- Integer 를 넣는 건 항상 안전하다 (쓰기 안전)
- 꺼내면 무엇인지 모른다. Object 로만 확실하다 (읽기 제한)
표준 라이브러리가 그대로 쓴다
// Collections.copy 의 시그니처가 PECS 의 교과서다
public static <T> void copy(List<? super T> dest, List<? extends T> src)
// 소비자 생산자
면접 함정
- ❌ "제네릭도 배열처럼 공변" → 불공변이다. 배열만 공변이고 그게 문제다.
- ❌ "
? extends면 아무거나 넣을 수 있다" → 오히려null외에는 못 넣는다.