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

Psj·2026년 5월 19일

F-lab

목록 보기
95/240

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

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


📌 학습 목표

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

  • 제네릭 클래스 vs 제네릭 메서드 의 결정적 차이는?
  • 타입 추론 (Type Inference) 의 메커니즘은?
  • 타깃 타입 (Target Typing) 이 무엇이고 어떻게 동작하나?
  • 제네릭 생성자 의 문법과 활용은?
  • 제네릭 클래스 + 제네릭 메서드 의 결합은?
  • var 키워드 (Java 10+) 와 제네릭의 관계는?
  • 익명 제네릭 클래스 와 로컬 제네릭 메서드 의 활용은?
  • 자바 표준 라이브러리 의 정밀한 제네릭 시그니처는?
  • 타입 추론 한계 와 명시적 타입 인자 사용 시점은?

🎯 핵심 한 문장

제네릭 클래스는 "타입이 인스턴스마다 다른" 클래스, 제네릭 메서드는 "타입이 호출마다 다른" 메서드다.
컴파일러의 타입 추론 으로 대부분의 호출에서 명시적 타입 인자가 불필요하며,
다이아몬드 (<>) 와 var 가 이를 더 간결하게 만든다.
자바 표준 라이브러리 (Collections.sort, Stream.collect, Optional.map 등) 의 정밀한 시그니처가
모두 이 두 도구의 조합과 PECS 원칙으로 설계되어 있다.

비유 — 공장과 작업자

제네릭 클래스 = 공장
  Box<T> = "타입 T 의 박스를 만드는 공장"
  - 한 공장이 정해진 한 타입의 박스만
  - 인스턴스마다 타입 다름
  - String 박스 공장, Integer 박스 공장 등

제네릭 메서드 = 작업자
  <T> T process(T t) = "한 호출당 한 타입을 처리하는 작업자"
  - 호출마다 타입 다름
  - 같은 메서드가 String 처리, Integer 처리, ...
  - 클래스 인스턴스화 없이 호출 가능 (static)

→ 클래스 = 인스턴스 단위 타입, 메서드 = 호출 단위 타입.


🧭 9개 섹션 로드맵

1. 제네릭 클래스 vs 제네릭 메서드의 차이
2. 타입 추론 (Type Inference) 메커니즘
3. 타깃 타입 (Target Typing)
4. 제네릭 생성자
5. 제네릭 클래스 + 제네릭 메서드 결합
6. var 와 제네릭
7. 익명/로컬 제네릭 활용
8. 자바 표준 라이브러리 정밀 분석
9. 면접 + 자기 점검

1️⃣ 제네릭 클래스 vs 제네릭 메서드의 차이

1.1 정의 시점의 차이

// 제네릭 클래스 — 클래스 이름 뒤에 <T>
public class Box<T> {
    private T value;
    
    public Box(T value) {
        this.value = value;
    }
    
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}

// 제네릭 메서드 — 반환 타입 앞에 <T>
public class Utils {
    public static <T> T identity(T input) {
        return input;
    }
}

핵심:

  • 클래스: class Name<T>
  • 메서드: public <T> ReturnType method(...)

1.2 사용 시점의 차이

// 제네릭 클래스 사용
Box<String> stringBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);
// 각 인스턴스가 정해진 T 보유

stringBox.set("world");   // String 만
intBox.set(99);            // Integer 만

// 제네릭 메서드 사용
String s = Utils.identity("hello");      // T = String
Integer i = Utils.identity(42);           // T = Integer
Shipment sh = Utils.identity(new Shipment());   // T = Shipment

// 같은 메서드, 호출마다 다른 T

1.3 인스턴스화 여부

// 제네릭 클래스 — 인스턴스화 필요
Box<String> box = new Box<>("hello");   // ★ new 필요
String value = box.get();

// 제네릭 메서드 — static 이면 인스턴스화 불필요
String result = Utils.identity("hello");   // 호출만

// 인스턴스 메서드라면 인스턴스화 필요
public class MyClass {
    public <T> T process(T input) { return input; }
}

MyClass obj = new MyClass();
String s = obj.process("hello");

1.4 T 의 스코프

public class Box<T> {                    // 클래스의 T
    private T value;                       // 필드 — 클래스 T
    
    public T get() {                       // 메서드 — 클래스 T
        return value;
    }
    
    public <U> U transform(Function<T, U> fn) {   // 메서드 — 자기 U
        return fn.apply(value);
        //     ↑
        //     value 의 타입 T (클래스), fn 의 반환 U (메서드)
    }
    
    // T 와 U 는 서로 무관
}

// 사용
Box<String> box = new Box<>("hello");
Integer length = box.transform(String::length);
//                                   ↑
//                                   T = String (클래스), U = Integer (메서드)

1.5 static 제약의 차이

public class Box<T> {
    
    private T value;   // OK — 인스턴스 필드
    
    // ❌ static 필드에 T 불가
    private static T staticValue;
    
    // ❌ static 메서드의 매개변수에 클래스 T 불가
    public static void process(T input) { }
    
    // ✓ static 메서드는 자기만의 T 가능
    public static <T> T identity(T input) {   // 메서드 T, 클래스와 무관
        return input;
    }
}

이유:

  • 클래스의 T 는 인스턴스마다 다름
  • static 은 클래스 레벨 (인스턴스 무관)
  • 모순

1.6 활용 패턴

// 제네릭 클래스 활용
public class Cache<K, V> {
    private Map<K, V> store = new HashMap<>();
    
    public V get(K key) { return store.get(key); }
    public void put(K key, V value) { store.put(key, value); }
}

Cache<Long, Shipment> shipmentCache = new Cache<>();
Cache<String, User> userCache = new Cache<>();

// 제네릭 메서드 활용 (보통 유틸리티)
public class CollectionUtils {
    public static <T> List<T> repeat(T value, int times) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            list.add(value);
        }
        return list;
    }
    
    public static <T> Optional<T> first(List<T> list) {
        return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
    }
}

// 호출
List<String> repeated = CollectionUtils.repeat("hello", 3);
Optional<Integer> first = CollectionUtils.first(List.of(1, 2, 3));

1.7 둘이 함께 사용되는 경우

