
제네릭은 데이터 타입을 일반화해서, 컴파일 시점에 타입을 체크할 수 있도록 해주는 문법입니다. 타입 안정성을 확보하고, 형변환 없이 다양한 타입에 재사용할 수 있도록 설계되었습니다.
제네릭이 없던 시절에는 컬렉션에 여러 타입을 섞어 담을 수 있었고, 값을 꺼낼 때마다 개발자가 직접 타입 캐스팅을 해야 했습니다. 하지만 잘못된 캐스팅이 발생하면 런타임 에러로 이어지는 문제가 있었습니다.
List list = new ArrayList();
list.add("string");
list.add(100); // 타입 혼합 가능
String str = (String) list.get(0); // 형변환 필요
제네릭 도입 이후에는 컴파일 시점에 타입 검사를 수행하여 잘못된 타입 삽입 자체를 막을 수 있고, 타입 캐스팅 없이 안전하게 값을 사용할 수 있게 되었습니다.
List<String> list = new ArrayList();
list.add("string");
list.add(100); // 컴파일 에러 발생
String str = list.get(0); // 형변환 불필요
와일드카드는 제네릭 타입을 더 유연하게 사용할 수 있도록 도와주는 문법입니다.
<?>는 모든 타입을 허용함을 의미합니다.
public void print(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
<? extends T>는 T 또는 T의 자식 클래스만 허용함을 의미합니다
public void printAnimals(List<? extends Animal> list) {
for (Animal animal : list) {
System.out.println(animal);
}
}
// 아래 모두 가능
printAnimals(new ArrayList<Dog>());
printAnimals(new ArrayList<Cat>());
printAnimals(new ArrayList<Animal>());
<? super T>는 T 또는 T의 부모 클래스만 허용함을 의미합니다.
public void saveDogs(List<? super Dog> list) {
list.add(new Dog());
list.add(new SubDog());
}
// 아래 모두 가능
saveDogs(new ArrayList<Dog>());
saveDogs(new ArrayList<Animal>());
saveDogs(new ArrayList<Object>());
PECS 원칙은 자바 제네릭에서 데이터를 조회(Producer)할 때는 extends, 적재(Consumer)할 때는 super를 사용하는 설계 원칙입니다.
자바 제네렉의 설계 철학은 타입 안정성입니다. 특히, List와 같이 읽기(get)와 쓰기(add)가 동시에 가능한 자료구조에서는 읽기와 쓰기 목적이 다를 때 발생할 수 있는 타입 문제를 컴파일 단계에서 방지하기 위해 extends와 super를 나눠서 사용합니다.
// 아래의 경우 Animal의 하위 클래스인 List<Dog>, List<Cat> 등 모두 들어올 수 있습니다.
// 만약, 들어온 리스트가 List<Cat>이라면 아래 코드의 경우 문제가 발생할 수 있습니다.
public void addAnimal(List<? extends Animal> list) {
list.add(new Dog());
}
// 아래의 경우 Dog의 상위 클래스인 List<Object>, List<Animal> 등 모두 들어올 수 있습니다.
// 만약, List<Animal>이 들어올 경우 아래 코드의 경우 컴파일 에러가 발생할 수 있습니다.
public void printAnimals(List<? super Dog> list) {
Dog dog = list.get(0);
}
위 예시처럼 컴파일 단계에서 타입 문제를 사전에 방지할 수 있으므로, PECS 설계 원칙에 따라 제네릭을 사용합니다.