F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
? 의 정확한 의미는?<?> (unbounded) 의 의미와 활용은?<? extends T> (upper bounded) 의 공변성 (Covariance) 은?<? super T> (lower bounded) 의 반공변성 (Contravariance) 은?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 로만 (정확한 타입 모름)
→ 와일드카드 = 불공변성의 해결 + 안전한 다형성.
1. 제네릭의 불공변성 (Invariance)
2. 와일드카드 ? 의 등장
3. unbounded wildcard <?>
4. upper bounded wildcard <? extends T> — 공변성
5. lower bounded wildcard <? super T> — 반공변성
6. 공변 vs 반공변 비교
7. 와일드카드 캡처
8. 타입 매개변수 vs 와일드카드
9. 면접 + 자기 점검
불공변성 (Invariance):
타입 A 가 타입 B 의 자식이어도,
Generic<A> 와 Generic<B> 는 무관한 타입.
자바 제네릭의 규칙:
Integer extends Number (Integer 는 Number 자식)
하지만:
List<Integer> 와 List<Number> 는 무관 (자식 아님)
// 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; // ❌ 컴파일 에러!
// 만약 공변성을 허용했다면 (가상):
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!
→ 제네릭의 불공변성은 타입 안전성을 위한 의도적 제약.
// 자바 배열은 공변
Integer[] ints = new Integer[10];
Number[] nums = ints; // ✓ OK — 배열은 공변
nums[0] = 3.14; // ❌ ArrayStoreException (런타임)
// 런타임에 검사하지만 컴파일은 통과
// 이게 자바 배열의 단점
비교:
효과:
1. 컴파일 타임 안전성
- 잘못된 타입 매개변수 검출
- 런타임 ClassCastException 회피
2. 명확한 의도
- List<Integer> = "Integer 만 담는 List"
- List<Number> = "Number 만 담는 List"
- 서로 다른 책임
3. 다형성 제약
- 단점이기도 함
- 와일드카드로 해결
// 합 메서드 - 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 도 합을 구할 수 있는데, 메서드가 거부.
→ 와일드카드로 해결 (다음 섹션).
// 해결: 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 든"제네릭이 불공변인 이유는?
답:
타입 안전성:
컴파일 타임 검증:
명확한 의도:
배열과 대조:
→ 단점 (다형성 제약) 은 와일드카드가 해결.
와일드카드 (Wildcard) ?:
"어떤 타입" 을 의미하는 placeholder.
타입 매개변수 자리에 사용.
문법:
List<?> ← unbounded wildcard
List<? extends T> ← upper bounded
List<? super T> ← lower bounded
문제:
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)
→ 공변성 + 안전성
// 1. unbounded — 어떤 타입이든
List<?> anything;
// 2. upper bounded — T 또는 자식 (공변)
List<? extends Number> numberOrChild;
// 3. lower bounded — T 또는 부모 (반공변)
List<? super Integer> integerOrParent;
// 타입 매개변수 — 정의 시
public class Box<T> { // T 는 매개변수
private T value; // T 사용
}
// 와일드카드 — 사용 시
public void process(Box<?> box) { // ? 는 와일드카드
// box 안에 뭐가 들어있는지 모름
}
차이:
// 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); // ✓
→ 매개변수 타입의 유연성.
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> 일 수도
// 안전을 위해 추가 금지
}
→ <?> 는 읽기 전용 에 가깝다.
와일드카드
?가 필요한 이유는?
답:
1. 불공변성 우회:
<? extends Number> 로 해결유연한 메서드:
printList(List<?>) 가 모든 List 받음타입 안전성 유지:
세 가지 변형:
unbounded wildcard:
List<?>
의미: "어떤 타입의 List 든"
사실상 List<? extends Object> 와 동등
(Object 가 모든 클래스의 부모)
// 어떤 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 의 사이즈만 알면 되는 작업.
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, 새 요소 추가 ❌.
// 큰 차이!
// 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<?> = "어떤 타입이든" (불특정)
// 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);
}
// 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 의 메서드
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 의 추가는 본질적으로 어렵다.
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 도 OK4.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 (소비자) 키워드 extends super 시각 자식 방향 부모 방향 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 또는 부모를 비교 가능한 T8.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 → 와일드카드
<?>
반환 타입 사용 여부:
- 반환에 T → 타입 매개변수
- 입력만 → 와일드카드
두 매개변수 일치 필요:
- 일치 → 타입 매개변수 + (extends/super)
- 무관 → 각각 와일드카드
사용자 코드 단순성:
- 단순 → 와일드카드
- 복잡한 다형성 → 타입 매개변수
→ 자바 표준은 둘을 적절히 조합.
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%)