public class Container<T> {        // 클래스의 T
    private List<T> items = new ArrayList<>();
    
    public void add(T item) {       // 클래스 T 사용
        items.add(item);
    }
    
    public T get(int i) {            // 클래스 T 반환
        return items.get(i);
    }
    
    // 메서드만의 추가 매개변수
    public <R> List<R> map(Function<T, R> fn) {   // 클래스 T, 메서드 R
        List<R> result = new ArrayList<>();
        for (T item : items) {
            result.add(fn.apply(item));
        }
        return result;
    }
    
    // 메서드만의 또 다른 T (shadowing — 권장 X)
    // public <T> void process(T anotherT) { ... }
    // 같은 이름 T 이지만 클래스 T 와 다름 — 혼란 → 회피
}

// 사용
Container<String> container = new Container<>();
container.add("hello");

List<Integer> lengths = container.map(String::length);
//                                  ↑
//                                  T = String, R = Integer

1.8 자기 점검 답변

제네릭 클래스와 제네릭 메서드의 결정적 차이는?

답:
1. 타입 결정 시점:

  • 클래스: 인스턴스화 시 결정 (인스턴스마다 다름)
  • 메서드: 호출 시 결정 (호출마다 다름)
  1. 선언 위치:

    • 클래스: class Name<T>
    • 메서드: <T> ReturnType method(...)
  2. static 제약:

    • 클래스 T 는 static 멤버에 사용 불가
    • 메서드 자신의 T 는 static 메서드에 가능
  3. 활용 패턴:

    • 클래스: 데이터 컨테이너 (Box, Cache)
    • 메서드: 유틸리티 (Collections, Stream)

2️⃣ 타입 추론 (Type Inference) 메커니즘

2.1 타입 추론의 정의

타입 추론 (Type Inference):

  컴파일러가 컨텍스트를 분석해 타입 매개변수를 자동으로 결정.
  
  사용자가 명시 안 해도 컴파일러가 추론.

2.2 추론의 시점

// 1. 다이아몬드 연산자 (Java 7+)
List<String> list = new ArrayList<>();
//                                 ↑
//                                 컴파일러가 String 추론

// 2. 제네릭 메서드 호출 (Java 5+)
String s = Utils.identity("hello");
//             ↑
//             T = String 추론

// 3. 람다 매개변수 (Java 8+)
List<String> list = ...;
list.stream().filter(s -> s.length() > 5);
//                   ↑
//                   s 의 타입 String 추론

// 4. var 키워드 (Java 10+)
var list = new ArrayList<String>();
//   ↑
//   ArrayList<String> 추론

2.3 추론 알고리즘 (간략)

컴파일러의 추론 과정:

1. 컨텍스트 분석:
   - 좌변 타입
   - 메서드 시그니처
   - 호출 인자

2. 제약 수집:
   - "T 는 String 과 호환"
   - "T 는 Comparable 의 구현"
   - 등등

3. 가장 좁은 타입 결정:
   - 제약을 만족하는 가장 정확한 타입
   - 보통 인자의 타입

4. 타입 매개변수 대체:
   - T 자리에 실제 타입 삽입

2.4 단순한 추론 예

public static <T> T identity(T input) {
    return input;
}

// 호출 분석:
String s = identity("hello");

// 1. 컨텍스트: 반환을 String 변수에 할당
// 2. 인자 타입: "hello" → String
// 3. 결과: T = String 추론
// 4. 메서드 시그니처가 됨: String identity(String input)

2.5 복잡한 추론

public static <T> List<T> singletonList(T value) {
    return Collections.singletonList(value);
}

// 호출 1: 인자만 사용
List<Integer> list = singletonList(42);
// 1. 인자: 42 → Integer
// 2. T = Integer 추론
// 3. 반환: List<Integer>

// 호출 2: 좌변 활용
List<Number> list = singletonList(42);
// 1. 좌변: List<Number>
// 2. 인자: 42 → Integer
// 3. T = Number? Integer? 
// 4. Integer 가 더 정확 → T = Integer
// 5. 반환 List<Integer> 를 List<Number> 에 ❌
// 컴파일 에러: incompatible types

// 호출 3: 명시적
List<Number> list = Utils.<Number>singletonList(42);
// 1. T = Number 명시
// 2. 인자: 42 → Integer (Number 의 자식, 자동 변환)
// 3. 반환 List<Number>, 좌변과 일치 ✓

2.6 자동 박싱과의 결합

public static <T> List<T> single(T value) {
    return List.of(value);
}

// 호출
List<Integer> list = single(42);
// 1. 42 는 int (primitive)
// 2. 자동 박싱: int → Integer
// 3. T = Integer 추론
// 4. 반환 List<Integer>

// 주의: int 의 List 는 불가
// List<int> list = single(42);   // ❌
// 제네릭은 객체 타입만 (Integer 만)

2.7 가장 좁은 공통 타입

public static <T> T pick(T a, T b) {
    return a;
}

// 호출 1: 같은 타입
String s = pick("a", "b");   // T = String

// 호출 2: 다른 타입 — LUB (Least Upper Bound)
// Integer 와 Double 의 공통 부모
Number n = pick(1, 1.0);
// T = Number (LUB)
// 또는 더 정확히: Number & Comparable<...>

// Java 의 LUB 알고리즘:
// 두 타입의 공통 부모 중 가장 좁은 것

2.8 추론 vs 명시

public class Utils {
    public static <T> List<T> emptyList() {
        return new ArrayList<>();
    }
}

// 추론 (가능한 경우)
List<String> list1 = Utils.emptyList();   // 좌변에서 T = String

// 명시 (필요한 경우)
List<String> list2 = Utils.<String>emptyList();   // T 명시

// 명시 필요한 경우:
// 1. 메서드 인자에 T 없음 (좌변만 추론 단서)
// 2. 다른 코드와 함께 사용
process(Utils.emptyList());   // process(Object) 추론
process(Utils.<String>emptyList());   // process(List<String>)

2.9 자기 점검 답변

자바의 타입 추론이 어떻게 동작하나?

