자바에서 제네릭이란 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다.
List<String> list = new ArrayList<>();
우리가 흔히 볼 수 있는 ArrayList 선언 시에 사용되는 <>이 바로 제네릭이다. 이 괄호 안에 타입명을 기재하면, 리스트 클래스 자료형의 타입은 지정한 타입의 데이터만 리스트에 적재할 수 있는 것이다.
이처럼 제네릭은 배열의 타입을 지정하듯이 리스트 자료형 같은 컬렉션 클래스나 메서드에서 사용할 내부 데이터 타입을 파라미터로 주듯이, 외부에서 지정하는 타입을 변수화한 기능이라고 이해하면 된다. 즉, 객체(Object)에 타입을 지정해주는 것이다.
<String>과 같이 타입을 명시하면, 컴파일러가 잘못된 타입이 들어오는 것을 사전에 차단 가능제네릭 사용 전
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 반드시 (String)으로 캐스팅 필요
제네릭 사용 후
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 캐스팅 불필요. 코드가 간결해짐
ArrayList<T> 하나로 ArrayList<String>, ArrayList<Integer> 등을 모두 처리할 수 있음컴파일 시점은 작성한 코드 .java가 기계어 .class로 변환되는 과정이다. 문법 오류와 타입 체크, 클래스 참조를 확인하며, 제네릭에 타입 정보가 존재한다.
런타임 시점은 컴파일된 프로그램이 메모리에 적재되어 실제로 실행되는 상태이다. NullPointerException, 메모리 부족(OOM), 로직 오류 등을 체크할 수 있으며, 타입 정보가 소거되어 Object로 변경된다.
자바 컴파일러는 제네릭을 사용하여 타입을 검사한 후, 런타임에는 이전 버전과의 호환성을 위해 제네릭 타입을 제거한다. 이러한 이유는 자바 5 이전 버전으로 작성된 코드와의 하위 호환성을 유지하기 위해서이며, 이것을 타입 소거(Type Erasure)라고 한다. 따라서 제네릭은 런타임 시점에는 체크가 불가능한 것이다.
PECS는 Producer-Extends, Consumer-Super의 약자로, 제네릭의 와일드카드 ?를 사용할 때 언제 extends를 쓰고 언제 super를 써야 하는지 결정하는 공식이다. 이 원칙은 컬렉션 입장에서 데이터를 제공하는지, 데이터를 소비하는지에 따라 결정된다.
<? extends T>)List<? extends Number>는 Number 혹은 그 하위 타입(Integer, Double 등)을 가지고 있음이 보장됨 -> 따라서 꺼낸 데이터는 안전하게 Number 타입으로 읽을 수 있음<? super T>)List<? super Integer>는 Integer 혹은 그 상위 타입(Number, Object)을 담는 리스트임이 보장됨 -> 따라서 Integer는 안전하게 담을 수 있음💡 따라서 데이터를 읽기만 할 거면
extends, 넣기만 할 거면super, 둘 다 해야 하면 와일드카드를 쓰지 않는게 좋다.
제네릭은 기본적으로 불공변이다. 즉, String이 Object의 자식이라도 List<String>은 List<Object>의 자식이 아니다.
이러한 제약으로 유연성이 떨어질 때, 이를 해결하기 위해 사용하는 것이 와일드카드(?)이다. 와일드카드는 알 수 없는 타입을 의미한다.
| 종류 | 표기법 | 설명 | 주 용도 |
|---|---|---|---|
| Unbounded (비한정) | <?> | 모든 타입을 허용<? extends Object>와 유사 | 타입에 의존하지 않는 메서드를 작성할 때 예) size(), clear() |
| Upper Bounded (상한 경계) | <? extends T> | T와 T의 자식 타입만 허용 | Producer(PECS) : 데이터를 안전하게 읽을 때 |
| Lower Bounded (하한 경계) | <? super T> | T와 T의 부모 타입만 허용 | Consumer(PECS) : 데이터를 안전하게 쓸 때 |
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
// 1. Upper Bounded (<? extends Fruit>)
// Fruit의 자식들은 다 올 수 있음 (과일 바구니)
// 꺼내면 최소한 Fruit임은 보장
void printFruits(List<? extends Fruit> basket) {
for (Fruit f : basket) { // 읽기 가능 (OK)
System.out.println(f);
}
// basket.add(new Apple()); // 쓰기 불가 (Error! 이 바구니가 List<Banana>일 수도 있음)
}
// 2. Lower Bounded (<? super Fruit>)
// Fruit의 부모들은 다 올 수 있음 (과일이 들어가는 상자)
// Fruit은 안전하게 담을 수 있음
void addApple(List<? super Fruit> basket) {
basket.add(new Apple()); // 쓰기 가능 (OK)
basket.add(new Fruit()); // 쓰기 가능 (OK)
// Fruit f = basket.get(0); // 읽기 불가 (Error! Object로만 읽힘)
}