List<String>List<Object>의 하위 타입이 아니라는 거, 공부하다 보니 처음에 정말 헷갈렸습니다. 제네릭의 더 깊은 곳을 파보겠습니다.

이게 뭔가요?

자바 제네릭의 기본(와일드카드, PECS)을 넘어서, 타입 시스템의 내부 동작과 고급 패턴을 다루는 심화 주제입니다. 라이브러리 설계나 프레임워크 코드를 읽을 때 반드시 만나게 됩니다.

재귀 타입 바운드 (Recursive Type Bound)

Comparable<T>를 보면 자기 자신을 타입 파라미터로 사용합니다.

JAVA
// T는 자기 자신과 비교 가능한 타입이어야 함
public static <T extends Comparable<T>> T max(List<T> list) {
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) result = item;
    }
    return result;
}

빌더 패턴에서도 사용됩니다:

JAVA
// 상속 가능한 빌더
public abstract class Builder<T extends Builder<T>> {
    private String name;

    @SuppressWarnings("unchecked")
    protected T self() { return (T) this; }

    public T name(String name) {
        this.name = name;
        return self(); // 하위 빌더 타입 반환
    }
}

public class UserBuilder extends Builder<UserBuilder> {
    private int age;

    public UserBuilder age(int age) {
        this.age = age;
        return this;
    }
}
// new UserBuilder().name("홍길동").age(25) — 체이닝이 끊기지 않음

타입 토큰 (Type Token)

제네릭 타입 정보는 런타임에 소거되지만, 클래스 리터럴을 "타입 토큰"으로 활용할 수 있습니다.

JAVA
// 타입 안전한 이종 컨테이너 (Effective Java Item 33)
public class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();

    public <T> void put(Class<T> type, T value) {
        map.put(type, value);
    }

    public <T> T get(Class<T> type) {
        return type.cast(map.get(type)); // 타입 안전한 캐스팅
    }
}
JAVA
TypeSafeMap favorites = new TypeSafeMap();
favorites.put(String.class, "자바");
favorites.put(Integer.class, 42);

String s = favorites.get(String.class);  // 캐스팅 불필요
Integer i = favorites.get(Integer.class);

슈퍼 타입 토큰 (Super Type Token)

Class<T>List<String>.class 같은 매개변수화 타입을 표현할 수 없습니다. 이를 해결하는 패턴입니다.

JAVA
// 익명 클래스로 제네릭 타입 정보 보존
public abstract class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
        Type superClass = getClass().getGenericSuperclass();
        this.type = ((ParameterizedType) superClass)
            .getActualTypeArguments()[0];
    }

    public Type getType() { return type; }
}
JAVA
// 사용 — 익명 클래스의 수퍼타입에 제네릭 정보가 남음
TypeReference<List<String>> ref = new TypeReference<>() {};
ref.getType(); // java.util.List<java.lang.String>

Jackson의 TypeReference, Spring의 ParameterizedTypeReference가 이 패턴을 사용합니다.

제네릭 배열은 왜 만들 수 없나?

JAVA
// 컴파일 에러: 제네릭 배열 생성 불가
List<String>[] array = new List<String>[10];  // 불가

만약 허용된다면:

JAVA
Object[] objArray = array;          // 배열은 공변이므로 허용
objArray[0] = List.of(42);          // Integer 리스트 삽입 (런타임에 체크 불가)
String s = array[0].get(0);         // ClassCastException!

배열은 공변(covariant)이고 런타임에 타입을 체크하지만, 제네릭은 불변(invariant)이고 컴파일 타임에만 체크합니다. 이 불일치 때문에 제네릭 배열 생성을 금지합니다.

교차 타입 (Intersection Type)

하나의 타입 파라미터에 여러 제약을 걸 수 있습니다.

JAVA
// T는 Serializable이면서 Comparable이어야 함
public <T extends Serializable & Comparable<T>> void process(T item) {
    // Serializable 메서드도, Comparable 메서드도 사용 가능
}

람다에서 교차 타입 캐스팅:

JAVA
// Serializable한 Runnable 만들기
Runnable r = (Runnable & Serializable) () -> System.out.println("hi");

타입 소거의 함정

JAVA
// 컴파일 에러: 같은 소거 타입
public void process(List<String> list) { }   // 소거 → process(List)
public void process(List<Integer> list) { }  // 소거 → process(List) — 충돌!
JAVA
// instanceof에서 제네릭 사용 불가
if (obj instanceof List<String>) { }   // 컴파일 에러
if (obj instanceof List<?>) { }        // 가능 (와일드카드)

힙 오염 (Heap Pollution)

가변 인자(varargs)와 제네릭을 함께 쓸 때 발생합니다.

JAVA
@SafeVarargs  // 안전하다고 보장하는 어노테이션
public static <T> List<T> listOf(T... elements) {
    return List.of(elements);
}

@SafeVarargs는 "이 메서드는 가변 인자 배열에 타입 안전하지 않은 작업을 하지 않는다"는 의미입니다. 배열에 값을 쓰지 않고 읽기만 한다면 안전합니다.

자주 헷갈리는 포인트

  • List<String>List<Object>의 하위 타입이 아닙니다: 제네릭은 불변입니다. 하지만 List<String>List<? extends Object>의 하위 타입입니다.
  • Class<T> vs Type: Class<T>는 구체적인 클래스만 표현합니다. List<String> 같은 매개변수화 타입은 ParameterizedType(Type의 하위 인터페이스)으로 표현합니다.
  • 브릿지 메서드: 제네릭을 상속하면 컴파일러가 브릿지 메서드를 자동 생성합니다. 리플렉션에서 예상치 못한 메서드가 보일 수 있습니다.

정리

항목설명
재귀 타입 바운드T extends Comparable<T> — 자기 참조 타입 제약
타입 토큰Class<T>로 런타임 타입 정보 보존
슈퍼 타입 토큰익명 클래스로 매개변수화 타입 정보 보존
제네릭 배열 금지배열 공변성과 타입 소거 불일치
교차 타입T extends A & B — 다중 제약
힙 오염varargs + 제네릭 시 @SafeVarargs

References

댓글 로딩 중...