답:
1. 컨텍스트 분석:

  • 좌변 타입
  • 메서드 시그니처
  • 호출 인자
  1. 제약 수집:

    • 각 사용처의 타입 요구사항
  2. 추론:

    • 가장 좁은 타입 결정
    • LUB (Least Upper Bound) 계산
  3. 결과:

    • 타입 매개변수 자동 결정
    • 명시 불필요

→ Java 7 다이아몬드, Java 10 var 로 더 강력해짐.


3️⃣ 타깃 타입 (Target Typing)

3.1 타깃 타입의 정의

타깃 타입 (Target Typing):

  표현식의 컨텍스트로부터 기대되는 타입.
  
  컴파일러가 이를 활용해 타입 추론.

예:
  List<String> list = new ArrayList<>();
                      ↑                  ↑
                      이게 표현식       타깃 타입은 List<String>

3.2 다이아몬드와 타깃 타입

// Java 7+
List<String> list = new ArrayList<>();
// 1. 좌변 타깃 타입: List<String>
// 2. 우변 다이아몬드: ArrayList<?>
// 3. 추론: ArrayList<String>

// 더 복잡한 케이스
Map<String, List<Integer>> map = new HashMap<>();
// 타깃: Map<String, List<Integer>>
// 추론: HashMap<String, List<Integer>>

3.3 메서드 인자의 타깃 타입

public void process(List<String> list) { ... }

// 호출
process(new ArrayList<>());
// 1. process 의 매개변수 타입: List<String>
// 2. 타깃 타입: List<String>
// 3. 추론: new ArrayList<String>()

// 더 복잡
process(Utils.emptyList());
// emptyList 의 반환 타입 추론
// 타깃: List<String>
// 추론: T = String, 반환 List<String>

3.4 람다와 타깃 타입

Comparator<String> comp = (a, b) -> a.length() - b.length();
// 1. 좌변 타깃: Comparator<String>
// 2. 람다의 매개변수: a, b (타입 없음)
// 3. Comparator<String>.compare(String, String) 시그니처
// 4. 추론: a 는 String, b 는 String

// 타깃 없으면 추론 실패
Object x = (a, b) -> a.length() - b.length();
// 컴파일 에러: 타깃이 Comparator 인지 모름

3.5 Stream 의 타깃 타입

List<String> names = ...;

// 타깃 타입 활용
List<String> upper = names.stream()
    .map(s -> s.toUpperCase())
    .collect(Collectors.toList());

// 컴파일러의 분석:
// 1. names.stream() → Stream<String>
// 2. map(Function<String, R>) — R 추론 필요
// 3. s -> s.toUpperCase() — s 는 String, 반환 String
// 4. R = String 추론
// 5. .map() 반환: Stream<String>
// 6. collect → 타깃이 List<String>
// 7. Collectors.toList() — T = String 추론

3.6 Optional 의 타깃 타입

public Optional<Integer> findAge(String name) {
    return Optional.of(...);
}

// 빈 Optional
public Optional<Integer> notFound() {
    return Optional.empty();
    // empty() 의 타입은 Optional<T>
    // 메서드 반환 타입 Optional<Integer> 가 타깃
    // T = Integer 추론
}

// 명시 필요한 경우
public <T> Optional<T> get() {
    return Optional.<T>empty();
    // 타깃이 Optional<T> 인데 T 는 메서드 자신의 T
    // 명시가 더 안전
}

3.7 메서드 체이닝과 타깃

List<Shipment> shipments = ...;

// 체이닝의 각 단계마다 타깃이 다음 단계
Map<String, BigDecimal> totalByRoute = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getRoute,
        Collectors.reducing(
            BigDecimal.ZERO,
            Shipment::getFare,
            BigDecimal::add
        )
    ));

// 컴파일러가 각 Collectors 의 T, K, V, R 추론

3.8 타깃 타입의 한계

// 타깃 타입이 없으면 추론 실패
Utils.emptyList().size();   
// 좌변이 size() 의 호출 — int
// emptyList() 의 타입 추론?
// 기본: List<Object>

// 명확하게:
List<String> list = Utils.emptyList();   // 타깃 명시
list.size();

3.9 자기 점검 답변

타깃 타입이 무엇이고 왜 중요한가?

답:

  • 정의:

    • 표현식의 컨텍스트에서 기대되는 타입
    • 좌변, 메서드 매개변수, 반환 등
  • 활용:

    • 다이아몬드: new ArrayList<>()
    • 람다: Comparator<String> c = (a, b) -> ...
    • Stream: Collectors.toList()
    • Optional: Optional.empty()
  • 중요한 이유:

    • 타입 추론의 핵심
    • 코드 간결화
    • 명시 불필요
  • 한계:

    • 타깃 없으면 추론 실패
    • 명시적 타입 인자 필요

4️⃣ 제네릭 생성자

4.1 제네릭 생성자의 정의

제네릭 생성자:
  
  자체 타입 매개변수를 가진 생성자.
  클래스가 제네릭이든 아니든 가능.

4.2 기본 문법

// 일반 클래스의 제네릭 생성자
public class MyClass {
    private Object value;
    
    public <T> MyClass(T value) {   // ← 생성자만의 T
        this.value = value;
        System.out.println("Created with: " + value.getClass());
    }
}

// 사용
MyClass m1 = new MyClass("hello");        // T = String
MyClass m2 = new MyClass(42);              // T = Integer
MyClass m3 = new <Integer>MyClass(42);     // 명시

4.3 제네릭 클래스의 제네릭 생성자

public class Pair<A, B> {
    private A first;
    private B second;
    
    // 기본 생성자 — 클래스 T 사용
    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }
    
    // 제네릭 생성자 — 자신만의 T
    public <T extends Comparable<T>> Pair(T value, B second) {
        this.first = (A) value;   // 캐스트 필요
        this.second = second;
    }
}

4.4 활용 예 — 변환 생성자

public class TypedBox<T> {
    private T value;
    
    public TypedBox(T value) {
        this.value = value;
    }
    
    // 다른 타입의 TypedBox 로부터 생성 + 변환
    public <U> TypedBox(TypedBox<U> source, Function<U, T> converter) {
        this.value = converter.apply(source.value);
    }
}

