
제네릭은 타입을 클래스 설계 시점이 아니라 사용 시점에 결정하여, 컴파일 타임에 타입 안전성을 보장하는 문법이다.
제네릭이 등장하기 전, 자바는 모든 객체를 Object로 다뤘다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("hello");
list.add(10); // 컴파일 에러 없음
String s = (String) list.get(1); // 런타임 에러
}
}
→ 에러는 반드시 컴파일 타임에 잡는 것이 최선이다.
class Box<T> {
T value;
}
T는 타입 파라미터(Type Parameter)다.
실제 타입은 객체 생성 시 결정된다.
public class Main {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.value = "hello";
// box.value = 10; // 컴파일 에러
}
}
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
사용:
Box<Integer> box = new Box<>();
box.set(100);
int num = box.get(); // 형변환 없음
형변환이 사라지고 타입 안전성이 확보된다.
Box<int> box = new Box<>(); // 컴파일 에러
제네릭은 참조형만 가능하다.
Box<Integer> box = new Box<>();
| 기본형 | 래퍼 클래스 |
|---|---|
| int | Integer |
| double | Double |
| char | Character |
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
}
사용:
Pair<String, Integer> p = new Pair<>("age", 20);
class Box<T extends Number> {
T value;
public double toDouble() {
return value.doubleValue();
}
}
사용:
Box<Integer> box = new Box<>();
box.value = 10;
System.out.println(box.toDouble());
extends는 클래스와 인터페이스 모두에 사용된다.
class Util {
public static <T> void print(T value) {
System.out.println(value);
}
}
사용:
Util.print("hello");
Util.print(100);
Box<String> box = new Box<>();
타입 추론이 적용된다.
public static void printList(List<Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
List<Integer> list = List.of(1, 2, 3);
printList(list); // 컴파일 에러
왜 에러일까?
Integer는 Number의 자식이지만
List<Integer> ≠ List<Number>
제네릭은 불공변(Invariant) 이다.
이를 해결하기 위해 와일드카드가 등장한다.
<?>public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
특징:
list.add("hello"); // 컴파일 에러
타입이 무엇인지 모르기 때문이다.
<? extends T>특정 타입 이하(자식 방향) 로만 허용하는 제한
즉,
<? extends Number>
은
만 허용한다는 뜻이다.
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
허용:
* List<Integer>
* List<Double>
* List<Float>
Number n = list.get(0);
모든 요소가 Number의 자식이므로 안전하다.
list.add(10); // 컴파일 에러
리스트가 실제로 List<Double>일 수도 있기 때문이다.
Integer를 넣으면 타입이 깨질 수 있다.
extends는 읽기 전용 (Producer)
<? super T>특정 타입 이상(부모 방향) 으로만 허용하는 제한
즉,
<? super Integer>
은
만 허용한다는 뜻이다.
public static void addInteger(List<? super Integer> list) {
list.add(10);
}
허용:
* List<Integer>
* List<Number>
* List<Object>
list.add(20);
Integer는 부모 타입에 항상 들어갈 수 있다.
Object obj = list.get(0); // Object로만 가능
리스트가 정확히 어떤 타입인지 모르기 때문이다.
super는 쓰기 전용 (Consumer)
좋아.
기존 구조는 그대로 두고, 그 아래에 아주 간단하게 보충 설명만 추가해줄게.
반드시 기억해야 할 원칙:
Producer Extends, Consumer Super
extends는 해당 타입 또는 그 자식을 보장한다.
그래서 꺼낼 때 최소한 그 타입으로 안전하게 받을 수 있다.
List<? extends Number> list;
Number n = list.get(0); // 안전
super는 해당 타입 또는 그 부모를 보장한다.
그래서 그 타입의 값을 안전하게 넣을 수 있다.
List<? super Integer> list;
list.add(10); // 항상 안전
한 줄 정리:
public static void copy(List<Number> dest, List<Number> src) {
for (Number n : src) {
dest.add(n);
}
}
이 메서드는 List만 받는다.
즉:
List<Integer>는 전달 불가List<Double>는 전달 불가List<Object>는 전달 불가실제 사용 환경에서는 이런 상황이 매우 흔하다.
List<Object> dest = new ArrayList<>();
List<Integer> src = List.of(1, 2, 3);
copy(dest, src); // 컴파일 에러
Integer는 Number의 자식이지만 제네릭은 불공변이기 때문에 막힌다.
즉,
상속 관계를 전혀 활용하지 못하는 API가 된다.
이게 확장성이 없는 이유다.
public static void copy(
List<? super Number> dest,
List<? extends Number> src) {
for (Number n : src) {
dest.add(n);
}
}
사용:
List<Object> dest = new ArrayList<>();
List<Integer> src = List.of(1, 2, 3);
copy(dest, src); // 정상 동작
이제 상속 관계를 제대로 활용하는 API가 된다.
이게 제네릭 설계의 핵심이다.
| 문법 | 의미 | 읽기 | 쓰기 |
|---|---|---|---|
<?> | 타입 모름 | Object로 가능 | 거의 불가 |
<? extends T> | T 또는 자식 | 가능 | 불가 |
<? super T> | T 또는 부모 | Object로 제한 | 가능 |
제네릭은 컴파일 타임에만 존재한다.
Box<String> box1 = new Box<>();
Box<Integer> box2 = new Box<>();
런타임에는 둘 다 단순한 Box다.
그래서 불가능:
if (box1 instanceof Box<String>) { } // 에러
Map<String, Integer> map = new HashMap<>();
map.put("age", 20);
int age = map.get("age");
형변환이 사라진다.