Chapter 2: 타입 시스템의 깊이 - 제네릭 소거와 PECS
Type Erasure, Recursive Type Bounds, PECS, Strategy Enum
1. 제네릭의 실체: 소거(Erasure)
면접 단골 질문: "자바의 제네릭은 런타임에 어떻게 존재하나요?"
C++의 템플릿과 달리, 자바의 제네릭은 컴파일 시점에만 존재하고 런타임에는 사라집니다(Erasure).
- Compile Time:
List<String>→ 타입 체크 수행. - Runtime:
List(Raw Type) → 모든T는Object로 치환됨. (명시적 캐스팅 코드가 자동 삽입됨)
이 때문에 new T(), instanceof T 같은 코드가 불가능한 것입니다. 런타임에는 T가 뭔지 모르니까요.
2. 한정적 와일드카드와 PECS 원칙
Effective Java Item 31: 한정적 와일드카드를 써서 API 유연성을 높이라
제네릭은 기본적으로 **불공변(Invariant)**입니다. List<Object>에 List<String>을 넣을 수 없다는 뜻입니다. 이를 해결하기 위해 와일드카드를 씁니다.
공식: PECS (Producer-Extends, Consumer-Super)
- Producer (생산자) -> extends: 컬렉션에서 꺼내서 읽기만 할 때. (
read-only) - Consumer (소비자) -> super: 컬렉션에 무언가를 집어넣을 때. (
write-only)
java
1 // Producer: src에서 꺼내서(Produce) 쓰니까 extends2 // Consumer: dst에 집어넣어(Consume) 소비하니까 super3 public void copy(List<? extends Number> src, List<? super Number> dst) {4 for (Number n : src) {5 dst.add(n);6 }7 }
3. 재귀적 타입 한정 (Recursive Type Bound)
"나 자신과 비교할 수 있는 타입만 허용한다"
Comparable 인터페이스를 구현할 때 자주 보게 되는 패턴입니다.
java
1 // T는 자신(T)과 비교 가능한(Comparable) 애여야 한다.2 public static <T extends Comparable<T>> T max(List<T> list) { ... }
이 패턴은 Object가 아닌, 비교 능력을 갖춘 타입들끼리만 정렬 등을 수행하도록 강제합니다.
4. Enum: 단순 상수가 아닌 '싱글톤 객체'
자바의 Enum은 본질적으로 final 클래스이며, 각 상수는 public static final 필드인 싱글톤 객체입니다.
따라서, 전략 패턴(Strategy Pattern)을 Enum 하나로 구현할 수 있습니다.
핵심 요약
- 1자바의 제네릭은 컴파일 타임에만 존재하며, 런타임에는 Object로 소거(Erasure)됩니다. (C++ 템플릿과의 차이점)
- 2API 유연성을 위해 PECS 원칙(Producer-extends, Consumer-super)을 반드시 지켜야 합니다. (Item 31)
- 3Enum은 상수별로 추상 메서드를 구현(Constant-specific method implementation)하여 다형성을 가질 수 있습니다.
실전 코드 예제
| 1 | public enum Operation { |
| 2 | PLUS("+") { |
| 3 | public double apply(double x, double y) { return x + y; } |
| 4 | }, |
| 5 | MINUS("-") { |
| 6 | public double apply(double x, double y) { return x - y; } |
| 7 | }, |
| 8 | TIMES("*") { |
| 9 | public double apply(double x, double y) { return x * y; } |
| 10 | }, |
| 11 | DIVIDE("/") { |
| 12 | public double apply(double x, double y) { return x / y; } |
| 13 | }; |
| 14 | |
| 15 | private final String symbol; |
| 16 | |
| 17 | Operation(String symbol) { this.symbol = symbol; } |
| 18 | |
| 19 | // 추상 메서드 선언 -> 각 상수가 이를 반드시 구현해야 함(Compiler Check) |
| 20 | public abstract double apply(double x, double y); |
| 21 | } |
Tech Interview Challenge
현업 면접관이 실제로 던지는 질문입니다. 먼저 스스로 답해보세요.
HardList<Object>와 List<?>의 차이는 무엇인가요?
List<Object>와 List<?>의 차이는 무엇인가요?
List<Object>는 Object 타입만 넣을 수 있는 '구체적인 리스트'이고, List<?>는 '어떤 타입인지 모르는 리스트'입니다. List<?>에는 null 외에는 어떤 값도 넣을 수 없습니다(타입이 뭔지 모르니 안전하지 않아서). 반면 읽기는 가능합니다(Object로 읽힘).
Hard제네릭 소거(Erasure)로 인해 발생하는 제약 사항은?
제네릭 소거(Erasure)로 인해 발생하는 제약 사항은?
런타임에 타입 정보가 없으므로 new T()로 인스턴스를 생성하거나, T[] 배열을 생성할 수 없습니다. 또한 static 변수에는 제네릭 타입 파라미터 T를 사용할 수 없습니다(모든 인스턴스가 공유하므로).
