3주차 Unit 5.3 — 와일드카드 ? extends, ? super

Psj·2026년 5월 19일

F-lab

목록 보기
94/238

Unit 5.3 — 와일드카드 ? extends, ? super

F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 제네릭의 불공변성 (Invariance) 이 무엇이고 왜 그렇게 설계되었나?
  • 와일드카드 ? 의 정확한 의미는?
  • <?> (unbounded) 의 의미와 활용은?
  • <? extends T> (upper bounded) 의 공변성 (Covariance) 은?
  • <? super T> (lower bounded) 의 반공변성 (Contravariance) 은?
  • 공변성 vs 반공변성 의 차이는?
  • 와일드카드 캡처 (Wildcard Capture) 가 무엇인가?
  • 타입 매개변수 vs 와일드카드 의 선택 기준은?
  • List<Object> vs List<?> 의 결정적 차이는?

🎯 핵심 한 문장

제네릭은 기본적으로 불공변 (invariant) 이라 List<String>List<Object> 의 자식이 아니다.
이 제약을 해결하기 위해 와일드카드 ? 가 도입되었고,
? extends T 는 "T 또는 그 자식" 을 받는 공변 (Producer), ? super T 는 "T 또는 그 부모" 를 받는 반공변 (Consumer) 의미.
와일드카드 덕분에 List<? extends Number> 같은 메서드 매개변수가 Integer, Double, Long 의 List 를 모두 받을 수 있고,
다음 Unit 5.5 의 PECS 원칙 으로 이를 체계적으로 활용한다.

비유 — 음식점의 메뉴 정책

List<String> = List<Object> ?

  같은 가게에서 "야채만 받는 가게" 와 "어떤 음식이든 받는 가게" 는
  같지 않다.
  
  야채 가게에 음식을 추가할 권한을 주면 → 고기 추가 가능 → 야채 가게 아님
  → 불공변

와일드카드 ? extends Number:
  "Number 의 한 종류 가게"
  - Integer 가게, Long 가게, Double 가게 모두 받음
  - 단, 새 음식 추가는 불가 (어떤 종류인지 모름)
  - 꺼내는 것만 가능 (꺼낸 건 분명히 Number)

와일드카드 ? super Number:
  "Number 까지 처리하는 가게"
  - Number 가게, Object 가게 받음
  - 어떤 Number 든 추가 가능 (Number 또는 그 부모니까)
  - 단, 꺼낼 때는 Object 로만 (정확한 타입 모름)

→ 와일드카드 = 불공변성의 해결 + 안전한 다형성.


🧭 9개 섹션 로드맵

1. 제네릭의 불공변성 (Invariance)
2. 와일드카드 ? 의 등장
3. unbounded wildcard <?>
4. upper bounded wildcard <? extends T> — 공변성
5. lower bounded wildcard <? super T> — 반공변성
6. 공변 vs 반공변 비교
7. 와일드카드 캡처
8. 타입 매개변수 vs 와일드카드
9. 면접 + 자기 점검

1️⃣ 제네릭의 불공변성 (Invariance)

1.1 불공변성의 정의

불공변성 (Invariance):

  타입 A 가 타입 B 의 자식이어도,
  Generic<A> 와 Generic<B> 는 무관한 타입.

자바 제네릭의 규칙:
  Integer extends Number   (Integer 는 Number 자식)
  하지만:
  List<Integer> 와 List<Number> 는 무관 (자식 아님)

1.2 불공변성의 시연

// Integer 는 Number 의 자식
Integer i = 42;
Number n = i;   // ✓ OK — 자식 → 부모

// 하지만 제네릭은 불공변
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;   // ❌ 컴파일 에러!

// 같은 방식
List<String> strings = new ArrayList<>();
List<Object> objects = strings;   // ❌ 컴파일 에러!

1.3 불공변성이 필요한 이유

// 만약 공변성을 허용했다면 (가상):
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;   // 가상으로 허용

nums.add(3.14);   // Double 추가 (Number 의 자식)
// 실제로는 List<Integer> 에 Double 들어감 → 깨짐

Integer i = ints.get(0);   // Double 을 Integer 로 → ClassCastException!

→ 제네릭의 불공변성은 타입 안전성을 위한 의도적 제약.

1.4 배열의 공변성 (대조)

// 자바 배열은 공변
Integer[] ints = new Integer[10];
Number[] nums = ints;   // ✓ OK — 배열은 공변

nums[0] = 3.14;   // ❌ ArrayStoreException (런타임)
// 런타임에 검사하지만 컴파일은 통과

// 이게 자바 배열의 단점

비교:

  • 배열: 공변, 단 런타임 검사
  • 제네릭: 불공변, 컴파일 타임 검사
  • 제네릭이 더 안전

1.5 불공변성의 효과

효과:

1. 컴파일 타임 안전성
   - 잘못된 타입 매개변수 검출
   - 런타임 ClassCastException 회피

2. 명확한 의도
   - List<Integer> = "Integer 만 담는 List"
   - List<Number> = "Number 만 담는 List"
   - 서로 다른 책임

3. 다형성 제약
   - 단점이기도 함
   - 와일드카드로 해결

1.6 불공변성의 한계 — 메서드 매개변수

