자바 언어·플랫폼 용어 사전
제네릭invariance · PECS · 와일드카드

제네릭 불공변

List<String>은 List<Object>가 아니다. 이 제약이 타입 안전을 지키고, PECS가 그 우회로다.

StringObject의 하위 타입이어도 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 외에는 못 넣는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 제네릭