제네릭 타입과 타입 안전성
제네릭은 같은 자료구조나 알고리즘을 여러 참조 타입에 적용하면서 컴파일 시점의 타입 검사를 유지합니다.
타입 매개변수는 객체를 만들 때 실제 타입 인자로 치환되는 자리입니다.
원시 타입을 사용하면 이전 코드와 호환은 되지만 검사되지 않은 대입이 들어와 실행 중 캐스트 오류가 날 수 있습니다.
기본 타입은 직접 타입 인자로 사용할 수 없어 래퍼 타입을 사용합니다.
이 절에서는 캐스트를 호출자에게 떠넘기지 않는 컨테이너를 만듭니다.
제네릭의 보장은 주로 컴파일 시점에 작동합니다. Box<String>과 Box<Integer>를 서로 다른 API 계약으로 검사한 뒤, JVM에서는 같은 Box 클래스의 객체로 실행합니다. 따라서 제네릭은 값을 자동 변환하는 기능이 아니라 잘못된 값이 경계를 통과하지 못하게 하는 정적 장치로 이해해야 합니다.
타입 매개변수가 저장과 반환의 관계를 묶는다
Box<T>의 put과 get이 같은 T를 사용하므로 넣은 타입과 꺼내는 타입을 컴파일러가 연결합니다.
다이아몬드 연산자가 문맥에서 타입을 추론한다
new Box<String>()의 오른쪽 타입 인자를 new Box<>()로 줄여도 왼쪽 선언에서 String을 압니다.
추론 결과가 불분명하면 명시합니다.
같은 Box 구현을 사용해도 T가 입력과 출력을 묶으므로 호출 지점마다 서로 다른 계약이 생깁니다.
public class GenericBox {
static final class Box<T> {
private T value;
void put(T value) { this.value = value; }
T get() { return value; }
}
public static void main(String[] args) {
Box<String> names = new Box<>();
names.put("Java");
String value = names.get();
System.out.println(value.toUpperCase());
Box<Integer> numbers = new Box<>();
numbers.put(25);
System.out.println(numbers.get() + 1);
// names.put(25); // 컴파일 오류: Integer는 String Box에 넣을 수 없음
}
}names.get()에는 별도 캐스트가 없는데도 String 메서드인 toUpperCase를 호출할 수 있고, numbers.get()은 Integer로 읽혀 산술 연산에 참여합니다. 주석의 잘못된 대입은 실행하기 전에 컴파일러가 막습니다.
JAVA
26Box의 T가 put 입력과 get 반환을 묶어 잘못된 대입을 컴파일 단계에서 막습니다.
원시 타입은 타입 검사를 우회한다
Box raw에는 어떤 객체든 넣을 수 있어 나중에 예상 타입으로 읽을 때 ClassCastException이 날 수 있습니다.
경고를 무시하지 않습니다.
원시 타입, 명시적·추론 타입 인자, 기본형 제한을 나란히 비교해 컴파일러의 검사가 사라지는 지점을 찾습니다.
Box raw = new Box(); // 원시 타입: 검사되지 않은 경고
raw.put(42);
// String broken = (String) raw.get(); // 실행 시 ClassCastExceptionBox<String> explicit = new Box<String>();
Box<String> inferred = new Box<>();// Box<int> values; // 컴파일 오류: 기본 타입은 타입 인자로 사용 불가
Box<Integer> values = new Box<>();기본 타입에는 래퍼를 사용한다
List<int>는 허용되지 않고 List<Integer>를 사용합니다.
오토박싱이 변환을 줄이지만 null과 객체 생성 비용은 남습니다.
Box<Integer>에 null을 넣고 numbers.get() + 1을 실행해 보세요. 오토박싱이 타입을 맞춰 주어도 null을 기본형 int로 언박싱하는 순간 NullPointerException이 납니다. 제네릭의 타입 안전성과 값의 유효성 검사가 별도 책임임을 보여 주는 경계입니다.
원시 타입 경고가 보이면 “예전 코드니까 괜찮다”고 넘기지 말고, 외부 라이브러리와 만나는 한 지점에서만 검사한 뒤 매개변수화된 타입으로 되돌리세요. 검사되지 않은 값이 내부로 퍼질수록 실제 오류는 원인에서 멀어진 곳에서 발생합니다.
제네릭의 핵심은 코드를 한 번 쓰는 것보다 입력과 출력 사이 타입 관계를 컴파일러가 끝까지 추적하게 하는 것입니다.