기본 패턴
// bounded type — T에 제약 추가
public static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
// wildcard — 읽기 전용 컬렉션 수용
public static double sum(List<? extends Number> numbers) {
return numbers.stream().mapToDouble(Number::doubleValue).sum();
}
max(List.of(3, 1, 4)); // T = Integer
sum(List.of(1, 2.5, 3L)); // Integer, Double, Long 모두 수용설명
<T extends Bound> — 타입에 기능을 요구하는 방법
비제약 제네릭(<T>)에서는 T가 어떤 타입인지 모르므로 T의 메서드를 호출할 수 없습니다. extends 제약을 붙이면 T가 특정 클래스나 인터페이스를 구현함을 컴파일러에 알려서 해당 메서드를 사용할 수 있습니다.
여러 제약을 &로 연결할 수 있습니다.
// 제약 없는 T — compareTo() 호출 불가
<T> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b; // 컴파일 오류 — T에 compareTo 없음
}
// T extends Comparable<T> — compareTo() 사용 가능
<T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b; // ✅
}
// 복수 제약 — Serializable이면서 Comparable인 타입만
<T extends Comparable<T> & Serializable> void store(T value) { ... }? extends vs ? super — PECS 원칙
? extends T(upper bounded wildcard)는 T의 서브타입을 허용합니다. 컬렉션에서 읽기만 할 때 씁니다. ? super T(lower bounded wildcard)는 T의 수퍼타입을 허용합니다. 컬렉션에 쓰기만 할 때 씁니다.
이 패턴을 **PECS(Producer Extends, Consumer Super)**라고 부릅니다. 컬렉션이 데이터를 생산(읽어줌)하면 extends, 소비(받아씀)하면 super를 씁니다.
// ? extends — 읽기 전용 (Producer)
// List<Integer>도, List<Double>도 받을 수 있음
void print(List<? extends Number> list) {
for (Number n : list) System.out.println(n); // 읽기 ✅
// list.add(1); // 쓰기 불가 — 컴파일 오류
}
// ? super — 쓰기 전용 (Consumer)
// List<Number>도, List<Object>도 받을 수 있음
void addNumbers(List<? super Integer> list) {
list.add(1); // Integer는 Number의 서브타입이므로 ✅
list.add(2);
// Integer n = list.get(0); // 읽기 불가 — Object로만 꺼낼 수 있음
}
// Collections.copy 시그니처에서 확인:
// static <T> void copy(List<? super T> dest, List<? extends T> src)
// Consumer Producer타입 소거 — 런타임에 제네릭 정보가 없는 이유
Java 제네릭은 컴파일 타임에만 타입을 검사합니다. 컴파일 후 바이트코드에는 타입 매개변수 정보가 지워지고(type erasure), T는 Object(또는 bound가 있으면 bound 타입)로 대체됩니다. 이는 Java 1.5 이전 코드와의 하위 호환성을 위한 설계입니다.
C#의 제네릭은 런타임에도 타입 정보가 보존되지만, Java는 그렇지 않습니다.
// 컴파일 전
List<String> strings = new ArrayList<>();
strings.add("hello");
String s = strings.get(0);
// 바이트코드 (타입 소거 후)
List strings = new ArrayList();
strings.add("hello");
String s = (String) strings.get(0); // 컴파일러가 cast 삽입
// 런타임에 타입 정보 없음 — instanceof 불가
List<String> list = new ArrayList<>();
System.out.println(list instanceof List<String>); // 컴파일 오류
System.out.println(list instanceof List<?>); // ✅ — wildcard는 가능
System.out.println(list instanceof List); // ✅ — raw type은 가능빠른 정리
| 상황 | 선택 |
|---|---|
| T에 특정 기능 요구 (정렬, 비교) | <T extends Comparable<T>> |
| 읽기 전용 컬렉션, 다양한 서브타입 수용 | ? extends T |
| 쓰기 전용 컬렉션, 다양한 수퍼타입 수용 | ? super T |
| 타입 무관하게 모든 제네릭 수용 | <?> |
| 런타임 타입 검사 | raw type 또는 <?> |
주의할 점
제네릭 배열은 만들 수 없습니다. 타입 소거로 인해 런타임에 배열 타입 검사가 불가능하기 때문입니다. 배열 대신 List를 사용하세요.
// ❌ 제네릭 배열 생성 불가 — 컴파일 오류
T[] arr = new T[10];
List<String>[] arr2 = new ArrayList<String>[10];
// ✅ List 사용
List<T> list = new ArrayList<>();
// ✅ 불가피하면 Object 배열 후 cast (unchecked 경고 발생)
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];참고 링크
3 sources