// 사용
TypedBox<String> stringBox = new TypedBox<>("123");
TypedBox<Integer> intBox = new TypedBox<>(stringBox, Integer::parseInt);
// String "123" → Integer 123

4.5 자바 표준의 제네릭 생성자

// ArrayList 의 생성자
public class ArrayList<E> {
    
    // 기본 — 클래스 E 사용
    public ArrayList() { ... }
    
    public ArrayList(int initialCapacity) { ... }
    
    // 컬렉션 생성자 — 자신의 E + 와일드카드
    public ArrayList(Collection<? extends E> c) { ... }
    //                          ↑
    //                          Producer
}

// HashMap 도 비슷
public class HashMap<K, V> {
    public HashMap() { ... }
    public HashMap(int initialCapacity) { ... }
    public HashMap(int initialCapacity, float loadFactor) { ... }
    public HashMap(Map<? extends K, ? extends V> m) { ... }
}

4.6 명시적 타입 인자

public class MyClass {
    public <T> MyClass(T value) { ... }
}

// 1. 추론 (대부분)
MyClass m = new MyClass("hello");   // T = String 추론

// 2. 명시 (드물게)
MyClass m = new <String>MyClass("hello");
// ← <Type> 이 new 와 클래스명 사이

// 또는
MyClass m = new MyClass.<String>method(...);
// 인스턴스 메서드 호출 시

// 다이아몬드 + 명시
List<String> list = new ArrayList<String>();   // 명시
List<String> list = new ArrayList<>();          // 다이아몬드

4.7 제네릭 생성자의 활용 시점

권장:

1. 변환 생성자
   - 다른 타입에서 생성
   - 예: TypedBox(TypedBox<U>, Function<U, T>)

2. 빌더 패턴
   - 다양한 입력에서 동일 객체 생성
   - 예: <T> Builder(T input)

3. 팩토리 메서드와 결합
   - 정적 팩토리가 제네릭이라면

회피:

1. 단순한 경우
   - 클래스 매개변수로 충분
   - 복잡도만 ↑

2. 의도 불명확한 경우
   - 사용자 혼란

4.8 자기 점검 답변

제네릭 생성자의 활용 사례는?

답:
1. 변환 생성자:

  • 다른 타입에서 객체 생성
  • <U> TypedBox(TypedBox<U>, Function<U, T>)
  1. 컬렉션 복사 생성자:

    • 와일드카드 활용
    • ArrayList(Collection<? extends E>)
  2. 유틸리티 빌더:

    • 다양한 입력 지원
  3. 드물게 사용:

    • 클래스 T 로 대부분 해결
    • 정말 필요할 때만

5️⃣ 제네릭 클래스 + 제네릭 메서드 결합

5.1 결합 패턴 1 — 클래스 T + 메서드 U

public class Box<T> {
    private T value;
    
    public Box(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
    
    // 메서드만의 U
    public <U> U transform(Function<T, U> fn) {
        return fn.apply(value);
    }
    
    // 다중 메서드 매개변수
    public <U, V> Pair<U, V> mapToPair(
            Function<T, U> first,
            Function<T, V> second) {
        return new Pair<>(first.apply(value), second.apply(value));
    }
}

// 사용
Box<String> box = new Box<>("hello");

Integer length = box.transform(String::length);   // T=String, U=Integer
Boolean empty = box.transform(String::isEmpty);   // T=String, U=Boolean

Pair<Integer, Character> pair = box.mapToPair(
    String::length,        // U=Integer
    s -> s.charAt(0)        // V=Character
);

5.2 결합 패턴 2 — 메서드의 타입 매개변수가 클래스 T 의 자식

public class Container<T> {
    private List<T> items = new ArrayList<>();
    
    public void add(T item) {
        items.add(item);
    }
    
    // 메서드 매개변수가 T 의 자식만
    public <S extends T> void addAll(Collection<S> source) {
        for (S item : source) {
            items.add(item);   // S 가 T 의 자식이라 가능
        }
    }
    
    // 와일드카드도 가능
    public void addAllWild(Collection<? extends T> source) {
        for (T item : source) {
            items.add(item);
        }
    }
}

// 사용
Container<Number> container = new Container<>();

container.addAll(List.of(1, 2, 3));   // S = Integer (Number 의 자식)
container.addAll(List.of(1.0, 2.0));   // S = Double

// 다음도 가능
container.addAllWild(List.<Integer>of(1, 2));   // ? extends Number

5.3 결합 패턴 3 — 정적 팩토리 메서드

public class Pair<A, B> {
    private A first;
    private B second;
    
    // 정적 팩토리 — 메서드 자체의 T, U
    public static <T, U> Pair<T, U> of(T first, U second) {
        return new Pair<>(first, second);
    }
    
    // 단일 타입 팩토리
    public static <T> Pair<T, T> twin(T value) {
        return new Pair<>(value, value);
    }
}

// 사용
Pair<String, Integer> p1 = Pair.of("age", 25);
Pair<String, String> p2 = Pair.twin("hello");

5.4 결합 패턴 4 — Self-referential

public abstract class Builder<B extends Builder<B>> {
    
    protected abstract B self();
    
    public B with(String key, Object value) {
        // 설정
        return self();
    }
}

// 자식이 B 명시
public class ShipmentBuilder extends Builder<ShipmentBuilder> {
    
    @Override
    protected ShipmentBuilder self() {
        return this;
    }
    
    // 자식의 메서드 추가
    public ShipmentBuilder withBlNo(String blNo) {
        return with("blNo", blNo);
    }
}

// 사용 — Fluent API
ShipmentBuilder builder = new ShipmentBuilder()
    .with("priority", 1)         // ShipmentBuilder 반환
    .withBlNo("BL-001")           // ShipmentBuilder 의 메서드
    .with("urgency", "high");

5.5 자바 표준의 결합 패턴

// 1. Optional<T>
public final class Optional<T> {
    
    public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        // 클래스 T, 메서드 U
    }
    
    public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
        // 클래스 T, 메서드 U
    }
    
    public static <T> Optional<T> of(T value) {
        // 정적 팩토리, 메서드 T
    }
}

// 2. Stream<T>
public interface Stream<T> {
    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    
    <R, A> R collect(Collector<? super T, A, R> collector);
}