// 합 메서드 - List<Number> 만 받음
public double sum(List<Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

// 호출
List<Number> nums = new ArrayList<>();
sum(nums);   // ✓ OK

List<Integer> ints = new ArrayList<>();
sum(ints);   // ❌ 컴파일 에러!
// List<Integer> 는 List<Number> 가 아님 (불공변)

List<Double> doubles = new ArrayList<>();
sum(doubles);   // ❌ 컴파일 에러!

→ Integer 의 List 도 합을 구할 수 있는데, 메서드가 거부.
→ 와일드카드로 해결 (다음 섹션).

1.7 불공변성의 우회 — 와일드카드

// 해결: List<? extends Number>
public double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

// 호출
List<Number> nums = ...;
sum(nums);   // ✓ OK

List<Integer> ints = ...;
sum(ints);   // ✓ OK!

List<Double> doubles = ...;
sum(doubles);   // ✓ OK!

핵심:

  • <? extends Number> = "Number 의 자식이라면 어떤 List 든"
  • 공변성 허용
  • 단, 추가 제약 있음 (다음 섹션)

1.8 자기 점검 답변

제네릭이 불공변인 이유는?

:

  • 타입 안전성:

    • List 가 List 의 자식이면
    • nums.add(3.14) → Integer List 에 Double 추가
    • ClassCastException 위험
  • 컴파일 타임 검증:

    • 잘못된 타입 매개변수 즉시 검출
    • 런타임 에러 회피
  • 명확한 의도:

    • List = "Integer 전용"
    • 분명한 책임
  • 배열과 대조:

    • 배열: 공변 + 런타임 검사 (덜 안전)
    • 제네릭: 불공변 + 컴파일 검사 (더 안전)

→ 단점 (다형성 제약) 은 와일드카드가 해결.


2️⃣ 와일드카드 ? 의 등장

2.1 와일드카드의 정의

와일드카드 (Wildcard) ?:
  
  "어떤 타입" 을 의미하는 placeholder.
  타입 매개변수 자리에 사용.

문법:
  List<?>            ← unbounded wildcard
  List<? extends T>  ← upper bounded
  List<? super T>    ← lower bounded

2.2 등장 이유

문제:
  List<Integer> 를 List<Number> 처럼 사용 못 함 (불공변)
  
해결 방안 1: 무한히 오버로딩
  sum(List<Integer>)
  sum(List<Double>)
  sum(List<Long>)
  ...
  → 비효율

해결 방안 2: raw type
  sum(List numbers)
  → 안전성 손실, 경고

해결 방안 3: 와일드카드 ★
  sum(List<? extends Number> numbers)
  → 공변성 + 안전성

2.3 세 종류의 와일드카드

// 1. unbounded — 어떤 타입이든
List<?> anything;

// 2. upper bounded — T 또는 자식 (공변)
List<? extends Number> numberOrChild;

// 3. lower bounded — T 또는 부모 (반공변)
List<? super Integer> integerOrParent;

2.4 와일드카드 vs 타입 매개변수

// 타입 매개변수 — 정의 시
public class Box<T> {     // T 는 매개변수
    private T value;       // T 사용
}

// 와일드카드 — 사용 시
public void process(Box<?> box) {   // ? 는 와일드카드
    // box 안에 뭐가 들어있는지 모름
}

차이:

  • 타입 매개변수 (T): 정의 시점, 본체에서 활용
  • 와일드카드 (?): 사용 시점, 본체에서 활용 제한

2.5 와일드카드의 의미

// List<?> — "어떤 타입의 List 든"
public void printList(List<?> list) {
    for (Object o : list) {   // Object 로만 접근
        System.out.println(o);
    }
}

// 호출
List<String> strings = ...;
List<Integer> ints = ...;
List<Shipment> shipments = ...;

printList(strings);    // ✓
printList(ints);       // ✓
printList(shipments);  // ✓

→ 매개변수 타입의 유연성.

2.6 와일드카드의 제약

public void process(List<?> list) {
    // 읽기는 가능 (Object 로만)
    Object o = list.get(0);
    
    // 쓰기는 매우 제한
    list.add(null);          // ✓ null 만 가능
    // list.add("hello");    // ❌ 컴파일 에러
    // list.add(42);         // ❌ 컴파일 에러
    
    // 이유: 어떤 타입의 List 인지 모름
    // List<Integer> 일 수도, List<String> 일 수도
    // 안전을 위해 추가 금지
}

<?>읽기 전용 에 가깝다.

2.7 자기 점검 답변

와일드카드 ? 가 필요한 이유는?

:
1. 불공변성 우회:

  • List 를 List 매개변수에 못 넣음
  • <? extends Number> 로 해결
  1. 유연한 메서드:

    • 한 메서드로 다양한 타입 매개변수 받기
    • printList(List<?>) 가 모든 List 받음
  2. 타입 안전성 유지:

    • raw type 보다 안전
    • 컴파일러 검증
  3. 세 가지 변형:

    • unbounded: 어떤 타입이든
    • upper bounded: 공변
    • lower bounded: 반공변

3️⃣ unbounded wildcard <?>

3.1 정의

unbounded wildcard:
  
  List<?>
  
  의미: "어떤 타입의 List 든"
  
  사실상 List<? extends Object> 와 동등
  (Object 가 모든 클래스의 부모)

3.2 활용 예

// 어떤 List 든 받는 메서드
public static int countItems(List<?> list) {
    return list.size();
}

// 호출
countItems(List.of("a", "b"));        // List<String>
countItems(List.of(1, 2, 3));         // List<Integer>
countItems(List.of(new Shipment()));  // List<Shipment>

→ List 의 사이즈만 알면 되는 작업.

3.3 unbounded 의 읽기/쓰기

public void process(List<?> list) {
    
    // 읽기 — Object 로만
    for (Object o : list) {   // ✓
        System.out.println(o);
    }
    
    Object first = list.get(0);   // ✓ Object 반환
    int size = list.size();       // ✓
    boolean empty = list.isEmpty();   // ✓
    
    // 쓰기 — null 만
    list.add(null);   // ✓
    // list.add("hello");   // ❌
    // list.add(42);        // ❌
    
    // 메타 정보 작업
    list.clear();    // ✓ 비우기
    list.remove(0);  // ✓ 인덱스로 제거
    
    // 변경 위치 작업도 OK
    Collections.reverse(list);   // ✓
    Collections.shuffle(list);   // ✓
}

→ 읽기 + 메타 작업 OK, 새 요소 추가 ❌.

3.4 List<?> vs List
// 큰 차이!

// 1. List<Object> — Object 의 List
List<Object> objects = new ArrayList<>();
objects.add("hello");   // ✓
objects.add(42);        // ✓
objects.add(new Object());   // ✓

// 2. List<?> — 어떤 타입의 List
List<?> wildcard = new ArrayList<String>();
// wildcard.add("hello");   // ❌ 추가 못 함

// 호환성
List<String> strings = new ArrayList<>();
List<Object> objs = strings;   // ❌ 컴파일 에러 (불공변)
List<?> wild = strings;        // ✓ OK (와일드카드)

// 정리:
// - List<Object> = Object 전용
// - List<?> = "어떤 타입이든" (불특정)

3.5 활용 시점

// 1. 타입에 무관한 작업
public static void shuffleAll(List<?> list) {
    Collections.shuffle(list);
}

public static int totalSize(Collection<?>... collections) {
    int total = 0;
    for (Collection<?> c : collections) {
        total += c.size();
    }
    return total;
}

public static boolean isEmptyOrNull(List<?> list) {
    return list == null || list.isEmpty();
}

// 2. 출력
public static void printAll(Collection<?> items) {
    for (Object item : items) {
        System.out.println(item);
    }
}

// 3. 타입 검사 (Class<?>)
public static boolean isOfType(Object obj, Class<?> clazz) {
    return clazz.isInstance(obj);
}

3.6 자바 표준의 unbounded 활용

// Class<?>
Class<?> clazz = String.class;   // 어떤 Class 든

// Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
// 단순 copy 는 List<?> 도 가능

// 반환 타입
public Class<?> getClass() { ... }   // Object 의 메서드

3.7 List<?> 의 한계와 우회

public void process(List<?> list) {
    // ❌ 새 요소 추가 못 함
    // list.add("hello");
    
    // 우회 1: 헬퍼 메서드로 타입 매개변수화
    addToList(list, "hello");
}

private <T> void addToList(List<T> list, T item) {
    list.add(item);   // ✓ T 가 명확해서 가능
}

// 단, addToList(list, "hello") 호출 시
// list 가 List<?> 라면 T 를 추론하지 못해 컴파일 에러
// List<String> 이어야 가능

→ unbounded 의 추가는 본질적으로 어렵다.

3.8 자기 점검 답변

List<?> 와 List 의 차이는?

:

  • List:

    • Object 의 List (구체 타입)
    • 어떤 객체든 추가 가능
    • List 을 받지 못함 (불공변)
  • List<?>:

    • "어떤 타입의 List 든" (불특정)
    • 새 요소 추가 불가 (null 제외)
    • List, List 등 모두 받음
  • 언제 어느 것?:

    • 어떤 타입의 List 든 받고 싶음 + 추가 불필요 → <?>
    • Object 의 List 가 정말 필요 → <Object>
    • 거의 항상 <?> 가 정답

    4️⃣ upper bounded wildcard <? extends T> — 공변성

    4.1 정의

    upper bounded wildcard:
      
      List<? extends Number>
      
      의미: "Number 또는 그 자식 타입의 List"
      
      허용:
        List<Number>
        List<Integer>
        List<Long>
        List<Double>
        List<BigDecimal>
      
      불허:
        List<String>   ← String 은 Number 아님
        List<Object>   ← Object 는 Number 부모

    4.2 공변성 (Covariance)

    공변성:
      타입 매개변수가 자식 방향으로 호환되는 성질
      
    일반:
      Integer is-a Number  (Integer 가 Number 자식)
      
    공변:
      List<Integer> is-a List<? extends Number>
      → "Number 의 List" 라는 의미에서 Integer 의 List 도 OK

    4.3 활용 예 — sum 메서드

    // 잘못된 시도 (불공변)
    public double sumOf(List<Number> numbers) {
        double total = 0;
        for (Number n : numbers) {
            total += n.doubleValue();
        }
        return total;
    }
    
    // List<Integer> 못 받음
    List<Integer> ints = List.of(1, 2, 3);
    sumOf(ints);   // ❌ 컴파일 에러
    
    // 와일드카드로 해결
    public double sumOf(List<? extends Number> numbers) {
        double total = 0;
        for (Number n : numbers) {   // Number 로 읽기 가능
            total += n.doubleValue();
        }
        return total;
    }
    
    // 호출
    sumOf(List.of(1, 2, 3));         // ✓ List<Integer>
    sumOf(List.of(1.0, 2.0));        // ✓ List<Double>
    sumOf(List.of(1L, 2L));          // ✓ List<Long>
    sumOf(List.<Number>of(1, 2.0, 3L));   // ✓ List<Number>

    4.4 읽기는 가능, 쓰기는 불가

    public void process(List<? extends Number> list) {
        
        // 읽기 — Number 또는 자식이라 Number 로 받음
        for (Number n : list) {   // ✓
            System.out.println(n.doubleValue());
        }
        
        Number first = list.get(0);   // ✓
        
        // 쓰기 — 어떤 자식인지 모르므로 불가
        // list.add(1);          // ❌
        // list.add(1.0);        // ❌
        // list.add(new BigDecimal("1"));   // ❌
        
        // 단, null 은 가능 (모든 타입의 자식)
        list.add(null);   // ✓
        
        // 이유:
        // list 가 List<Integer> 일 수도, List<Double> 일 수도
        // 어떤 타입을 추가해도 안전 보장 X
    }

    4.5 왜 쓰기 불가?

    // 가상 시나리오:
    List<? extends Number> numbers;
    
    // numbers 가 실제로는 List<Integer> 일 수 있음
    numbers = new ArrayList<Integer>();
    
    // 만약 add 허용한다면:
    numbers.add(3.14);   // Double 추가
    // List<Integer> 에 Double 들어감 → 깨짐!
    
    // 그래서 컴파일러가 add 차단

    4.6 Producer 의 역할

    <? extends T> 는 "Producer (생산자)" 역할
    
    - 데이터를 꺼내는 (read) 용도
    - 외부에 데이터 제공
    - 입력만 받음
    
    비유:
      과일 가게 (사과 또는 사과 자식)
      - 손님이 사과를 사가는 것 가능 (꺼내기 OK)
      - 손님이 사과를 추가하지 X (추가는 불가)

    PECS 원칙 (Unit 5.5):

    • Producer Extends
    • 생산자 → extends 사용

    4.7 흔한 사용 예

    // 1. Collections.copy
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        //                                            ↑
        //                                            Producer
        // src 에서 읽어와서 dest 로 복사
    }
    
    // 2. addAll
    public boolean addAll(Collection<? extends E> c) {
        //                       ↑
        //                       Producer
        // c 의 요소를 자신에게 추가
    }
    
    // 3. Stream.collect
    <R, A> R collect(Collector<? super T, A, R> collector);
    
    // 4. ILIC 예
    public BigDecimal totalWeight(List<? extends Shipment> shipments) {
        BigDecimal total = BigDecimal.ZERO;
        for (Shipment s : shipments) {
            total = total.add(s.getWeight());
        }
        return total;
    }
    
    // 사용
    List<SeaShipment> seaShipments = ...;
    totalWeight(seaShipments);   // ✓
    
    List<AirShipment> airShipments = ...;
    totalWeight(airShipments);   // ✓

    4.8 자기 점검 답변

    <? extends T> 의 의미와 효과는?

    :

    • 의미:

      • "T 또는 그 자식 타입의 List"
      • 공변성 (Covariance)
      • List 는 List<? extends Number> 의 일종
    • 읽기/쓰기:

      • 읽기: T 로 가능
      • 쓰기: null 제외 불가 (어떤 자식인지 모름)
    • 용도:

      • Producer (생산자, 데이터 꺼내기)
      • 메서드 매개변수의 유연성
      • 다형성 활용

    → "안전한 다형성".


    5️⃣ lower bounded wildcard <? super T> — 반공변성

    5.1 정의

    lower bounded wildcard:
      
      List<? super Integer>
      
      의미: "Integer 또는 그 부모 타입의 List"
      
      허용:
        List<Integer>
        List<Number>
        List<Object>
      
      불허:
        List<Long>     ← Integer 부모 아님
        List<Byte>     ← Integer 부모 아님

    5.2 반공변성 (Contravariance)

    반공변성:
      타입 매개변수가 부모 방향으로 호환되는 성질
      
    일반:
      Integer is-a Number
      
    반공변:
      List<Number> is-a List<? super Integer>
      → "Integer 까지 처리하는 List" 라는 의미에서 Number 의 List 도 OK
      
    (공변과 반대 방향)

    5.3 활용 예 — copy 메서드

    // Integer 를 List 에 추가하는 메서드
    public void addIntegers(List<? super Integer> list) {
        for (int i = 0; i < 10; i++) {
            list.add(i);   // ✓ Integer 추가 가능
        }
    }
    
    // 호출
    List<Integer> ints = new ArrayList<>();
    addIntegers(ints);   // ✓
    
    List<Number> nums = new ArrayList<>();
    addIntegers(nums);   // ✓ (Integer 도 Number 니까)
    
    List<Object> objs = new ArrayList<>();
    addIntegers(objs);   // ✓ (Integer 도 Object 니까)

    5.4 쓰기는 가능, 읽기는 Object 로만

    public void process(List<? super Integer> list) {
        
        // 쓰기 — Integer 또는 그 자식이라 Integer 추가 가능
        list.add(42);              // ✓
        list.add(Integer.MAX_VALUE);   // ✓
        list.add(Integer.valueOf(0));   // ✓
        
        // Integer 자식이면 가능? Integer 는 final 이라 자식 없음
        // 그러나 Number 자식 (Double 등) 은 불가
        // list.add(3.14);   // ❌
        
        // 읽기 — Object 로만 (정확한 타입 모름)
        Object o = list.get(0);   // ✓
        // Integer i = list.get(0);   // ❌ 캐스트 필요
        
        // 이유:
        // list 가 List<Integer> 일 수도, List<Number> 일 수도, List<Object> 일 수도
        // 가장 좁은 보장은 Object
    }

    5.5 Consumer 의 역할

    <? super T> 는 "Consumer (소비자)" 역할
    
    - 데이터를 받아 추가 (write) 용도
    - 외부에서 데이터 받음
    - 출력만 함
    
    비유:
      쓰레기통 (사과 또는 사과 부모 = 과일 통)
      - 사과를 통에 넣을 수 있음 (추가 OK)
      - 뭐가 들어있는지 정확히 모름 (꺼낼 때 Object)

    PECS 원칙:

    • Consumer Super
    • 소비자 → super 사용

    5.6 흔한 사용 예

    // 1. Collections.copy
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        //                              ↑
        //                              Consumer
        // src 에서 읽어 dest 에 추가
    }
    
    // 2. Collections.addAll
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        //                                       ↑
        //                                       Consumer
        // c 에 elements 추가
    }
    
    // 3. Comparator
    Comparator<? super T> comparator;   // T 또는 부모를 비교
    
    // 4. ILIC 예
    public void collectShipments(List<? super Shipment> destination) {
        for (Shipment s : findAll()) {
            destination.add(s);
        }
    }
    
    // 사용
    List<Shipment> shipments = new ArrayList<>();
    collectShipments(shipments);   // ✓
    
    List<Object> all = new ArrayList<>();
    collectShipments(all);   // ✓ (Object 가 Shipment 부모)

    5.7 ? super 의 활용 — Comparator

    // Comparator<T> 사용 시 ? super T 가 자주 등장
    public class Shipment { ... }
    public class SeaShipment extends Shipment { ... }
    
    // Shipment 비교기
    Comparator<Shipment> byWeight = (a, b) -> a.getWeight().compareTo(b.getWeight());
    
    // 정렬 메서드 시그니처
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        //                                              ↑
        //                                              Consumer
        // c 는 T 또는 그 부모를 받는 Comparator
    }
    
    // 사용
    List<SeaShipment> seaShipments = ...;
    sort(seaShipments, byWeight);   // ✓
    // byWeight 는 Comparator<Shipment>
    // SeaShipment 의 부모 Shipment 의 Comparator → OK

    → "SeaShipment 정렬에 Shipment 비교기를 쓸 수 있다" 의 의미.

    5.8 자기 점검 답변

    <? super T> 의 의미와 효과는?

    :

    • 의미:

      • "T 또는 그 부모 타입의 List"
      • 반공변성 (Contravariance)
      • List 는 List<? super Integer> 의 일종
    • 읽기/쓰기:

      • 쓰기: T 와 그 자식 가능
      • 읽기: Object 로만 (정확한 타입 모름)
    • 용도:

      • Consumer (소비자, 데이터 추가)
      • addAll, copy 의 destination
      • Comparator<? super T>

    → "데이터 받을 때 유연성".


    6️⃣ 공변 vs 반공변 비교

    6.1 비교 표

    항목<? extends T><? super T>
    의미T 또는 자식T 또는 부모
    변성공변 (Covariant)반공변 (Contravariant)
    읽기T 로 가능Object 로만
    쓰기null 제외 불가T 와 자식 가능
    역할Producer (생산자)Consumer (소비자)
    키워드extendssuper
    시각자식 방향부모 방향

    6.2 시각적 비교

    타입 계층:
    
      Object
        │
        Number
          │
          Integer
    
    읽기 <? extends Number>:
      - List<Number>, List<Integer> 받음
      - 꺼낸 건 분명히 Number (또는 자식)
      - Number 로 안전하게 사용
    
    쓰기 <? super Integer>:
      - List<Integer>, List<Number>, List<Object> 받음
      - 어떤 List 든 Integer 추가 가능
      - 모두 Integer 부모니까

    6.3 코드 비교

    // Producer (꺼내기) — extends
    public double sumOf(List<? extends Number> source) {
        double total = 0;
        for (Number n : source) {   // 꺼내기
            total += n.doubleValue();
        }
        return total;
    }
    
    // Consumer (추가) — super
    public void fillWithIntegers(List<? super Integer> dest) {
        for (int i = 0; i < 10; i++) {
            dest.add(i);   // 추가
        }
    }
    
    // 둘 다 활용 — Collections.copy
    public static <T> void copy(
            List<? super T> dest,        // Consumer (추가)
            List<? extends T> src) {     // Producer (꺼내기)
        for (T item : src) {
            dest.add(item);
        }
    }

    6.4 PECS 원칙 (예고)

    PECS:
      Producer Extends, Consumer Super
    
    매개변수 선택 가이드:
    
    데이터를 꺼내는 매개변수 → ? extends T
      - 메서드 입력 (in)
      - 생산자 역할
      - 예: List<? extends Number> input
    
    데이터를 추가하는 매개변수 → ? super T
      - 메서드 출력 (out)
      - 소비자 역할
      - 예: List<? super Integer> output
    
    → Unit 5.5 에서 정밀

    6.5 같은 매개변수의 두 변형

    // 시나리오: 두 List 의 원소를 옮기는 메서드
    
    // 1. 같은 타입 (제약적)
    public <T> void transfer(List<T> from, List<T> to) {
        while (!from.isEmpty()) {
            to.add(from.remove(0));
        }
    }
    
    // List<Integer>, List<Integer> 가능
    // List<Integer>, List<Number> 불가
    
    // 2. 유연성 (PECS)
    public <T> void transfer(List<? extends T> from, List<? super T> to) {
        while (!from.isEmpty()) {
            to.add(from.remove(0));
        }
    }
    
    // List<Integer> 에서 List<Number> 로 OK
    // List<Integer> 에서 List<Object> 로 OK

    → PECS 가 메서드를 더 유연하게.

    6.6 자바 표준의 PECS 활용

    // Stream.collect
    <R, A> R collect(Collector<? super T, A, R> collector);
    //                          ↑
    //                          T 받음 → super
    
    // Stream.reduce
    T reduce(T identity, BinaryOperator<T> accumulator);
    // BinaryOperator<T> = BiFunction<T, T, T>
    
    // Collections.max
    public static <T extends Object & Comparable<? super T>> T max(
            Collection<? extends T> coll) {
        //              ↑
        //              T 꺼냄 → extends
    }
    
    // Collections.sort
    public static <T> void sort(
            List<T> list, 
            Comparator<? super T> c) {
        //              ↑
        //              T 비교 (받음) → super
    }

    6.7 자기 점검 답변

    공변 vs 반공변의 차이를 PECS 로 설명하면?

    :

    • 공변 (? extends T) = Producer:

      • 데이터를 꺼내는 매개변수
      • 자식 타입 자유롭게 받음
      • 메서드의 입력 (소스)
    • 반공변 (? super T) = Consumer:

      • 데이터를 추가하는 매개변수
      • 부모 타입 자유롭게 받음
      • 메서드의 출력 (목적지)

    기억법:

    • Producer = Extends (PE)
    • Consumer = Super (CS)
    • 합치면 PECS

    7️⃣ 와일드카드 캡처

    7.1 와일드카드 캡처의 정의

    와일드카드 캡처 (Wildcard Capture):
      
      와일드카드가 메서드 매개변수에 나올 때
      컴파일러가 임시 타입 이름을 부여하는 것.
      
      예: List<?> → List<CAP#1>
      (CAP#1 은 컴파일러가 만든 이름)

    7.2 캡처의 예

    // 컴파일 에러 — 이름이 없음
    public void reverse(List<?> list) {
        Object first = list.remove(0);   // ✓ 읽기
        list.add(first);   // ❌ 무슨 타입인지 모름
    }
    
    // 우회 — 헬퍼 메서드로 캡처
    public void reverse(List<?> list) {
        reverseHelper(list);
    }
    
    private <T> void reverseHelper(List<T> list) {
        // 이 안에서는 T 가 캡처됨
        T first = list.remove(0);
        list.add(first);   // ✓ 가능 (T 가 명확)
    }

    핵심:

    • List<?>? 는 이름 없음
    • 헬퍼 메서드의 <T> 가 그 자리에서 T 로 캡처
    • T 가 명확해서 add 가능

    7.3 캡처가 일어나는 시점

    // 1. 메서드 매개변수
    public void process(List<?> list) {
        // list 의 ? 는 캡처됨
        // 하지만 그 이름이 없어 활용 어려움
    }
    
    // 2. 헬퍼 메서드 호출 시 캡처
    private <T> void helper(List<T> list) {
        // T 는 캡처된 이름
    }

    7.4 캡처 오류와 해결

    // 컴파일 에러
    public void swap(List<?> list, int i, int j) {
        Object temp = list.get(i);
        list.set(i, list.get(j));   // ❌ ? 가 무엇인지 모름
        list.set(j, temp);   // ❌
    }
    
    // 해결 — 헬퍼 메서드
    public void swap(List<?> list, int i, int j) {
        swapHelper(list, i, j);
    }
    
    private <T> void swapHelper(List<T> list, int i, int j) {
        T temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    }

    7.5 캡처 활용 — 안전한 swap

    // 자바 표준의 swap 패턴
    public static void swap(List<?> list, int i, int j) {
        swapHelper(list, i, j);
    }
    
    private static <T> void swapHelper(List<T> list, int i, int j) {
        T tmp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, tmp);
    }
    
    // 외부엔 List<?> 노출 (간단)
    // 내부엔 T 활용 (명확)

    7.6 캡처의 이름

    // 컴파일러 에러 메시지에 "CAP#1" 같은 이름 등장
    public class CaptureExample {
        public static void main(String[] args) {
            List<?> list = new ArrayList<String>();
            
            // 시도: list.add(list.get(0))
            // 에러:
            // error: incompatible types: 
            //   CAP#1 cannot be converted to capture#1 of ?
            //   where CAP#1 is a fresh type-variable:
            //   CAP#1 extends Object from capture of ?
        }
    }

    → 컴파일러가 와일드카드를 임시 T (CAP#1) 로 처리.

    7.7 캡처 활용 패턴

    // 1. 자기 자신과의 작업
    public static void swap(List<?> list, int i, int j) {
        swapImpl(list, i, j);
    }
    
    private static <T> void swapImpl(List<T> list, int i, int j) {
        T tmp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, tmp);
    }
    
    // 2. 같은 List 안에서 변환
    public static void reverse(List<?> list) {
        reverseImpl(list);
    }
    
    private static <T> void reverseImpl(List<T> list) {
        int size = list.size();
        for (int i = 0; i < size / 2; i++) {
            T tmp = list.get(i);
            list.set(i, list.get(size - 1 - i));
            list.set(size - 1 - i, tmp);
        }
    }

    7.8 자기 점검 답변

    와일드카드 캡처가 왜 필요한가?

    :

    • 상황:

      • List<?> 에선 ? 가 이름 없음
      • 같은 ? 의 두 요소를 옮기는 작업 어려움
    • 해결:

      • 헬퍼 메서드의 <T> 가 캡처
      • T 라는 이름으로 ? 를 표현
      • 같은 작업이 가능해짐
    • 패턴:

      • 외부 API 는 List<?> (간단)
      • 내부 구현은 <T> (명확)
      • swap, reverse 등에 활용

    8️⃣ 타입 매개변수 vs 와일드카드

    8.1 비교 표

    항목타입 매개변수 <T>와일드카드 <?>
    위치정의 (클래스, 메서드)사용 (매개변수, 변수)
    이름T, E, K, V 등?
    본체 활용가능 (T 사용)제한적
    다중 사용T 일관성 보장? 마다 다름
    반환 타입가능권장 X

    8.2 사용 시점

    // 1. 타입 매개변수 — 정의 + 본체에서 활용
    public <T> T process(T input) {   // 정의
        return input;                  // 본체 활용
    }
    
    public <T> List<T> repeat(T value, int times) {   // 정의
        List<T> result = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            result.add(value);   // 본체 활용
        }
        return result;
    }
    
    // 2. 와일드카드 — 매개변수에서 유연성
    public int count(List<?> list) {   // 사용
        return list.size();             // 활용 제한적
    }
    
    public double sum(List<? extends Number> nums) {
        double total = 0;
        for (Number n : nums) {
            total += n.doubleValue();
        }
        return total;
    }

    8.3 선택 기준

    타입 매개변수 <T> 선택:
      ✓ 본체에서 T 의 메서드/타입 활용
      ✓ 여러 곳에서 같은 T 사용
      ✓ 반환 타입에 T 사용
      ✓ 두 매개변수의 일치 요구
    
    와일드카드 <?> 선택:
      ✓ 매개변수의 다양성만 필요
      ✓ 본체에서 타입 정보 없어도 OK
      ✓ 사용자 코드 단순화
      ✓ 반환 타입 아닌 입력만

    8.4 잘못된 선택 예

    // ❌ 타입 매개변수 불필요 (와일드카드가 더 깔끔)
    public <T> int count(List<T> list) {
        return list.size();
    }
    
    // ✓ 와일드카드로 충분
    public int count(List<?> list) {
        return list.size();
    }
    
    // ❌ 와일드카드 부적합 (T 일관성 필요)
    public void copy(List<?> src, List<?> dest) {
        // src 와 dest 가 다른 ? → 매칭 어려움
    }
    
    // ✓ 타입 매개변수가 적합 (PECS 활용)
    public <T> void copy(List<? extends T> src, List<? super T> dest) {
        for (T item : src) {
            dest.add(item);
        }
    }

    8.5 두 가지 동시 사용

    // 자바 표준의 패턴
    public static <T> void copy(
            List<? super T> dest,        // 와일드카드 + T
            List<? extends T> src) {     // 와일드카드 + T
        for (T item : src) {
            dest.add(item);
        }
    }
    
    // T 가 두 매개변수의 관계를 명시
    // 와일드카드가 각각의 유연성

    8.6 자바 표준의 종합 패턴

    // 패턴 1: 단순 작업 → 와일드카드
    public int size(Collection<?> c) { return c.size(); }
    public void print(Collection<?> c) { ... }
    
    // 패턴 2: 타입 보존 → 타입 매개변수
    public <T> List<T> singleton(T item) { ... }
    public <T> Optional<T> first(List<T> list) { ... }
    
    // 패턴 3: PECS 활용 → 결합
    public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
    public static <T> int max(Collection<? extends T> coll, Comparator<? super T> c) { ... }
    
    // 패턴 4: bounded + 와일드카드
    public static <T extends Comparable<? super T>> void sort(List<T> list) { ... }
    //                              ↑
    //                              T 또는 부모를 비교 가능한 T

    8.7 ILIC 활용 종합

    public class ShipmentService {
        
        // 1. 단순 작업 — 와일드카드
        public int countShipments(List<? extends Shipment> shipments) {
            return shipments.size();
        }
        
        // 2. 타입 보존 — 타입 매개변수
        public <T extends Shipment> Optional<T> findFirst(List<T> shipments) {
            return shipments.stream().findFirst();
        }
        
        // 3. PECS — 결합
        public <T extends Shipment> void copy(
                List<? extends T> source,
                List<? super T> destination) {
            for (T s : source) {
                destination.add(s);
            }
        }
        
        // 4. 비교 — bounded + 와일드카드
        public <T extends Shipment> T findHeaviest(
                List<? extends T> shipments,
                Comparator<? super T> comparator) {
            return shipments.stream().max(comparator).orElseThrow();
        }
    }
    
    // 사용
    List<SeaShipment> seaShipments = ...;
    
    service.countShipments(seaShipments);           // ✓
    Optional<SeaShipment> first = service.findFirst(seaShipments);   // ✓
    service.copy(seaShipments, new ArrayList<Shipment>());           // ✓
    SeaShipment heaviest = service.findHeaviest(seaShipments, 
        Comparator.comparing(Shipment::getWeight));

    8.8 자기 점검 답변

    타입 매개변수 vs 와일드카드 선택의 결정적 기준은?

    :
    1. 본체에서 타입 활용 여부:

    • 활용 → 타입 매개변수 <T>
    • 활용 X → 와일드카드 <?>
    1. 반환 타입 사용 여부:

      • 반환에 T → 타입 매개변수
      • 입력만 → 와일드카드
    2. 두 매개변수 일치 필요:

      • 일치 → 타입 매개변수 + (extends/super)
      • 무관 → 각각 와일드카드
    3. 사용자 코드 단순성:

      • 단순 → 와일드카드
      • 복잡한 다형성 → 타입 매개변수

    → 자바 표준은 둘을 적절히 조합.


    9️⃣ 면접 + 자기 점검

    9.1 면접 단골 질문 매핑

    Q핵심 답변
    제네릭의 불공변성?List 와 List 무관
    와일드카드 ? 의미?"어떤 타입" placeholder
    <?> 의 의미?어떤 타입의 List 든 (unbounded)
    <? extends T>?T 또는 자식 (공변, Producer)
    <? super T>?T 또는 부모 (반공변, Consumer)
    공변성 vs 반공변성?자식 방향 vs 부모 방향
    extends 의 쓰기 제약?어떤 자식인지 모름
    super 의 읽기 제약?어떤 부모인지 모름 (Object 로만)
    List<?> vs List?어떤 타입 vs Object 전용
    와일드카드 캡처?헬퍼 메서드의 T 가 캡처
    배열의 공변성?공변 (단점, 런타임 검사)

    9.2 자기 점검 체크리스트

    불공변성

    • 불공변성 정의를 안다
    • 왜 자바가 불공변인지 안다
    • 배열의 공변과 비교 가능
    • 메서드 매개변수의 한계를 안다

    와일드카드

    • <?> 의 의미를 안다
    • <?> 의 읽기/쓰기 제약을 안다
    • <?> vs <Object> 차이를 안다
    • <?> 활용 패턴을 안다

    공변 / 반공변

    • <? extends T> 의미와 제약
    • <? super T> 의미와 제약
    • Producer vs Consumer 구분
    • PECS 원칙 예고

    캡처

    • 와일드카드 캡처의 의미
    • swap, reverse 패턴
    • 헬퍼 메서드 활용
    • 컴파일러 에러 메시지 (CAP#1)

    종합

    • 타입 매개변수 vs 와일드카드 선택
    • 자바 표준의 패턴 (copy, sort 등)
    • ILIC 활용 가능
    • PECS 원칙 (Unit 5.5 예고) 이해

    9.3 추가 심화 질문

    Q1: List<?> 에 add 가능한 것은?

    답:

    • null 만 가능
    • 어떤 타입의 List 인지 모르므로
    • null 은 모든 타입의 자식이라 안전
    List<?> list = ...;
    list.add(null);     // ✓
    list.add("hello");  // ❌

    Q2: List<? extends Number> 에 add 가능?

    답:

    • null 만 가능
    • 어떤 Number 자식 List 인지 모름
    • List 일 수도 → Integer 만 안전
    • List 일 수도 → Double 만 안전
    • 안전 보장 X → add 금지

    Q3: List<? super Integer> 에서 get 의 결과 타입?

    답:

    • Object
    • List, List, List 중 무엇인지 모름
    • 공통 부모 Object 가 가장 좁은 보장
    • List<? super Integer> list = ...;
      Object o = list.get(0);   // ✓
      // Number n = list.get(0);   // ❌ 캐스트 필요
      // Integer i = list.get(0);   // ❌ 캐스트 필요

      Q4: List 와 List<?> 의 호환?

      답:

      List<Object> objs = new ArrayList<>();
      List<?> wild = objs;        // ✓ OK
      
      List<?> wild = new ArrayList<String>();
      List<Object> objs = wild;   // ❌ 컴파일 에러
      // wild 가 List<String> 일 수도 있음 → Object 추가 시 깨짐

      Q5: 와일드카드를 반환 타입에 쓸 수 있나?

      답:

      • 가능하지만 권장 X
      public List<?> getItems() { ... }   // ✗ 사용자가 활용 어려움
      
      public List<? extends Number> getNumbers() { ... }   // △
      
      // 권장
      public List<Number> getNumbers() { ... }   // ✓ 명확
      public <T> List<T> getItems() { ... }       // ✓ 타입 매개변수

      이유:

      • 사용자가 활용 어려움 (Object 로만 받거나, 캐스트 필요)
      • 타입 정보 손실

      🎯 핵심 요약 — 3줄 정리

      1. 불공변성과 와일드카드

      • 제네릭은 불공변 (List 는 List 자식 X)
      • 와일드카드로 우회
      • 세 종류: <?>, <? extends T>, <? super T>

      2. extends vs super

      • <? extends T> = 공변 = Producer (꺼내기)
      • <? super T> = 반공변 = Consumer (추가)
      • 다음 Unit 5.5 의 PECS 원칙

      3. 타입 매개변수 vs 와일드카드

      • 타입 매개변수: 본체에서 활용, 반환 타입
      • 와일드카드: 매개변수의 유연성
      • 자바 표준은 둘을 조합 (<T> copy(List<? super T>, List<? extends T>))

      📚 다음으로...

      Unit 5.4 — 제네릭 메서드 + 제네릭 클래스

      이번 Unit에서 와일드카드를 봤다면, 다음은 제네릭 활용의 정밀.

      • 제네릭 메서드와 제네릭 클래스 더 자세히
      • 타입 추론의 메커니즘
      • 다양한 시그니처 패턴
      • 자바 표준의 정밀 분석 (Collections, Stream)

      Phase 5 진행 상황

      🚀 Phase 5 — 제네릭과 와일드카드
        ✅ Unit 5.1 제네릭의 등장과 raw type
        ✅ Unit 5.2 타입 매개변수와 타입 인자
        ✅ Unit 5.3 와일드카드 ? extends, ? super ← 여기
        ⏭ Unit 5.4 제네릭 메서드 + 제네릭 클래스
        ⏭ Unit 5.5 PECS 원칙 (★ 마스터 깊이)

      3주차 누적 진행

      ✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
      ✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
      ✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
      ✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
      🚀 Phase 5 — 제네릭과 와일드카드 (3/5 진행)
      
      총: 20/43 Unit 작성 (약 47%)

profile
Software Developer

0개의 댓글