제네릭

컴파일 타임에 타입을 파라미터로 받아 타입 안전성을 보장하는 Java 기능입니다 (Generics).

왜 필요한가

// 제네릭 없이
List list = new ArrayList();
list.add("hello");
list.add(123);

String s = (String) list.get(1);  // X ClassCastException (런타임)

// 제네릭 사용
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123);   // X 컴파일 에러 — 런타임 전에 잡힘

String s = list.get(0);  // 캐스팅 불필요

기본 문법

// 제네릭 클래스
class Box<T> {
    private T value;

    Box(T value) { this.value = value; }
    T get()      { return value; }
}

Box<String>  sBox = new Box<>("hello");
Box<Integer> iBox = new Box<>(42);

// 제네릭 메서드
class Utils {
    static <T> T firstOrDefault(List<T> list, T defaultValue) {
        return list.isEmpty() ? defaultValue : list.get(0);
    }
}

바운디드 타입 (타입 제한)

// T는 Number 또는 그 하위 타입만 허용
class NumericBox<T extends Number> {
    T value;
    double doubleValue() { return value.doubleValue(); }
}

NumericBox<Integer> ib = new NumericBox<>();   // O
NumericBox<String>  sb = new NumericBox<>();   // X 컴파일 에러

// 여러 타입 제한
<T extends Comparable<T> & Serializable>

와일드카드 ?

// 비한정 와일드카드: 어떤 타입이든
void printList(List<?> list) {
    list.forEach(System.out::println);
    // list.add("hello");  // X 불가 (타입 알 수 없음)
}

// 상한 경계 (extends): T 또는 T의 하위 타입 읽기 전용
void sumNumbers(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) sum += n.doubleValue();  // Number로 읽기 가능
}

// 하한 경계 (super): T 또는 T의 상위 타입 쓰기 가능
void addNumbers(List<? super Integer> list) {
    list.add(1);    // Integer 넣기 가능
    list.add(2);
}
PECS 원칙 (Effective Java):
  Producer → extends (데이터를 꺼낼 때)
  Consumer → super   (데이터를 넣을 때)

타입 변성 (공변·반공변·무공변)

제네릭은 기본적으로 무공변(Invariant) 입니다. S가 T의 하위 타입이어도 List<S>와 List<T>는 아무 관계가 없어, 타입이 정확히 일치하지 않으면 컴파일 에러가 발생합니다.

List<Animal> animals = new ArrayList<Cat>();  // X 컴파일 에러
List<Cat> cats = new ArrayList<Animal>();     // X 컴파일 에러

무공변은 타입 안전성을 보장하지만 유연성이 떨어지기 때문에, 와일드카드로 공변과 반공변을 지원한다.

<?>(비한정 와일드카드)와 <Object>는 둘 다 넓은 타입을 받는 것처럼 보이지만 다르다. <?>는 모든 타입을 인자로 받되 null 외에는 추가할 수 없는 읽기 전용이고, <Object>는 정확히 Object 타입만 인자로 받되 모든 객체를 자유롭게 추가할 수 있다.

타입 소거 (Type Erasure)

컴파일 후 제네릭 타입 정보가 제거됨 — 런타임에는 타입 파라미터가 없음.

List<String>  a = new ArrayList<>();
List<Integer> b = new ArrayList<>();

a.getClass() == b.getClass()  // true — 런타임에 모두 List

// 런타임에 제네릭 타입 확인 불가
if (list instanceof List<String>) {}  // X 컴파일 에러

타입 소거 이유: 제네릭 도입 전 코드(로우 타입)와의 하위 호환성 유지.

제네릭 사용 시 주의

// 배열과 제네릭 혼용 불가
List<String>[] arr = new List<String>[10];  // X 컴파일 에러

// instanceof 불가 (타입 소거)
if (obj instanceof T) {}   // X

// static 컨텍스트에서 타입 파라미터 사용 불가
class Box<T> {
    static T value;  // X (T는 인스턴스 레벨)
}

핵심 정리

→ 인터페이스 | 컬렉션 프레임워크

CSLOG에서 보기