제네릭
컴파일 타임에 타입을 파라미터로 받아 타입 안전성을 보장하는 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 컴파일 에러
무공변은 타입 안전성을 보장하지만 유연성이 떨어지기 때문에, 와일드카드로 공변과 반공변을 지원한다.
- 공변(Covariant):
<? extends T>—S가T의 하위 타입이면List<S>를List<? extends T>자리에 쓸 수 있다. 값을 꺼내는 읽기 전용(생산자)이며, 쓰기는null만 가능하다. - 반공변(Contravariant):
<? super T>—T의 상위 타입까지 허용해 값을 넣을 수 있는 쓰기 전용(소비자)이며, 읽으면Object로만 꺼낼 수 있다.
<?>(비한정 와일드카드)와 <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는 인스턴스 레벨)
}
핵심 정리
-
컴파일 타임 타입 안전성 보장 —
ClassCastException을 런타임이 아닌 컴파일에서 잡음 -
바운디드 타입으로 타입 범위 제한, 와일드카드로 유연한 API 설계
-
타입 소거로 런타임에 제네릭 정보는 존재하지 않음 — instanceof, 배열 생성 불가