Java컬렉션과 제네릭

제네릭 와일드카드와 bounded type

`<T extends Comparable<T>>`로 타입에 제약을 거는 방법, `? extends`와 `? super`가 어떤 문제를 해결하는지, 타입 소거가 런타임에 미치는 영향을 정리합니다.

마지막 수정 2026년 3월 27일

기본 패턴

java
// 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가 특정 클래스나 인터페이스를 구현함을 컴파일러에 알려서 해당 메서드를 사용할 수 있습니다.

여러 제약을 &로 연결할 수 있습니다.

java
// 제약 없는 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를 씁니다.

java
// ? 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), TObject(또는 bound가 있으면 bound 타입)로 대체됩니다. 이는 Java 1.5 이전 코드와의 하위 호환성을 위한 설계입니다.

C#의 제네릭은 런타임에도 타입 정보가 보존되지만, Java는 그렇지 않습니다.

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를 사용하세요.

java
// ❌ 제네릭 배열 생성 불가 — 컴파일 오류
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