// 3. CompletableFuture<T>
public class CompletableFuture<T> {
    
    public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) { ... }
    
    public <U, V> CompletableFuture<V> thenCombine(
            CompletionStage<? extends U> other,
            BiFunction<? super T, ? super U, ? extends V> fn) { ... }
}

5.6 ILIC 결합 예

public class ShipmentRepository<T extends Shipment> {
    private final Map<Long, T> cache = new HashMap<>();
    
    public T save(T shipment) {
        cache.put(shipment.getId(), shipment);
        return shipment;
    }
    
    public Optional<T> findById(Long id) {
        return Optional.ofNullable(cache.get(id));
    }
    
    // 메서드 자체의 R
    public <R> List<R> findAllAndMap(Function<T, R> mapper) {
        return cache.values().stream()
            .map(mapper)
            .toList();
    }
    
    // S extends T — T 의 자식만 추가
    public <S extends T> void saveAll(Collection<S> shipments) {
        for (S s : shipments) {
            save(s);
        }
    }
    
    // 정적 팩토리
    public static <T extends Shipment> ShipmentRepository<T> create() {
        return new ShipmentRepository<>();
    }
}

// 사용
ShipmentRepository<Shipment> repo = ShipmentRepository.create();
repo.save(new SeaShipment());
repo.save(new AirShipment());

repo.saveAll(List.<SeaShipment>of(...));   // S = SeaShipment

List<String> blNos = repo.findAllAndMap(Shipment::getBlNo);   // R = String
List<BigDecimal> weights = repo.findAllAndMap(Shipment::getWeight);   // R = BigDecimal

5.7 결합의 한계

public class Box<T> {
    private T value;
    
    // ❌ T 와 같은 이름 사용 (shadowing — 권장 X)
    public <T> T processAnother(T other) {
        // 여기 T 는 메서드의 T, 클래스 T 와 무관
        // 혼란!
        return other;
    }
    
    // ✓ 다른 이름 사용
    public <U> U processAnother(U other) {
        return other;
    }
}

→ 같은 이름의 타입 매개변수는 shadowing 발생, 회피.

5.8 자기 점검 답변

제네릭 클래스의 T 와 제네릭 메서드의 U 를 함께 쓰는 경우는?

답:
1. 변환 메서드:

  • <U> U transform(Function<T, U> fn)
  • T 입력, U 출력
  1. 추가 매개변수:

    • <S extends T> void addAll(Collection<S>)
    • T 의 자식만 받기
  2. 정적 팩토리:

    • static <T> Optional<T> of(T value)
    • 클래스와 무관한 메서드 T
  3. 자바 표준의 정밀 패턴:

    • Optional.map, Stream.map, Stream.collect
    • PECS 활용

→ 클래스 T 와 메서드 U 를 명확히 구분.


6️⃣ var 와 제네릭

6.1 var 키워드 (Java 10+)

// Java 10 부터
var list = new ArrayList<String>();
//   ↑
//   ArrayList<String> 으로 추론

var map = new HashMap<Long, Shipment>();
//   ↑
//   HashMap<Long, Shipment> 으로 추론

6.2 var 의 제약

// ✓ 가능
var x = 10;                              // int
var s = "hello";                          // String
var list = new ArrayList<String>();       // ArrayList<String>
var stream = list.stream();               // Stream<String>

// ❌ 불가
// 1. 초기화 없이
var x;   // 컴파일 에러

// 2. null 만으로
var x = null;   // 컴파일 에러

// 3. 람다 직접 (Java 11+ 부터 매개변수에 가능)
var fn = (String s) -> s.length();   // 에러

// 4. 메서드 매개변수
public void process(var x) { }   // 에러

// 5. 필드
public class MyClass {
    var field;   // 에러
}

6.3 var 와 다이아몬드

// 명시적 타입 + 다이아몬드
List<String> list = new ArrayList<>();
// 좌변에서 타입, 우변 다이아몬드

// var + 명시적 타입
var list = new ArrayList<String>();
// 우변에서 타입, 좌변 var

// var + 다이아몬드 — 컴파일러 추론 실패
var list = new ArrayList<>();   // ArrayList<Object> 가 됨
// 좌변 var 는 타깃 X
// 우변 <> 는 좌변 의존
// 결과: ArrayList<Object> (의도와 다름)

→ var 와 다이아몬드 동시 사용은 의도와 다른 결과 가능성.

6.4 var 의 활용

// 1. 명확한 우변
var list = new ArrayList<String>();       // 명확
var map = new HashMap<Long, Shipment>();   // 명확

// 2. Stream 결과
var groups = shipments.stream()
    .collect(Collectors.groupingBy(Shipment::getRoute));
// 타입: Map<String, List<Shipment>>
// 명시하면 매우 김

// 3. 메서드 반환 활용
var result = service.findActive();
// service.findActive() 의 반환 타입으로

// 4. 람다 매개변수 (Java 11+)
list.forEach((var s) -> System.out.println(s));
//             ↑
//             s 의 타입 추론 (String)

6.5 var 사용 가이드

권장:
  ✓ 우변에서 타입이 명확
  ✓ 긴 제네릭 타입 (가독성 ↑)
  ✓ 로컬 변수
  ✓ Stream 결과 등 복잡한 타입

회피:
  ✗ 우변이 모호
  ✗ 변수 이름이 의미 불명확
  ✗ public API
  ✗ 디버깅 어려운 곳

6.6 ILIC 활용 예

public class ShipmentService {
    
    public void processShipments() {
        // var 활용
        var shipments = repository.findAll();
        // List<Shipment>
        
        var byRoute = shipments.stream()
            .collect(Collectors.groupingBy(Shipment::getRoute));
        // Map<String, List<Shipment>>
        
        var totalByRoute = byRoute.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().stream()
                    .map(Shipment::getFare)
                    .reduce(BigDecimal.ZERO, BigDecimal::add)
            ));
        // Map<String, BigDecimal>
        
        // 짧고 명확
    }
}

6.7 var vs 다이아몬드 비교

