**제네릭(Generics)**은 컴파일 타임에 타입을 파라미터화해 타입 안전성을 보장하는 기법으로, 컬렉션(List<T>)과 java-functional-interface|함수형 인터페이스]의 기반이 됩니다.

기본 문법

// 제네릭 클래스
public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}
 
// 제네릭 메서드 — 반환 타입 앞에 <T> 선언
public static <T> void swap(T[] arr, int i, int j) {
    T tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}
 
// 생성 시 diamond 연산자로 타입 생략 (Java 7+)
Map<String, List<Integer>> map = new HashMap<>();

타입 파라미터 컨벤션: T(Type), E(Element), K/V(Key/Value), R(Result), U·V(두 번째·세 번째 타입).

bounded type parameter (타입 한정)

// 상한 한정 — Comparable을 구현한 타입만 허용
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;
}
 
max(Arrays.asList(3, 1, 2));        // Integer는 Comparable 구현 → OK
// max(Arrays.asList(new Object())); // ❌ Comparable 미구현
 
// 다중 한정 — 클래스 1개 + 인터페이스 여러 개 (&로 연결)
<T extends Number & Comparable<T> & Serializable>

extends만 가능하고 super 한정은 타입 파라미터에 사용할 수 없습니다(와일드카드에서만).

와일드카드와 PECS

표기이름읽기쓰기
List<?>비한정 와일드카드Object로만불가
List<? extends Number>상한 와일드카드 (공변)Number로불가
List<? super Integer>하한 와일드카드 (반공변)Object로Integer 가능

PECS (Producer Extends, Consumer Super) — 값이 들어오면(생산하면) extends, 값이 나가면(소비하면) super:

// src에서 값을 생산 → extends / dst가 값을 소비 → super
public static <T> void copy(List<? extends T> src, List<? super T> dst) {
    for (T item : src) dst.add(item);
}
 
// 실전 예: Integer 목록을 Number 목록에 복사
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(ints, nums);

와일드카드를 쓰지 않고 List<Number>에 List<Integer>를 대입할 수는 없습니다 — 제네릭은 **불공변(invariant)**이라 List<Integer>는 List<Number>의 하위 타입이 아니기 때문입니다. 배열은 공변이라 런타임에 ArrayStoreException이 날 수 있지만, 리스트는 컴파일 타임에 막힙니다.

타입 소거 (Type Erasure)

제네릭 타입 정보는 컴파일 타임에만 존재하고, 컴파일 후 바이트코드에서는 소거됩니다(한정 없으면 Object, 한정이 있으면 그 상한 타입으로 교체). 이것이 제네릭의 여러 제약의 원인입니다.

제약이유
new T() 불가런타임에 T가 뭔지 알 수 없음
new T[] 불가 (제네릭 배열 생성 금지)배열은 런타임 타입 검사를 하는데 T 정보가 없음
instanceof List<String> 불가런타임에 소거됨 (List 자체는 가능)
static 필드/메서드에 타입 파라미터 사용 불가인스턴스별 타입인데 static은 공유
타입 파라미터에 primitive 불가 (List<int> ❌)소거 시 Object로 바뀌므로 — Integer 사용
같은 소거 형태의 오버로드 불가 (f(List<String>) vs f(List<Integer>))바이트코드 시그니처가 동일
// ❌ 제네릭 배열 생성
// List<String>[] arrays = new List<String>[10];
 
// ✅ 우회: raw 타입 배열 + 억지 캐스팅 (비검사 경고)
@SuppressWarnings("unchecked")
List<String>[] arrays = new List[10];

런타임에 타입 정보가 필요할 때

// 1. Class 객체를 함께 전달
public static <T> T newInstance(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}
 
// 2. Supplier로 팩토리 전달 (함수형 인터페이스 활용)
public static <T> List<T> fill(int n, Supplier<T> maker) {
    List<T> result = new ArrayList<>();
    for (int i = 0; i < n; i++) result.add(maker.get());
    return result;
}
 
List<StringBuilder> builders = fill(3, StringBuilder::new);

주의사항

// ❌ raw 타입 사용 — 타입 안전성 상실
List list = new ArrayList();
list.add("hello");
list.add(42);
 
// ✅ 제네릭 명시 또는 <?> (모르는 타입은 비한정 와일드카드)
List<String> names = new ArrayList<>();
List<?> unknown = names;
 
// ❌ 힙 오염 — varargs에 제네릭을 섞으면 컴파일러 경고
// @SafeVarargs + final/static 메서드에서만 경고 억제 가능
@SafeVarargs
static <T> void safeVarargs(List<T>... lists) { }
 
// ❌ 매개변수에 매번 제네릭 타입을 강제하는 API
void process(Function<String, String> f) { }
 
// ✅ 필요 범위만 제네릭화 — 와일드카드로 호출자 유연성 확보
void processAll(Function<? super String, ? extends CharSequence> f) { }

관련 페이지