F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
🎯 마스터 프롬프트 깊이 Unit — 🏆 Phase 5 완주
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Comparable<? super T> 의 정확한 의미는?Collections.copy 시그니처 의 정밀 분석은?Collections.max, min 의 PECS 활용은?PECS 원칙 — Producer Extends, Consumer Super — 은 제네릭 매개변수 설계의 황금률이다.
데이터를 꺼내는 매개변수는? extends T(Producer, 공변), 데이터를 넣는 매개변수는? super T(Consumer, 반공변).
자바 표준 라이브러리의 모든 정교한 제네릭 시그니처가 이 원칙을 따르며,
Collections.copy(List<? super T> dest, List<? extends T> src)가 가장 완벽한 PECS 모범.
이 Unit 을 마치면 어떤 제네릭 메서드 시그니처도 자신 있게 설계 가능.
PECS = 강물의 방향성
Producer (생산자):
강의 상류 — 물이 흘러나옴
- 강에서 물 받는 곳 (input)
- 자식들 (구체적 종류) 도 됨
- "? extends Number" = "Number 이거나 자식 (Integer, Double, ...)"
Consumer (소비자):
강의 하류 — 물이 흘러들어감
- 강에 물을 보내는 곳 (output)
- 부모들 (더 넓은 통) 도 됨
- "? super Integer" = "Integer 이거나 부모 (Number, Object)"
PECS = 강의 양쪽:
- 상류 (extends): 데이터 출발지
- 하류 (super): 데이터 도착지
→ PECS = 데이터 흐름의 두 방향.
1. PECS 원칙의 정확한 의미
2. Producer Extends 의 깊은 분석
3. Consumer Super 의 깊은 분석
4. PECS 결정 알고리즘 4단계
5. Comparable<? super T> 의 정밀
6. Collections 의 모든 PECS 분석
7. Stream, Function, Optional 의 PECS
8. 자체 라이브러리 설계 가이드
9. Phase 5 졸업 시험 + Phase 5 완주
PECS:
Producer Extends, Consumer Super
해석:
- 매개변수가 데이터를 "생산" (외부로 제공) 한다 → `? extends T`
- 매개변수가 데이터를 "소비" (외부에서 받음) 한다 → `? super T`
용도:
제네릭 메서드 매개변수의 와일드카드 선택 가이드.
Joshua Bloch 의 Effective Java 에서 정립:
Item 31: 한정적 와일드카드를 사용해 API 유연성을 높이라
Producer Extends, Consumer Super (PECS)
- 매개변수가 PE 처럼 사용되면 ? extends T
- 매개변수가 CS 처럼 사용되면 ? super T
public void process(List<? extends Number> producer) {
// ↑
// 데이터를 꺼내는 매개변수
for (Number n : producer) { // 꺼냄 (read)
System.out.println(n);
}
Number first = producer.get(0); // 꺼냄
// producer.add(...) → 추가 불가 (Producer 라서)
}
// 호출
List<Integer> ints = List.of(1, 2, 3);
process(ints); // ints 는 메서드에 데이터 제공
// → ints 는 Producer
public void fill(List<? super Integer> consumer) {
// ↑
// 데이터를 받는 매개변수
for (int i = 0; i < 10; i++) {
consumer.add(i); // 넣음 (write)
}
// Object o = consumer.get(0); → 읽기는 Object 로만
}
// 호출
List<Number> nums = new ArrayList<>();
fill(nums); // nums 는 메서드에 데이터를 받음
// → nums 는 Consumer
| 항목 | Producer Extends | Consumer Super |
|---|---|---|
| 와일드카드 | <? extends T> | <? super T> |
| 방향 | 자식 (좁은) | 부모 (넓은) |
| 변성 | 공변 (Covariant) | 반공변 (Contravariant) |
| 읽기 | T 로 가능 | Object 로만 |
| 쓰기 | null 제외 불가 | T 와 자식 가능 |
| 역할 | 데이터 제공 | 데이터 수용 |
| 메서드 입출력 | 입력 (in) | 출력 (out) |
| 예 | List<? extends Number> | List<? super Integer> |
// Collections.copy — 가장 완벽한 PECS 모범
public static <T> void copy(
List<? super T> dest, // Consumer (받음)
List<? extends T> src) { // Producer (제공)
for (T t : src) {
dest.add(t);
}
}
// 사용
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints);
// dest: nums (List<Number>) — Number 부모 → Integer 받음
// src: ints (List<Integer>) — Integer 또는 자식 → 꺼냄
// T = Integer 추론
public class ShipmentUtils {
// Producer (꺼내기) — extends
public static BigDecimal totalWeight(
List<? extends Shipment> shipments) {
BigDecimal total = BigDecimal.ZERO;
for (Shipment s : shipments) { // 꺼내기
total = total.add(s.getWeight());
}
return total;
}
// Consumer (추가) — super
public static void collectAll(
List<? super Shipment> destination,
List<Shipment> source) {
for (Shipment s : source) {
destination.add(s); // 추가
}
}
// 둘 다 — PECS
public static <T extends Shipment> void copy(
List<? extends T> source, // Producer
List<? super T> destination) { // Consumer
for (T s : source) {
destination.add(s);
}
}
}
// 사용
List<SeaShipment> seaShipments = ...;
List<Object> allObjects = new ArrayList<>();
BigDecimal total = ShipmentUtils.totalWeight(seaShipments);
ShipmentUtils.collectAll(allObjects, ...);
ShipmentUtils.copy(seaShipments, allObjects);
PECS 의 정확한 의미와 적용 방법은?
답:
? extends T? super T적용:
1. 매개변수의 역할 분석 (PE? CS? 둘 다?)
2. 그에 맞는 와일드카드 선택
3. 읽기/쓰기 동작 검증
4. 호출자의 유연성 ↑
→ 자바 표준 라이브러리의 황금률.
Producer Extends:
"데이터를 외부에 제공하는 매개변수는 자식 타입을 모두 받을 수 있어야 한다."
→ 공변성 (Covariance)
→ ? extends T
// 문제: 불공변성
public void sum(List<Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
}
// 호출 못 함
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // ❌ List<Integer> 는 List<Number> 자식 아님
// PE 해결
public void sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
}
// 호출 OK
sum(ints); // ✓ List<Integer> 가 List<? extends Number> 의 일종
public void process(List<? extends Number> list) {
// ✓ 읽기 (Number 로 받음)
Number first = list.get(0);
for (Number n : list) { ... }
int size = list.size();
// 캐스트로 더 좁은 타입 받기 가능 (이지만 권장 X)
Object obj = list.get(0); // 너무 넓음
// 안전한 읽기 보장:
// - list 가 List<Integer> 면 Integer → Number ✓
// - list 가 List<Double> 면 Double → Number ✓
// - 어떤 자식이든 Number 로 안전
}
public void process(List<? extends Number> list) {
// ❌ 쓰기 (어떤 자식인지 모름)
// list.add(1); // Integer? Double?
// list.add(1.0); // Integer 면 깨짐
// list.add(new BigDecimal("1")); // 위험
// ✓ null 만 가능
list.add(null);
// ✓ 같은 list 의 요소는 가능 (런타임 안전)
list.add(list.get(0)); // ❌ 컴파일 에러 (정적으로 안전 보장 X)
// 이유:
// List<Integer> 일 수도, List<Double> 일 수도
// 어떤 타입을 추가하든 안전 보장 못 함
// 쓰기가 필요하면 ? super T (Consumer) 사용
}
// 1. 데이터 합치기
public static <T> List<T> concat(
List<? extends T> a,
List<? extends T> b) {
List<T> result = new ArrayList<>();
result.addAll(a);
result.addAll(b);
return result;
}
// 2. 집계
public static <T> long count(
Collection<? extends T> coll,
Predicate<? super T> filter) {
return coll.stream().filter(filter).count();
}
// 3. 최대값
public static <T extends Comparable<? super T>> T max(
Collection<? extends T> coll) {
Iterator<? extends T> it = coll.iterator();
T max = it.next();
while (it.hasNext()) {
T next = it.next();
if (next.compareTo(max) > 0) {
max = next;
}
}
return max;
}
// 4. 변환
public static <T, R> List<R> map(
List<? extends T> source,
Function<? super T, ? extends R> mapper) {
return source.stream().map(mapper).toList();
}
// Collection.addAll
public boolean addAll(Collection<? extends E> c) {
// ↑
// c 는 Producer (데이터 제공)
// 자신에게 c 의 데이터 추가
}
// Stream.of
public static <T> Stream<T> of(T... values) { ... }
public static <T> Stream<T> of(
Stream<? extends T>... values) {
// values 의 각 Stream 이 Producer
}
// Collectors
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper,
// ↑
// ? extends K — K 자식의 키 생산
Function<? super T, ? extends U> valueMapper) {
}
public class ShipmentAnalytics {
// 1. 총 무게 — Producer
public static BigDecimal totalWeight(
List<? extends Shipment> shipments) {
return shipments.stream()
.map(Shipment::getWeight)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 2. 가장 무거운 — Producer
public static Optional<Shipment> heaviest(
Collection<? extends Shipment> shipments) {
return shipments.stream()
.max(Comparator.comparing(Shipment::getWeight));
}
// 3. 그룹핑 — Producer
public static <T extends Shipment, K> Map<K, List<T>> groupBy(
Collection<? extends T> shipments,
Function<? super T, ? extends K> keyExtractor) {
return shipments.stream()
.collect(Collectors.groupingBy(keyExtractor));
}
// 4. 평균 — Producer
public static BigDecimal averageWeight(
Collection<? extends Shipment> shipments) {
if (shipments.isEmpty()) return BigDecimal.ZERO;
BigDecimal total = totalWeight(new ArrayList<>(shipments));
return total.divide(BigDecimal.valueOf(shipments.size()));
}
}
// 사용
List<SeaShipment> seaShipments = ...;
List<AirShipment> airShipments = ...;
BigDecimal seaTotal = ShipmentAnalytics.totalWeight(seaShipments); // ✓
BigDecimal airTotal = ShipmentAnalytics.totalWeight(airShipments); // ✓
Producer Extends 의 본질은?
답:
본질:
동작:
활용:
예:
Collection.addAll(Collection<? extends E>)Function<? super T, ? extends R> 의 R 부분Comparator<? super T> 의 입력Consumer Super:
"데이터를 받는 매개변수는 부모 타입을 모두 받을 수 있어야 한다."
→ 반공변성 (Contravariance)
→ ? super T
// 문제: 데이터 받는 곳을 더 유연하게
// 좁은 매개변수
public void addInteger(List<Integer> list, Integer value) {
list.add(value);
}
// List<Integer> 만 받음 → 유연성 ↓
List<Number> nums = new ArrayList<>();
addInteger(nums, 42); // ❌ List<Number> 못 받음
// CS 해결
public void addInteger(List<? super Integer> list, Integer value) {
list.add(value);
}
// 호출 OK
addInteger(nums, 42); // ✓ List<Number>
addInteger(new ArrayList<Object>(), 42); // ✓ List<Object>
public void process(List<? super Integer> list) {
// ✓ 쓰기 (Integer 와 자식 가능)
list.add(42);
list.add(Integer.MAX_VALUE);
list.add(Integer.valueOf(0));
// Integer 의 자식 (final 이라 없음, 가상)
// 만약 SubInteger extends Integer 면 추가 가능
// ❌ Number 추가 (Integer 부모)
// list.add(3.14); // Double 은 Integer 아님
// list.add(BigInteger.ONE); // BigInteger 는 Integer 아님
// 이유:
// list 가 List<Integer> 면 → Integer 만 안전
// list 가 List<Number> 면 → Number 자식 OK
// 가장 좁은 보장은 Integer
}
public void process(List<? super Integer> list) {
// ❌ 정확한 타입 모름
// Integer i = list.get(0); // ❌
// Number n = list.get(0); // ❌
// ✓ Object 로만
Object obj = list.get(0);
// 이유:
// list 가 List<Integer>, List<Number>, List<Object> 중 무엇인지 모름
// 가장 좁은 보장은 Object
// 읽기가 필요하면 ? extends T (Producer) 사용
}
// 1. 데이터 채우기
public static <T> void fillWith(
List<? super T> dest,
Supplier<T> supplier,
int count) {
for (int i = 0; i < count; i++) {
dest.add(supplier.get());
}
}
// 2. 컬렉션에 추가
public static <T> void addAll(
Collection<? super T> coll,
T... items) {
for (T item : items) {
coll.add(item);
}
}
// 3. forEach 와 부모 Consumer
public static <T> void forEach(
List<T> list,
Consumer<? super T> action) {
for (T item : list) {
action.accept(item);
}
}
// 4. Comparator 의 활용
public static <T> T max(
List<T> list,
Comparator<? super T> comparator) {
T max = list.get(0);
for (T item : list) {
if (comparator.compare(item, max) > 0) {
max = item;
}
}
return max;
}
// Collections.addAll
public static <T> boolean addAll(
Collection<? super T> c,
T... elements) {
// ↑
// c 는 Consumer (데이터 받음)
}
// Stream.forEach
default void forEach(Consumer<? super T> action) {
// ↑
// action 은 T 또는 부모 받음
}
// Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
// Comparator<? super T> 활용
public static <T> void sort(
List<T> list,
Comparator<? super T> c) {
// ↑
// c 는 T 또는 부모 받아서 비교
}
// Shipment 비교기
Comparator<Shipment> byWeight =
Comparator.comparing(Shipment::getWeight);
// SeaShipment 의 자식 정렬에 사용
List<SeaShipment> seaShipments = ...;
Collections.sort(seaShipments, byWeight);
// ↑
// Comparator<Shipment> 가
// Comparator<? super SeaShipment> 로 받아짐
// (Shipment 가 SeaShipment 의 부모)
// 즉:
// sort(List<SeaShipment>, Comparator<? super SeaShipment>)
// Shipment 가 SeaShipment 의 부모 → OK
public class ShipmentCollector {
// 1. 받는 통 — Consumer
public static <T extends Shipment> void collectInto(
List<? super T> destination,
T... shipments) {
for (T s : shipments) {
destination.add(s);
}
}
// 2. 처리 동작 — Consumer (Function)
public static <T extends Shipment> void processAll(
List<T> shipments,
Consumer<? super T> processor) {
shipments.forEach(processor);
}
// 3. 변환 후 받기 — Consumer
public static <T extends Shipment, R> void transformInto(
List<? extends T> source,
List<? super R> destination,
Function<? super T, ? extends R> mapper) {
for (T s : source) {
destination.add(mapper.apply(s));
}
}
}
// 사용
List<Object> all = new ArrayList<>();
ShipmentCollector.collectInto(all, new SeaShipment(), new AirShipment());
List<Shipment> shipments = ...;
ShipmentCollector.processAll(shipments, s -> log(s));
List<BigDecimal> weights = new ArrayList<>();
ShipmentCollector.transformInto(shipments, weights, Shipment::getWeight);
Consumer Super 의 본질은?
답:
본질:
동작:
활용:
예:
Collections.addAll(Collection<? super T>)Stream.forEach(Consumer<? super T>)sort(List<T>, Comparator<? super T>)Step 1: 매개변수의 역할 분석
- 데이터를 꺼냄? → Producer
- 데이터를 추가? → Consumer
- 둘 다? → 일반 T
Step 2: 입출력 검토
- 메서드의 입력 (in)? → Producer 가능
- 메서드의 출력 (out)? → Consumer 가능
Step 3: 와일드카드 선택
- Producer → ? extends T
- Consumer → ? super T
- 둘 다 (PECS 안 됨) → T (와일드카드 X)
Step 4: 호환성 검증
- 호출자가 다양한 타입 전달 가능?
- 메서드 내부 사용 안전?
매개변수 X 를 결정할 때:
질문 1: X 에서 데이터를 꺼내는가? (read)
YES → Producer 후보
질문 2: X 에 데이터를 추가하는가? (write)
YES → Consumer 후보
조합:
- Producer 만 → ? extends T
- Consumer 만 → ? super T
- 둘 다 → T (불가, PECS 안 됨)
- 둘 다 안 함 → ? (와일드카드, 다양한 타입 받기만)
// 시나리오: dest 에 src 의 모든 요소 복사
// Step 1: dest 와 src 의 역할
// - dest: 데이터 받음 (write) → Consumer
// - src: 데이터 꺼냄 (read) → Producer
// Step 2: 입출력
// - dest: 출력 (out, 데이터 받음)
// - src: 입력 (in, 데이터 제공)
// Step 3: 와일드카드
// - dest: Consumer → ? super T
// - src: Producer → ? extends T
// Step 4: 검증
public static <T> void copy(
List<? super T> dest,
List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
// 호출자 유연성:
// copy(new ArrayList<Number>(), List.of(1, 2, 3)) // T=Integer
// ↑ ↑
// Number 는 Integer 부모 Integer 의 List
// 시나리오: source 에서 조건 만족하는 것만 반환
// Step 1: source 와 predicate 의 역할
// - source: 데이터 꺼냄 → Producer
// - predicate: predicate 자체는 데이터 받는 함수
// - predicate.test(T) 는 T 를 입력 받음
// - Predicate<? super T> 가 적절
// Step 2: 입출력
// - source: 입력 (Producer)
// - predicate: 입력 (사용자가 정의)
// - 반환: 출력
// Step 3: 와일드카드
// - source: List<? extends T>
// - predicate: Predicate<? super T>
// Step 4: 검증
public static <T> List<T> filter(
List<? extends T> source,
Predicate<? super T> predicate) {
List<T> result = new ArrayList<>();
for (T item : source) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
// 호출
Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;
List<SeaShipment> seaShipments = ...;
// SeaShipment 의 List + Shipment 의 Predicate
List<SeaShipment> active = filter(seaShipments, isActive);
// ↑
// ? super SeaShipment
// = Shipment ✓
// 시나리오: source 의 각 요소를 변환
// Step 1: 역할
// - source: 데이터 꺼냄 → Producer
// - mapper: 함수 자체
// - mapper.apply(T) → R
// - 입력 T 는 ? super T (Consumer 측면)
// - 출력 R 은 ? extends R (Producer 측면)
// Step 2-3: Function<? super T, ? extends R>
public static <T, R> List<R> map(
List<? extends T> source,
Function<? super T, ? extends R> mapper) {
List<R> result = new ArrayList<>();
for (T item : source) {
result.add(mapper.apply(item));
}
return result;
}
// 호출
Function<Shipment, BigDecimal> getFare = Shipment::getFare;
List<SeaShipment> seaShipments = ...;
List<Number> fares = map(seaShipments, getFare);
// ↑ ↑
// List<SeaShipment>
// ? extends Shipment ✓
//
// Function<Shipment, BigDecimal>
// Function<? super SeaShipment, ? extends Number>
// ✓ (BigDecimal 은 Number)
// 시나리오: 컬렉션을 한 값으로 축약
// Step 1: 역할
// - source: 데이터 꺼냄 → Producer
// - identity: 초기값 (Producer? 아님, 단순 값)
// - accumulator: BiFunction (두 값을 받아 하나로)
// - 입력 T 두 개
// - 출력 T
public static <T> T reduce(
List<? extends T> source,
T identity,
BinaryOperator<T> accumulator) {
T result = identity;
for (T item : source) {
result = accumulator.apply(result, item);
}
return result;
}
// 자바 표준의 더 정밀
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
// ↑
// T 의 Consumer
1. 매개변수가 둘 다 (read + write) 이면 ?
→ T (와일드카드 X)
예:
public void modify(List<T> list) {
T item = list.get(0); // read
list.add(item); // write
}
2. 호출자의 의도 고려
- 다양한 타입 받기 → 와일드카드
- 같은 타입 일관 → T
3. 반환 타입은 와일드카드 회피
- List<? extends T> 반환은 사용 어려움
- List<T> 반환 권장
4. PECS 가 항상 필요한 것은 아님
- 단순한 경우 T 만으로 충분
- 호출자 유연성이 중요할 때 PECS
PECS 결정의 4단계 알고리즘은?
답:
1. 역할 분석:
입출력 검토:
와일드카드 선택:
? extends T? super T호환성 검증:
→ 자바 표준 라이브러리의 모든 시그니처가 이 알고리즘.
// 단순 시그니처
public static <T extends Comparable<T>> T max(List<T> list) {
// ...
}
// 문제 시나리오
public class Parent implements Comparable<Parent> {
@Override
public int compareTo(Parent other) { ... }
}
public class Child extends Parent {
// Comparable<Parent> 상속 (Comparable<Child> 가 아님)
}
// 호출
List<Child> children = ...;
max(children); // ❌ 컴파일 에러
// Child 는 Comparable<Child> 가 아니라 Comparable<Parent>
// 자바 표준의 정밀
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
// ↑
// T 또는 부모와 비교 가능
// ...
}
// 의미:
// T 가 자기 자신 또는 부모를 비교할 수 있는 Comparable
// Child 의 경우:
// Child 는 Comparable<Parent>
// Parent 는 Child 의 부모
// 즉, Child 는 Comparable<? super Child> = Comparable<Parent> ✓
// 호출
List<Child> children = ...;
max(children); // ✓ OK
타입 계층:
Comparable<Parent>
↑
Parent
↑
Child (Comparable<Parent> 상속)
Comparable<T> 시도:
- Child 가 Comparable<Child> 인가? NO
- 거부
Comparable<? super T> 시도:
- Child 가 Comparable<? super Child> 인가?
- ? super Child = Child or Parent or Object
- Child 는 Comparable<Parent> 보유 → ✓
→ 부모의 Comparable 을 자식이 활용
// Collections.sort
public static <T extends Comparable<? super T>> void sort(List<T> list) {
// T 또는 부모의 compareTo 사용
}
// Collections.binarySearch
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key) {
// list 의 요소는 T 또는 자식 (Producer)
// 요소들은 T 또는 부모와 비교 가능
}
// Collections.max
public static <T extends Object & Comparable<? super T>> T max(
Collection<? extends T> coll) {
// ...
}
// Collections.min — 같은 시그니처
public static <T extends Object & Comparable<? super T>> T min(
Collection<? extends T> coll) {
// ...
}
// TreeMap, TreeSet
public class TreeMap<K, V> {
// K 는 Comparable 또는 Comparator 제공해야
}
// Collections.reverseOrder
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
// 이런 시그니처를 본다면:
<T extends Comparable<? super T>>
// 해석:
// 1. T 는 어떤 타입
// 2. T 는 Comparable 의 구현
// 3. Comparable 의 타입 매개변수는 T 또는 그 부모
// 4. 즉, T 는 자기 자신 또는 부모와 비교 가능
// 효과:
// - T 가 Comparable<T> 면 OK
// - T 가 Comparable<Parent> (Parent 가 T 부모) 면 OK
// - 자식 클래스가 부모의 Comparable 을 상속받은 경우 활용
// 자바 표준 sort
public static <T> void sort(
List<T> list,
Comparator<? super T> c) {
// T 의 List + T 또는 부모를 비교하는 Comparator
}
// 사용
Comparator<Shipment> byWeight = Comparator.comparing(Shipment::getWeight);
// Shipment 자식 정렬
List<SeaShipment> seaShipments = ...;
Collections.sort(seaShipments, byWeight);
// ↑ ↑
// T = SeaShipment
// Comparator<? super SeaShipment>
// = Comparator<Shipment> ✓
public class Shipment implements Comparable<Shipment> {
private LocalDateTime createdAt;
@Override
public int compareTo(Shipment other) {
return this.createdAt.compareTo(other.createdAt);
}
}
public class SeaShipment extends Shipment {
// Comparable<Shipment> 상속
// Comparable<SeaShipment> 가 아님
}
public class AirShipment extends Shipment {
// 같음
}
// 사용
List<SeaShipment> seaShipments = ...;
// Collections.sort 가 가능한 이유:
// SeaShipment extends Comparable<? super SeaShipment>
// = Comparable<Shipment>
// SeaShipment 는 Comparable<Shipment> ✓
Collections.sort(seaShipments);
// 마찬가지로 max, min, binarySearch 모두 가능
SeaShipment first = Collections.max(seaShipments);
public class ShipmentUtils {
// PECS + Comparable<? super T> 활용
public static <T extends Comparable<? super T>> T findOldest(
Collection<? extends T> shipments) {
if (shipments.isEmpty()) {
throw new IllegalArgumentException("Empty");
}
Iterator<? extends T> it = shipments.iterator();
T oldest = it.next();
while (it.hasNext()) {
T next = it.next();
if (next.compareTo(oldest) < 0) {
oldest = next;
}
}
return oldest;
}
}
// 사용
List<SeaShipment> seaShipments = ...;
SeaShipment oldest = ShipmentUtils.findOldest(seaShipments);
// T = SeaShipment
// Collection<? extends SeaShipment> ← seaShipments
// SeaShipment extends Comparable<? super SeaShipment>
// = Comparable<Shipment> ✓
Comparable<? super T> 가 Comparable 보다 유연한 이유는?
답:
Comparable:
Comparable<? super T>:
예:
Child extends Parent implements Comparable<Parent><? super Child> 가 Parent 를 포함 → OKPECS 의 일종:
public class Collections {
// sort
public static <T extends Comparable<? super T>> void sort(List<T> list);
public static <T> void sort(List<T> list, Comparator<? super T> c);
// binarySearch
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key);
public static <T> int binarySearch(
List<? extends T> list, T key, Comparator<? super T> c);
// max, min
public static <T extends Object & Comparable<? super T>> T max(
Collection<? extends T> coll);
public static <T> T max(
Collection<? extends T> coll, Comparator<? super T> comp);
// copy
public static <T> void copy(
List<? super T> dest,
List<? extends T> src);
// fill
public static <T> void fill(List<? super T> list, T obj);
// replaceAll
public static <T> boolean replaceAll(
List<T> list, T oldVal, T newVal);
// addAll
public static <T> boolean addAll(
Collection<? super T> c, T... elements);
// disjoint
public static boolean disjoint(
Collection<?> c1, Collection<?> c2);
// frequency
public static int frequency(Collection<?> c, Object o);
// unmodifiableList
public static <T> List<T> unmodifiableList(
List<? extends T> list);
// emptyList
public static <T> List<T> emptyList();
// singletonList
public static <T> List<T> singletonList(T o);
// reverseOrder
public static <T> Comparator<T> reverseOrder();
public static <T> Comparator<T> reverseOrder(
Comparator<T> cmp);
}
public static <T extends Comparable<? super T>> void sort(List<T> list);
// 분석:
// - T 는 자기 자신 또는 부모와 비교 가능
// - list 는 T 의 List (정확히, 와일드카드 X)
// - 이유: sort 가 list 의 내부 순서 변경 → T 의 일관성 필요
// - PE 도 CS 도 아니라 그냥 T
// 와일드카드 안 쓴 이유:
// list 의 요소를 읽고 쓰기 모두 함 (sort 내부)
// PE 도 CS 도 아닌 일반 T
public static <T> void sort(List<T> list, Comparator<? super T> c);
// 분석:
// - list: T 의 List
// - c: Comparator<? super T> — CS
// - c.compare(T, T) — T 받아 비교
// - T 또는 부모 비교 가능
// 사용
List<SeaShipment> seaShipments = ...;
Comparator<Shipment> byWeight = ...;
Collections.sort(seaShipments, byWeight); // ✓
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key);
// 분석:
// - list: List<? extends Comparable<? super T>> — 복잡!
// - list 의 요소는 Comparable<? super T> 의 구현
// - 그 자체가 ? extends Comparable<? super T>
// - PE 이중 활용
// - key: T (검색할 키)
// 의미:
// list 의 요소는 T 또는 그 부모와 비교 가능
// 예
List<Shipment> shipments = ...;
SeaShipment target = ...;
int idx = Collections.binarySearch(shipments, target);
// list: List<Shipment>
// ? extends Comparable<? super SeaShipment>
// = Comparable<Shipment>
// Shipment ✓
public static <T> void copy(
List<? super T> dest,
List<? extends T> src);
// 분석:
// - dest: Consumer (src 의 요소 받음)
// - src: Producer (요소 제공)
// - PECS 의 완벽한 모범
// 사용
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
List<Integer> src = List.of(1, 2, 3);
Collections.copy(dest, src);
// T = Integer
// dest: List<Number> = List<? super Integer> ✓
// src: List<Integer> = List<? extends Integer> ✓
public static <T> boolean addAll(
Collection<? super T> c, T... elements);
// 분석:
// - c: Consumer (elements 추가)
// - elements: T... (가변 인자)
// 사용
List<Number> nums = new ArrayList<>();
Collections.addAll(nums, 1, 2, 3); // T = Integer
// c: List<Number> = List<? super Integer> ✓
public static <T> List<T> unmodifiableList(List<? extends T> list);
// 분석:
// - list: Producer (요소 읽기)
// - 사용자가 List<Integer> 를 List<Number> 의 unmodifiableList 로 받기 가능
// - 반환: List<T> — 정확한 T
// 사용
List<Integer> ints = ...;
List<Number> unmodifiable = Collections.unmodifiableList(ints);
// ↑
// T = Number
// List<? extends Number> ← List<Integer> ✓
패턴 1: T 자기 자신 비교
<T extends Comparable<? super T>>
패턴 2: 외부 Comparator
Comparator<? super T>
패턴 3: 데이터 받는 곳
Collection<? super T>
List<? super T>
패턴 4: 데이터 주는 곳
Collection<? extends T>
List<? extends T>
패턴 5: 변환 함수
Function<? super T, ? extends R>
패턴 6: 검사 함수
Predicate<? super T>
패턴 7: 동작 함수
Consumer<? super T>
패턴 8: 공급 함수
Supplier<? extends T>
Collections 메서드의 PECS 활용을 정리하면?
답:
1. sort: <T extends Comparable<? super T>> + Comparator<? super T>
2. binarySearch: List<? extends Comparable<? super T>> (이중 PE)
3. copy: List<? super T> dest (CS) + List<? extends T> src (PE)
4. max/min: Collection<? extends T> (PE) + Comparator<? super T> (CS)
5. addAll: Collection<? super T> (CS) + T... 가변 인자
6. unmodifiableList: List<? extends T> (PE)
→ 모든 메서드가 PECS 의 일관된 적용.
public interface Stream<T> {
// filter — Predicate is Consumer
Stream<T> filter(Predicate<? super T> predicate);
// map — Function input is Consumer, output is Producer
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// flatMap — 더 복잡
<R> Stream<R> flatMap(
Function<? super T, ? extends Stream<? extends R>> mapper);
// forEach — Consumer
void forEach(Consumer<? super T> action);
// peek — Consumer
Stream<T> peek(Consumer<? super T> action);
// sorted
Stream<T> sorted(Comparator<? super T> comparator);
// min, max
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
// reduce
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
// collect
<R, A> R collect(Collector<? super T, A, R> collector);
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
}
1. filter — Predicate<? super T>
- Predicate.test(T) 받음 → CS
- T 또는 부모의 Predicate 사용 가능
2. map — Function<? super T, ? extends R>
- 입력 T: CS (받음)
- 출력 R: PE (생산)
3. forEach — Consumer<? super T>
- Consumer.accept(T) 받음 → CS
4. min/max — Comparator<? super T>
- Comparator.compare(T, T) 받음 → CS
5. reduce 3-arg — BiFunction<U, ? super T, U>
- 첫 U: 누적
- 두 번째 T: CS (받음)
- 반환 U: 누적
6. collect — Collector<? super T, A, R>
- T 받음 → CS
- A 는 내부
- R 은 결과
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// compose — 다른 Function 을 앞에
// Function<V, R> = Function<V, T> . Function<T, R>
default <V> Function<V, R> compose(
Function<? super V, ? extends T> before) {
// ↑
// 입력 V (앞에 올 함수의 입력) → CS
// 출력 T (앞에 올 함수의 출력) → PE
return (V v) -> apply(before.apply(v));
}
// andThen — 다른 Function 을 뒤에
// Function<T, V> = Function<T, R> . Function<R, V>
default <V> Function<T, V> andThen(
Function<? super R, ? extends V> after) {
// ↑
// 입력 R (뒤에 올 함수의 입력) → CS
// 출력 V (뒤에 올 함수의 출력) → PE
return (T t) -> after.apply(apply(t));
}
}
public final class Optional<T> {
// map — Function input is CS, output is PE
public <U> Optional<U> map(
Function<? super T, ? extends U> mapper) {
if (!isPresent()) return empty();
return Optional.ofNullable(mapper.apply(value));
}
// flatMap
public <U> Optional<U> flatMap(
Function<? super T, ? extends Optional<? extends U>> mapper) {
// 더 복잡 — Optional<? extends U> 도 ? extends
}
// ifPresent — Consumer
public void ifPresent(Consumer<? super T> action) {
if (isPresent()) action.accept(value);
}
// filter — Predicate
public Optional<T> filter(Predicate<? super T> predicate) {
// ...
}
// orElseGet — Supplier
public T orElseGet(Supplier<? extends T> supplier) {
return isPresent() ? value : supplier.get();
}
// orElseThrow with supplier
public <X extends Throwable> T orElseThrow(
Supplier<? extends X> exceptionSupplier) throws X {
if (isPresent()) return value;
throw exceptionSupplier.get();
}
}
public interface Comparator<T> {
int compare(T o1, T o2);
// thenComparing — 다른 Comparator 와 결합
default Comparator<T> thenComparing(Comparator<? super T> other) {
// ↑
// CS (T 받음)
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
// thenComparing with key extractor
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor) {
// ↑ ↑ ↑
// U 비교 가능 T 의 CS U 의 PE
return thenComparing(comparing(keyExtractor));
}
// comparing — static
static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
return (c1, c2) ->
keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
}
public class ShipmentService {
public List<Shipment> processShipments(List<Shipment> shipments) {
return shipments.stream()
.filter(this::isValid) // Predicate<? super Shipment>
.map(this::enrichWithData) // Function<? super Shipment, ? extends Shipment>
.sorted(Comparator.comparing(Shipment::getCreatedAt)) // Comparator<? super Shipment>
.peek(s -> log(s)) // Consumer<? super Shipment>
.toList();
}
public Optional<BigDecimal> findFare(Long id) {
return repository.findById(id)
.map(Shipment::getFare) // Function<? super Shipment, ? extends BigDecimal>
.filter(fare -> fare.compareTo(BigDecimal.ZERO) > 0); // Predicate<? super BigDecimal>
}
public BigDecimal totalFare(List<Shipment> shipments) {
return shipments.stream()
.map(Shipment::getFare)
.reduce(BigDecimal.ZERO, BigDecimal::add); // BiFunction<BigDecimal, ? super BigDecimal, BigDecimal>
}
public Map<String, List<Shipment>> groupByRoute(List<Shipment> shipments) {
return shipments.stream()
.collect(Collectors.groupingBy(Shipment::getRoute)); // Function<? super Shipment, ? extends String>
}
}
Function<? super T, ? extends R> 의 의미는?
답:
? super T (입력):
? extends R (출력):
활용:
Stream.map(Function<? super T, ? extends R>)결과:
PECS 적용 가이드:
1. 매개변수의 역할 명확화
- 무엇을 위해 받는가?
- 어떻게 사용되는가?
2. 호출자 유연성 vs 단순성
- 다양한 타입 받기 → 와일드카드
- 항상 같은 타입 → 일반 T
3. 일관성
- 비슷한 메서드는 비슷한 시그니처
- 자바 표준과 일관
4. 가독성
- 너무 복잡한 시그니처 회피
- 명확한 의도
Step 1: 매개변수 식별
- 무엇을 받는가?
- 어떻게 사용하는가?
Step 2: 역할 분류
- Producer (read only)
- Consumer (write only)
- Both (read + write)
- Neither (메타 작업)
Step 3: PECS 적용
- Producer → ? extends T
- Consumer → ? super T
- Both → T (제약)
- Neither → ? (와일드카드)
Step 4: 반환 타입
- 일반 T 권장
- 와일드카드 회피
Step 5: 호출자 유연성 검증
- 다양한 타입 사용 가능?
- 다양한 자식 사용 가능?
public class ShipmentLibrary {
// 1. 단순 PE — 컬렉션 처리
public static BigDecimal totalWeight(
Collection<? extends Shipment> shipments) {
return shipments.stream()
.map(Shipment::getWeight)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 2. 단순 CS — 데이터 수용
public static <T extends Shipment> void collectInto(
Collection<? super T> destination,
T... shipments) {
for (T s : shipments) {
destination.add(s);
}
}
// 3. 양방향 PECS — 복사
public static <T extends Shipment> void copy(
List<? extends T> source,
List<? super T> destination) {
for (T s : source) {
destination.add(s);
}
}
// 4. 변환
public static <T extends Shipment, R> List<R> map(
Collection<? extends T> source,
Function<? super T, ? extends R> mapper) {
List<R> result = new ArrayList<>();
for (T s : source) {
result.add(mapper.apply(s));
}
return result;
}
// 5. 필터링
public static <T extends Shipment> List<T> filter(
Collection<? extends T> source,
Predicate<? super T> filter) {
List<T> result = new ArrayList<>();
for (T s : source) {
if (filter.test(s)) {
result.add(s);
}
}
return result;
}
// 6. 비교 + PE + Comparable
public static <T extends Shipment & Comparable<? super T>> T heaviest(
Collection<? extends T> shipments) {
return shipments.stream()
.max(Comparator.naturalOrder())
.orElseThrow();
}
// 7. 외부 Comparator
public static <T extends Shipment> T heaviestBy(
Collection<? extends T> shipments,
Comparator<? super T> comparator) {
return shipments.stream()
.max(comparator)
.orElseThrow();
}
// 8. 그룹핑
public static <T extends Shipment, K> Map<K, List<T>> groupBy(
Collection<? extends T> shipments,
Function<? super T, ? extends K> keyExtractor) {
return shipments.stream()
.collect(Collectors.groupingBy(keyExtractor));
}
}
// 다양한 자식 타입과의 활용
List<SeaShipment> seaShipments = ...;
List<AirShipment> airShipments = ...;
// PE — 다양한 자식 받음
BigDecimal seaTotal = ShipmentLibrary.totalWeight(seaShipments); // ✓
BigDecimal airTotal = ShipmentLibrary.totalWeight(airShipments); // ✓
// CS — 다양한 부모 받음
List<Object> all = new ArrayList<>();
ShipmentLibrary.collectInto(all, new SeaShipment(), new AirShipment()); // ✓
// PECS — 양방향
List<Shipment> mixed = new ArrayList<>();
ShipmentLibrary.copy(seaShipments, mixed); // SeaShipment → Shipment
// 변환
List<String> blNos = ShipmentLibrary.map(
seaShipments,
Shipment::getBlNo); // ✓ (Shipment 의 메서드)
// 필터링
List<SeaShipment> heavy = ShipmentLibrary.filter(
seaShipments,
s -> s.getWeight().compareTo(new BigDecimal("1000")) > 0);
// 그룹핑
Map<String, List<SeaShipment>> byRoute = ShipmentLibrary.groupBy(
seaShipments,
Shipment::getRoute);
실수 1: PECS 과도 적용
- 단순한 경우에 와일드카드 남용
- 가독성 ↓
해결:
단순하면 일반 T
실수 2: 반환 타입에 와일드카드
public List<? extends T> ... { ... } ❌
사용자가 활용 어려움
해결:
반환은 일반 T 또는 명시적
실수 3: 같은 매개변수에 양방향 사용
public void process(List<? extends T> list) {
T item = list.get(0); // 읽기 OK
list.add(item); // 쓰기 ❌
}
해결:
read + write 면 일반 T
실수 4: 와일드카드 캡처 무시
public void swap(List<?> list, int i, int j) {
// ? 의 이름이 없어 swap 어려움
}
해결:
헬퍼 메서드의 <T> 캡처
// 안티 패턴 1: 단순한 매개변수에 와일드카드
public <T> int size(Collection<? extends T> coll) { // ❌ 과함
return coll.size();
}
// 권장
public int size(Collection<?> coll) { // ✓ 단순
return coll.size();
}
// 안티 패턴 2: 반환에 와일드카드
public List<? extends T> getItems() { ... } // ❌
// 권장
public List<T> getItems() { ... } // ✓
// 안티 패턴 3: 항상 일치하는 타입에 와일드카드
public <T> void process(List<? extends T> a, List<? extends T> b) { // ❌
// T 가 두 매개변수에서 같아야 하는데 와일드카드 사용
}
// 권장
public <T> void process(List<T> a, List<T> b) { ... } // ✓
// 또는 다른 자식들 OK 면
public <T> void process(List<? extends T> a, List<? extends T> b) { ... } // ✓
// 단, 호출 시 T 의 LUB 추론
PECS 적용 시 흔한 실수 4가지는?
답:
1. 과도 적용: 단순한 경우 와일드카드
2. 반환 타입 와일드카드: 사용자 활용 어려움
3. 양방향 매개변수에 와일드카드: read + write 충돌
4. 캡처 무시: 헬퍼 메서드 없이 자기 자신과의 작업
→ PECS 는 도구, 만능 아님. 단순한 경우엔 일반 T.
Q1. 제네릭이 도입된 Java 버전과 연도?
A1. Java 5 (2004)
Q2. 제네릭의 4가지 효과?
A2. 타입 안전성, 캐스트 제거, 코드 재사용, 가독성
Q3. raw type 정의?
A3. 타입 매개변수 없는 제네릭 사용 (List, Map)
Q4. raw type 의 4가지 위험?
A4. 타입 안전성 손실, CCE, 혼용 위험, 의도 불명확
Q5. 타입 소거 정의?
A5. 컴파일 시 제네릭 타입 정보 제거 (T → Object)
Q6. 타입 소거의 4가지 의도?
A6. 호환성, JVM 최소 변경, 메모리 효율, 점진적 도입
Q7. 다이아몬드 연산자 (Java 7+) 효과?
A7. 우변 타입 추론, 중복 제거
Q8. 제네릭 배열 불가 이유?
A8. 런타임 타입 정보 X (소거)
Q9. instanceof T 불가 이유?
A9. 런타임에 T 무엇인지 모름
Q10. C# 과 자바 제네릭 차이?
A10. C# 은 런타임 타입 보유, 자바는 소거
Q11. T, E, K, V, R 의 의미?
A11. Type, Element, Key, Value, Return
Q12. 제네릭 클래스 정의?
A12. class Name<T> { ... }
Q13. 제네릭 메서드 정의?
A13. public <T> ReturnType method(...) { ... }
Q14. 다중 매개변수 예?
A14. Map<K, V>, Function<T, R>, BiFunction<T, U, R>
Q15. bounded type parameter?
A15. <T extends UpperBound>
Q16. 다중 bound?
A16. <T extends Number & Comparable<T>>
Q17. 재귀 bound?
A17. <T extends Comparable<T>>
Q18. static 멤버에 T 못 쓰는 이유?
A18. 인스턴스 vs 클래스 레벨 충돌
Q19. new T() 못 하는 이유?
A19. 런타임에 T 무엇인지 모름
Q20. 우회 방법?
A20. Class<T>, Supplier<T> 사용
Q21. 제네릭의 불공변성 의미?
A21. List<Integer> 와 List<Number> 무관
Q22. 와일드카드 ? 의 정의?
A22. "어떤 타입" placeholder
Q23. <?> 의 의미?
A23. 어떤 타입의 List 든 (unbounded)
Q24. <? extends T> 의미?
A24. T 또는 자식 (공변, Producer)
Q25. <? super T> 의미?
A25. T 또는 부모 (반공변, Consumer)
Q26. List<?> 에 add 가능한 것?
A26. null 만
Q27. List<? extends Number> 에 add 가능?
A27. null 만 (어떤 자식인지 모름)
Q28. List<? super Integer> 의 get 결과 타입?
A28. Object
Q29. 와일드카드 캡처?
A29. 헬퍼 메서드의 T 가 ? 를 캡처
Q30. List<?> vs List<Object> 차이?
A30. 어떤 타입 vs Object 전용
Q31. 제네릭 클래스 vs 메서드 차이?
A31. 인스턴스 단위 vs 호출 단위
Q32. 타입 추론 메커니즘?
A32. 컨텍스트 분석 + 제약 수집 + LUB
Q33. 타깃 타입?
A33. 표현식의 기대되는 타입
Q34. var 와 다이아몬드 동시 사용?
A34. ArrayList<Object> 가 됨 (의도와 다름)
Q35. 클래스 T + 메서드 U 패턴?
A35. Optional.map, Stream.map
Q36. S extends T 의 의미?
A36. T 의 자식만 받음
Q37. Self-referential bound 활용?
A37. Builder 패턴, Fluent API
Q38. 익명 vs 람다?
A38. SAM 만 람다, 일반은 익명
Q39. Function.compose vs andThen?
A39. V→T→R vs T→R→V
Q40. 자바 표준의 정적 팩토리 예?
A40. Optional.of, Stream.of, List.of
Q41. PECS 의 의미?
A41. Producer Extends, Consumer Super
Q42. PE 의 본질?
A42. 데이터 꺼내는 매개변수, 자식 받기
Q43. CS 의 본질?
A43. 데이터 받는 매개변수, 부모 받기
Q44. PECS 결정 4단계?
A44. 역할 분석, 입출력 검토, 와일드카드 선택, 호환성 검증
Q45. Comparable<? super T> 의미?
A45. T 또는 부모와 비교 가능
Q46. Collections.copy 시그니처?
A46. <T> void copy(List<? super T> dest, List<? extends T> src)
Q47. Function<? super T, ? extends R> 의미?
A47. T 의 부모 받고, R 의 자식 반환
Q48. PECS 안티 패턴?
A48. 과도 적용, 반환 와일드카드, 양방향 매개변수
Q49. Comparator<? super T> 가 필요한 이유?
A49. T 의 부모를 비교하는 Comparator 도 받음
Q50. Phase 5 마스터 후 가능한 일?
A50. 어떤 제네릭 시그니처도 자신 있게 설계
50 / 50 → Phase 5 마스터
45-49 → 거의 마스터, 일부 복습
40-44 → 핵심 다시 학습
< 40 → Unit 5.1 ~ 5.5 재학습
1. 자바 제네릭 자유자재
- 어떤 시그니처도 이해
- 직접 설계 가능
- PECS 자동 적용
2. 자바 표준 라이브러리 깊이 이해
- Collections, Stream, Optional
- Function, Comparator
- 모든 시그니처 분석 가능
3. 면접 자신감
- 제네릭 모든 질문 즉답
- PECS 설명 가능
- 와일드카드 활용
4. 자체 라이브러리 설계
- 유틸리티 클래스
- 함수형 인터페이스
- 도메인 추상화
5. 다음 Phase 준비
- Phase 6: 객체 비교 (Comparable/Comparator)
- Phase 7: I/O 시스템
- Phase 8: Stream 실전
- Phase 9: I/O 강화
- Phase 10: 함수형 프로그래밍
✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit) ← 완주
🚀 Phase 6 — 객체 비교 (다음)
⏭ Phase 7 — I/O 시스템 큰 그림
⏭ Phase 8 — Stream 실전
⏭ Phase 9 — I/O 강화
⏭ Phase 10 — 함수형 프로그래밍
총: 22/43 Unit 작성 (Phase 5 완주, 약 51%)
1. PECS 원칙
? extends T? super T2. 결정 알고리즘
3. Phase 5 마스터
🚀 Phase 5 — 제네릭과 와일드카드
✅ Unit 5.1 제네릭의 등장과 raw type
✅ Unit 5.2 타입 매개변수와 타입 인자
✅ Unit 5.3 와일드카드 ? extends, ? super
✅ Unit 5.4 제네릭 메서드 + 제네릭 클래스
✅ Unit 5.5 PECS 원칙 (마스터 깊이) ← 여기, Phase 5 완주
→ 자바 제네릭 정복
→ PECS 원칙 자유 활용
→ 자바 표준 라이브러리 깊이 이해
→ 자체 라이브러리 설계 가능
다음 Phase 는 Comparable/Comparator 정밀.
Phase 6 — 객체 비교
Unit 6.1 — equals 와 hashCode 의 계약
→ Object 의 두 메서드 정밀
→ 5가지 계약 (자기성, 대칭성, 추이성 등)
Unit 6.2 — Comparable<T> 의 자연 순서
→ compareTo 의 구현
→ 일관성 (equals 와의 관계)
Unit 6.3 — Comparator<T> 의 외부 비교
→ 다양한 기준의 비교
→ comparing, thenComparing 활용
Unit 6.4 — 비교의 종합 활용 (마스터 깊이)
→ 정렬, 검색, TreeMap, TreeSet
→ equals/hashCode/compareTo 의 일관성
Phase 5 와의 연결:
🏆 Phase 5 완주 — 제네릭 마스터 달성