자바의 제네릭(Generic)은 재사용 가능한 코드를 작성할 수 있고, 동시에 타입 안정성(type safety)를 확보할 수 있는 편리한 문법입니다.
Box 클래스는 아직 타입이 지정되지 않은 T라는 타입을 받는 범용적인 박스입니다. set() 메서드를 통해 필드 값을 설정하고, get() 메서드로 해당 필드를 반환하는 간단한 클래스입니다.
즉, T는 어떤 타입이든 될 수 있기 때문에 코드를 한 번만 작성해도 다양한 타입에 대해 유연하게 대응할 수 있습니다.
1 : 타입 안정성
stringBox.set("Hello")
가 호출되면 컴파일러는 T를 String으로 추론하게 됩니다. 이후 stringBox.set(123)
처럼 다른 타입(int)을 넣으려고 하면 컴파일 오류를 발생시킵니다.
이는 제네릭이 타입 안정성(type safety)을 컴파일 시점에 보장해주기 때문에 런타임 에러의 위험을 줄일 수 있습니다.
2 : 재사용성
제네릭 클래스를 한 번 정의해두면
Box<String>
, Box<Integer>
처럼 다양한 타입에 대해 인스턴스를 생성해 재사용할 수 있습니다.
만약, 제네릭이 없었다면 타입별로 StringBox
, IntegerBox
클래스를 일일이 만들어야 했을 것입니다.
우리는 제네릭을 통해 코드의 중복을 줄이고, 유지보수성과 확장성을 높일 수 있습니다.
제네릭에 대해 알아보았으니 아래 코드의 결과를 예측해봅니다.
class Main {
private static class Collection<T> {
private T value;
public Collection(T t) {
value = t;
}
public void print() {
Printer.print(value);
}
}
class Printer {
static void print(Integer a) {
System.out.println("A" + a);
}
static void print(Object b) {
System.out.println("B" + b);
}
static void print(Number c) {
System.out.println("C" + c);
}
}
public static void main(String[] args) {
new Collection<>(0).print();
}
}
new Collection<>(0)
호출 시, Integer로 오토박싱 되니까 제네릭의 T 타입 추론에 의해 print(Integer) 호출될 것하지만, 실제 결과는 B0입니다.
왤까요 ?
자바는 저희를 속이고 있는데요, 사실 제네릭 타입은 컴파일러에게만 존재하는 정보일 뿐, 런타임 시점에는 T라는 타입은 어디에도 존재하지 않게 됩니다.
자바는 처음부터 제네릭을 지원하지 않았고, 기존의 JVM이 제네릭 타입을 모르기 때문에 하위 호환성을 위해 나온 방법입니다.
쉽게 말해
즉, T는 컴파일 체크를 위해 보여질 뿐 바이트 코드로 변환되면 T는 존재하지 않고 모두 Object가 됩니다.
위의 Collection<T>
코드의 바이트 코드 모습은 아래와 같습니다.
제네릭 타입(<T>)은 소거되고, T 타입은 모두 Object 타입으로 변환되게 됩니다.
이를 통해 제네릭은
JVM과의 호환성도 지키며 위와 같은 장점들을 사용할 수 있게 해주었습니다.
"자바의 제네릭은 컴파일 시점에만 살아있고, 런타임에 사라지는 유령 같은 존재였던 것입니다!"
단계 | 설명 |
---|---|
컴파일 시점 | 제네릭 타입 체크 (List 등) |
타입 소거 | T -> Object 또는 상한 타입으로 변환 |
런타임 | 제네릭 타입 정보는 존재하지 않음 |
이제부터 제네릭을 사용할 때마다 이 사실을 기억하며 사용하세요!