항목다이아몬드 <>var
도입Java 7Java 10
타입 정보좌변우변
사용변수 선언변수 선언
제약거의 없음초기화 필수, 람다 X
활용컬렉션 생성모든 로컬 변수

6.8 자기 점검 답변

var 와 다이아몬드를 어떻게 조합하나?

답:
1. 좌변 명시 + 다이아몬드 (Java 7+):

  • List<String> list = new ArrayList<>();
  • 좌변에서 타입, 우변 추론
  1. var + 우변 명시 (Java 10+):

    • var list = new ArrayList<String>();
    • 우변에서 타입, 좌변 추론
  2. 둘 다 사용은 의도와 다름:

    • var list = new ArrayList<>();
    • → ArrayList

      가이드:

      • 우변이 명확하면 var
      • 좌변이 명확하면 다이아몬드
      • 동시 사용은 회피

      7️⃣ 익명/로컬 제네릭 활용

      7.1 익명 제네릭 클래스

      public interface Comparator<T> {
          int compare(T o1, T o2);
      }
      
      // 익명 클래스로 구현
      Comparator<String> byLength = new Comparator<String>() {
          @Override
          public int compare(String s1, String s2) {
              return s1.length() - s2.length();
          }
      };
      
      // Java 9+ 다이아몬드 가능
      Comparator<String> byLength2 = new Comparator<>() {
          @Override
          public int compare(String s1, String s2) {
              return s1.length() - s2.length();
          }
      };
      
      // 람다로 더 간결
      Comparator<String> byLength3 = (s1, s2) -> s1.length() - s2.length();

      7.2 익명 제네릭 클래스의 활용

      // Iterator 직접 구현
      public Iterator<Shipment> activeIterator(List<Shipment> all) {
          return new Iterator<Shipment>() {   // Java 9+: new Iterator<>()
              private int index = 0;
              private Shipment next = null;
              
              @Override
              public boolean hasNext() {
                  while (index < all.size()) {
                      Shipment s = all.get(index++);
                      if (s.getStatus() == ShipmentStatus.ACTIVE) {
                          next = s;
                          return true;
                      }
                  }
                  return false;
              }
              
              @Override
              public Shipment next() {
                  if (next == null) throw new NoSuchElementException();
                  Shipment result = next;
                  next = null;
                  return result;
              }
          };
      }
      
      // Callable, Runnable 등
      ExecutorService executor = Executors.newSingleThreadExecutor();
      Future<String> result = executor.submit(new Callable<String>() {
          @Override
          public String call() throws Exception {
              return doWork();
          }
      });
      
      // 람다로
      Future<String> result2 = executor.submit(this::doWork);

      7.3 로컬 제네릭 메서드 (Java 8+ 람다)

      // 람다로 제네릭 비슷한 효과
      public void process() {
          Function<Integer, Integer> doubleIt = n -> n * 2;
          Function<String, Integer> length = String::length;
          
          // 람다는 함수형 인터페이스의 인스턴스
          // 그 자체로 제네릭은 아님
      }
      
      // 로컬 제네릭 메서드는 불가
      public void process() {
          // ❌ 로컬 메서드의 제네릭 불가
          // public <T> T helper(T input) { ... }
          
          // ✓ 로컬 람다는 가능 (제네릭이 아닌 특정 타입)
          Function<String, Integer> helper = String::length;
      }

      → 자바엔 로컬 제네릭 메서드 없음.

      7.4 람다와 제네릭의 결합

      // 제네릭 메서드 + 람다 매개변수
      public static <T, R> List<R> map(List<T> list, Function<T, R> fn) {
          List<R> result = new ArrayList<>();
          for (T t : list) {
              result.add(fn.apply(t));
          }
          return result;
      }
      
      // 사용 — 람다 전달
      List<String> names = List.of("Alice", "Bob");
      List<Integer> lengths = map(names, String::length);
      
      // 또는
      List<Integer> lengths2 = map(names, s -> s.length());
      
      // 컴파일러 추론:
      // 1. map(List<String>, Function<String, Integer>)
      // 2. T = String, R = Integer
      // 3. 결과 List<Integer>

      7.5 익명 클래스 vs 람다

      // 익명 클래스
      Comparator<String> c1 = new Comparator<String>() {
          @Override
          public int compare(String a, String b) {
              return a.length() - b.length();
          }
      };
      
      // 람다
      Comparator<String> c2 = (a, b) -> a.length() - b.length();
      
      // 차이:
      // 1. 코드 길이 — 람다가 짧음
      // 2. this — 익명 클래스의 this 는 자기, 람다는 외부 this
      // 3. 직렬화 — 람다는 직렬화 가능 (특수 조건)
      // 4. 성능 — 비슷 (JIT 후엔 거의 동일)
      // 5. 표현력 — 람다는 SAM 인터페이스만, 익명은 일반 클래스 가능

      7.6 함수형 인터페이스의 제네릭

      @FunctionalInterface
      public interface Function<T, R> {
          R apply(T t);
      }
      
      // 다양한 활용
      Function<String, Integer> length = String::length;
      Function<Integer, String> toStr = String::valueOf;
      Function<Shipment, BigDecimal> toFare = Shipment::getFare;
      
      // 합성
      Function<String, String> combined = length.andThen(toStr);
      combined.apply("hello");   // "5"

      7.7 BiFunction, Predicate 등

      // BiFunction<T, U, R>
      BiFunction<Integer, Integer, Integer> add = Integer::sum;
      
      // Predicate<T>
      Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;
      Predicate<Shipment> isHeavy = s -> s.getWeight().compareTo(new BigDecimal("1000")) > 0;
      
      // 조합
      Predicate<Shipment> activeAndHeavy = isActive.and(isHeavy);
      
      // Consumer<T>
      Consumer<Shipment> printer = s -> System.out.println(s.getBlNo());
      
      // Supplier<T>
      Supplier<List<String>> listSupplier = ArrayList::new;

      7.8 자기 점검 답변

      익명 제네릭 클래스와 람다 중 어느 것을?

      답:

      • 람다 권장 (대부분):

        • 단일 메서드 (SAM) 만
        • 코드 간결
        • 의도 명확
      • 익명 클래스 사용:

        • SAM 아닌 인터페이스 (메서드 여러 개)
        • 상태 (필드) 필요
        • 추상클래스 익명 구현
        • this 가 자기 자신 필요

      → 가능하면 람다, 어려우면 익명 클래스.


      8️⃣ 자바 표준 라이브러리 정밀 분석

      8.1 Collections 의 메서드들

      public class Collections {
          
          // sort
          public static <T extends Comparable<? super T>> void sort(List<T> list) {
              list.sort(null);
          }
          
          public static <T> void sort(List<T> list, Comparator<? super T> c) {
              list.sort(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
          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) {
              // PECS!
          }
          
          // addAll
          public static <T> boolean addAll(
                  Collection<? super T> c, T... elements) {
              // ...
          }
          
          // emptyList
          public static <T> List<T> emptyList() {
              return (List<T>) EMPTY_LIST;
          }
          
          // unmodifiableList
          public static <T> List<T> unmodifiableList(List<? extends T> list) {
              return new UnmodifiableList<>(list);
          }
      }

      분석:

      • 모든 메서드가 PECS 패턴
      • extends for input (Producer)
      • super for output (Consumer)
      • Comparable<? super T> for 비교

      8.2 Stream 의 메서드들

      public interface Stream<T> extends BaseStream<T, Stream<T>> {
          
          // filter — Predicate
          Stream<T> filter(Predicate<? super T> predicate);
          
          // map — Function
          <R> Stream<R> map(Function<? super T, ? extends R> mapper);
          
          // flatMap — Function
          <R> Stream<R> flatMap(
              Function<? super T, ? extends Stream<? extends R>> mapper);
          
          // forEach — Consumer
          void forEach(Consumer<? super T> action);
          
          // 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 — Collector
          <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);
          
          // min, max
          Optional<T> min(Comparator<? super T> comparator);
          Optional<T> max(Comparator<? super T> comparator);
          
          // sorted
          Stream<T> sorted();
          Stream<T> sorted(Comparator<? super T> comparator);
          
          // 다양한 static 팩토리
          static <T> Stream<T> of(T t);
          static <T> Stream<T> of(T... values);
          static <T> Stream<T> empty();
          static <T> Stream<T> generate(Supplier<? extends T> s);
          static <T> Stream<T> iterate(T seed, UnaryOperator<T> f);
      }

      8.3 Optional 의 메서드들

      public final class Optional<T> {
          
          // 정적 팩토리
          public static <T> Optional<T> empty() { ... }
          public static <T> Optional<T> of(T value) { ... }
          public static <T> Optional<T> ofNullable(T value) { ... }
          
          // 인스턴스 메서드 — 클래스 T 활용
          public T get() { ... }
          public boolean isPresent() { ... }
          public T orElse(T other) { ... }
          public T orElseGet(Supplier<? extends T> supplier) { ... }
          
          // 메서드 자체의 U
          public <U> Optional<U> map(Function<? super T, ? extends U> mapper) { ... }
          public <U> Optional<U> flatMap(
              Function<? super T, ? extends Optional<? extends U>> mapper) { ... }
          
          public void ifPresent(Consumer<? super T> action) { ... }
          public Optional<T> filter(Predicate<? super T> predicate) { ... }
      }

      8.4 Function<T, R> 의 메서드들

      @FunctionalInterface
      public interface Function<T, R> {
          
          R apply(T t);
          
          // 합성 — 자기 자신과 다른 Function 결합
          default <V> Function<V, R> compose(
                  Function<? super V, ? extends T> before) {
              // V → T → R
              return (V v) -> apply(before.apply(v));
          }
          
          default <V> Function<T, V> andThen(
                  Function<? super R, ? extends V> after) {
              // T → R → V
              return (T t) -> after.apply(apply(t));
          }
          
          // 정적
          static <T> Function<T, T> identity() {
              return t -> t;
          }
      }

      분석:

      • compose: V → T → R (다른 Function 먼저)
      • andThen: T → R → V (자신 먼저, 다른 후)
      • ? super V, ? extends T 등 PECS 활용

      8.5 Comparator 의 메서드들

      public interface Comparator<T> {
          
          int compare(T o1, T o2);
          
          // 자기 자신 + 변형
          default Comparator<T> reversed() {
              return Collections.reverseOrder(this);
          }
          
          default Comparator<T> thenComparing(Comparator<? super T> other) {
              return (c1, c2) -> {
                  int res = compare(c1, c2);
                  return (res != 0) ? res : other.compare(c1, c2);
              };
          }
          
          default <U extends Comparable<? super U>> Comparator<T> thenComparing(
                  Function<? super T, ? extends U> keyExtractor) {
              return thenComparing(comparing(keyExtractor));
          }
          
          // 정적 팩토리
          static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
              return (c1, c2) -> c1.compareTo(c2);
          }
          
          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));
          }
      }

      8.6 Collectors 의 메서드들

      public final class Collectors {
          
          public static <T> Collector<T, ?, List<T>> toList() { ... }
          
          public static <T> Collector<T, ?, Set<T>> toSet() { ... }
          
          public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(
                  Function<? super T, ? extends K> classifier) { ... }
          
          public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(
                  Function<? super T, ? extends K> classifier,
                  Collector<? super T, A, D> downstream) { ... }
          
          public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
                  Function<? super T, ? extends K> keyMapper,
                  Function<? super T, ? extends U> valueMapper) { ... }
          
          public static <T> Collector<T, ?, Long> counting() { ... }
          
          public static <T> Collector<T, ?, Integer> summingInt(
                  ToIntFunction<? super T> mapper) { ... }
      }

      8.7 패턴 종합

      자바 표준의 제네릭 패턴:
      
      1. 클래스 T + 메서드 U (Optional, Stream)
         <U> Optional<U> map(Function<? super T, ? extends U>)
         <R> Stream<R> map(Function<? super T, ? extends R>)
      
      2. PECS 활용 (메서드)
         <T> void copy(List<? super T>, List<? extends T>)
         <T> void sort(List<T>, Comparator<? super T>)
      
      3. 재귀적 bound (Comparable)
         <T extends Comparable<? super T>>
      
      4. 정적 팩토리
         static <T> Optional<T> of(T value)
         static <T> Stream<T> of(T... values)
         static <T> List<T> of(T... values)
      
      5. 함수 합성
         default <V> Function<T, V> andThen(Function<? super R, ? extends V>)

      8.8 자기 점검 답변

      자바 표준 라이브러리의 제네릭 패턴 5가지는?

      답:
      1. 클래스 T + 메서드 U: Optional, Stream 의 map/flatMap
      2. PECS: Collections.copy, sort
      3. 재귀적 bound: Comparable<? super T>
      4. 정적 팩토리: Optional.of, Stream.of, List.of
      5. 함수 합성: Function.compose, andThen

      → 모두 타입 안전성 + 유연성 의 균형.


      9️⃣ 면접 + 자기 점검

      9.1 면접 단골 질문 매핑

      Q핵심 답변
      제네릭 클래스 vs 메서드?인스턴스 단위 vs 호출 단위
      제네릭 메서드 정의 위치?반환 타입 앞 <T>
      타입 추론 메커니즘?컨텍스트 분석 + 제약 수집 + LUB
      타깃 타입?표현식이 기대되는 타입
      제네릭 생성자?클래스 무관 메서드 매개변수
      정적 메서드에 클래스 T?불가, 자신의 T 필요
      var 와 다이아몬드?우변 vs 좌변에서 타입
      익명 vs 람다?SAM 만 람다, 일반은 익명
      Function.compose vs andThen?V→T→R vs T→R→V
      Collections.copy 시그니처?<T> + PECS

      9.2 자기 점검 체크리스트

      클래스 vs 메서드

      • 정의 시점 차이
      • 사용 시점 차이
      • T 스코프 차이
      • static 제약

      타입 추론

      • 추론 메커니즘 4단계
      • 타깃 타입의 활용
      • LUB 의 의미
      • 명시 vs 추론 시점

      생성자

      • 제네릭 생성자 문법
      • 변환 생성자 패턴
      • 자바 표준의 생성자 (ArrayList 등)

      결합

      • 클래스 T + 메서드 U
      • S extends T 패턴
      • 정적 팩토리
      • Self-referential (Builder)

      var

      • var 의 제약
      • var 와 다이아몬드 조합
      • var 활용 가이드

      자바 표준

      • Collections 패턴
      • Stream 패턴
      • Optional 패턴
      • Function/Comparator 패턴

      9.3 추가 심화 질문

      Q1: <T extends Comparable<T>> vs <T extends Comparable<? super T>> 차이는?

      답:

      • 첫 번째: T 만 비교 가능
      • 두 번째: T 또는 그 부모 비교 가능

      예:

      class Parent implements Comparable<Parent> { ... }
      class Child extends Parent { }   // Comparable<Parent> 상속
      
      // 첫 번째 — Child 는 Comparable<Child> 가 아니라 거부
      <T extends Comparable<T>> void sort(List<T> list) { ... }
      sort(childList);   // ❌
      
      // 두 번째 — Child 는 Comparable<? super Child> (Parent) 라 OK
      <T extends Comparable<? super T>> void sort(List<T> list) { ... }
      sort(childList);   // ✓

      → PECS 의 정밀 활용 (Unit 5.5).

      Q2: var 가 람다에 안 되는 이유는?

      답:

      • 람다는 타깃 타입 필요
      • var fn = (s) -> s.length() — fn 의 타입은?
      • Function? Predicate? 다른 SAM?
      • 모호함 → 거부

      Q3: 제네릭 메서드 호출 시 명시가 필수인 경우는?

      답:
      1. 인자 없는 메서드: Utils.<String>emptyList()
      2. 모호한 추론: process(Utils.<String>empty())
      3. null 인자: Utils.<String>method(null)

      Q4: new ArrayList<>(Collections.emptyList()) 동작?

      답:

      • Collections.emptyList() 의 추론 필요
      • new ArrayList<>() 의 다이아몬드와 매칭
      • 좌변 없으면 추론 어려움
      // ❌ 모호
      new ArrayList<>(Collections.emptyList()).add("hello");
      
      // ✓ 명시
      new ArrayList<String>(Collections.emptyList()).add("hello");
      
      // 또는
      List<String> list = new ArrayList<>(Collections.emptyList());
      list.add("hello");

      Q5: 제네릭 클래스의 정적 메서드에서 자기 T 사용 불가 이유?

      답:

      • 클래스의 T 는 인스턴스마다 다름
      • 정적 메서드는 인스턴스 없이 호출
      • 어떤 T 인지 모름 → 사용 불가

      해결:

      public class Box<T> {
          // ❌ 클래스 T 사용
          public static T staticMethod(T input) { return input; }
          
          // ✓ 자기 T 정의
          public static <U> U staticMethod(U input) { return input; }
      }

      🎯 핵심 요약 — 3줄 정리

      1. 제네릭 클래스 vs 메서드

      • 클래스: 인스턴스 단위 타입 (Box)
      • 메서드: 호출 단위 타입 ( T identity(T))
      • 결합 패턴: 클래스 T + 메서드 U

      2. 타입 추론

      • 타깃 타입 (좌변, 매개변수)
      • 컨텍스트 분석 + 제약 수집 + LUB
      • 다이아몬드, var, 람다 추론

      3. 자바 표준의 정밀 패턴

      • PECS: ? extends (Producer), ? super (Consumer)
      • 재귀 bound: Comparable<? super T>
      • 클래스 T + 메서드 U: Optional.map, Stream.map
      • 정적 팩토리: of, empty, identity

      📚 다음으로...

      Unit 5.5 — PECS 원칙 (★ 마스터 깊이)

      이번 Unit에서 제네릭의 정밀을 봤다면, 다음은 PECS 원칙의 완성.

      • Producer Extends, Consumer Super 의 깊은 의미
      • 입출력 분석으로 매개변수 결정
      • Comparable, Comparator 의 정밀 분석
      • Collections 모든 메서드의 PECS
      • 자체 라이브러리 설계 가이드
      • Phase 5 졸업

      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 완주

      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 — 제네릭과 와일드카드 (4/5 진행)
      
      총: 21/43 Unit 작성 (약 49%)
profile
Software Developer

0개의 댓글