목차
Java Generic(제네릭)
제네릭은 클래스·인터페이스·메서드가 다룰 값의 타입을 매개변수로 받는 문법이다. List<String>은 문자열 목록이고 List<Integer>는 정수 목록이다. 둘을 컴파일 시 구분해 잘못된 대입을 미리 찾고, 값을 꺼낼 때 호출자가 형변환을 반복하지 않게 한다. 주된 이점은 타입 안전성이다. 제네릭이 불필요한 런타임 형변환을 모두 없애서 성능이 항상 좋아진다고 단정할 수는 없다. Java 제네릭은 대체로 타입 소거(type erasure)를 거쳐 구현된다.
아래 코드 조각은 순서대로 jshell에서 입력하거나 클래스의 메서드 안으로 옮겨 확인할 수 있다. 선언 예제는 같은 파일에 둘 수 있다.
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>();
names.add("Kim");
String first = names.get(0);
// names.add(42); // 컴파일 오류
원시 타입(raw type) List를 쓰면 42 같은 다른 타입을 넣어도 컴파일러가 충분히 검사하지 못하고 값을 꺼내 형변환할 때 실패할 수 있다. 오래된 API와 연결하는 구간 외에는 타입 인자를 명시한다.
제네릭 클래스와 메서드
class Box<T> {
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
class Product<K, V> {
private final K kind;
private final V model;
Product(K kind, V model) {
this.kind = kind;
this.model = model;
}
K kind() { return kind; }
V model() { return model; }
}
class Examples {
static <T> Box<T> box(T value) {
Box<T> result = new Box<>();
result.set(value);
return result;
}
}
Box<T>의 T는 값을 넣고 꺼낼 타입을 연결한다. Product<K,V>는 종류와 모델을 서로 다른 타입으로 다룰 수 있다. Examples.box(10)에서는 인자에서 T를 Integer로 추론해 Box<Integer>를 돌려준다. 원문의 예제에는 Box.add로 정의하고 다른 곳에서는 Box.set을 호출하는 불일치가 있었으므로 한 이름으로 맞췄다. 메서드의 <T>는 반환 타입 앞에 놓인다.
상한이 필요한 경우
static <T extends Number> double twice(T value) {
return value.doubleValue() * 2;
}
System.out.println(twice(3)); // 6.0
System.out.println(twice(1.5)); // 3.0
T extends Number는 Number 또는 그 하위 타입만 받겠다는 뜻이다. 따라서 doubleValue()를 호출할 수 있다. 원문은 longValue()로 두 숫자를 비교했는데, 1.2와 1.8은 둘 다 1로 변환되어 같다고 잘못 판단한다. 숫자를 비교하는 요구라면 허용할 타입과 소수·정밀도 규칙을 먼저 정하고, 같은 타입의 비교 가능한 값에는 Comparable 같은 경계를 검토한다.
와일드카드와 읽기·쓰기 방향
List<Student>는 List<Person>의 하위 타입이 아니다. 그렇지 않으면 List<Student>에 Worker를 넣는 위험이 생긴다. 여러 하위 목록을 읽을 때 와일드카드를 쓴다.
class Person { }
class Student extends Person { }
class Worker extends Person { }
static void printPeople(List<? extends Person> people) {
for (Person person : people) System.out.println(person);
// people.add(new Student()); // 컴파일 오류: 실제 목록의 원소 타입을 모름
}
static void addStudent(List<? super Student> people) {
people.add(new Student());
Object first = people.get(0); // Person이라고 보장할 수 없음
}
? extends Person은 Person의 하위 타입 목록을 생산자로 읽기 좋다. ? super Student는 Student를 담을 수 있는 List<Student>, List<Person>, List<Object> 같은 소비자를 받는다. 읽을 때는 최소 Object만 보장된다. 흔히 PECS(Producer Extends, Consumer Super)라고 기억한다. 원문의 Course<? super Worker> 설명은 Worker와 그 상위 타입을 담는 Course만 받는다는 점은 맞지만, 그 목록에 임의의 Person을 추가해도 된다는 뜻은 아니다.
타입 소거와 런타임 경계
List<String> text = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(text.getClass() == numbers.getClass()); // true
타입 인자는 주로 컴파일 시 검사된다. 그래서 런타임에 value instanceof List<String>처럼 원소 타입까지 검사할 수 없다. JSON이나 네트워크에서 읽은 데이터는 컴파일러가 타입 안전성을 보장하지 않으므로 값의 실제 형태를 별도로 검증해야 한다. 제네릭은 코드 내부의 계약을 안전하게 만들지만 외부 데이터의 신뢰 문제를 해결하지는 않는다.