Java Generic - Type Erasure
타입 이레이저에 대한 설명을 하기 전에 컴파일러가 제네릭을 어떻게 처리하는지부터 살펴보자.
제네릭 타입은 컴파일 단계에서 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다!!
제네릭에 사용한 변수는 모두 사라지는 절차가 있다. 즉, .java 제네릭이 존재하나, 바이트코드로 구성된 .class 에서는 타입 파라미터가 존재하지 않게 되는 것이다.
동작 과정을 코드로 살펴보도록 한다.
Box.class
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
- 제네릭 타입을 선언한다.
Main.class
public class Main {
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.set(100);
Integer value = box.get();
}
}
- 제네릭 타입에 Integer 타입 인자를 전달한다.
컴파일 시점에서 매개변수와 타입 인자를 포함한 제너럴 정보가 "Integer" 인지하고 컴파일을 시도한다.
Box.java - 컴파일 시점
public class Box<Integer> {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
- 컴파일 에서는 제네릭 타입을 모두 Integer로 정보로 인지한다.
- 컴파일 종료되면 코드의 Integer 는 모두 제거된다.
Box.class - 컴파일 종료 후 런타임 시점
public class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
- 컴파일 종료 시점에서 상한 제한 없이 선언한 타입 매개변수들은 모두 변환된다.
- 구체적으로 T 제네릭 타입이 모두 Object 타입으로 변환되었다.
다음으로 호출부에도 변화가 일어난다.
Main.class - 컴파일 종료 후 런타임 시점
public class Main {
public static void main(String[] args) {
Box box = new Box();
box.set(100);
Integer value = (Integer) box.get(); // 컴파일러가 강제로 캐스팅
}
}
- 값을 반환하는 부분을 Object 받고 싶다. 타입이 맞지 않으므로 컴파일러가 스스로 Integer 캐스트하는 작업을 추가한다.
- 개발자에게 숨으면서 컴파일러가 검증하고 추가하는 작업을 통해 문제가 발생되지 않는다.
제네릭 처리 과정 정리
앞에서 설명한 것처럼 컴파일 단계 전에 컴파일러가 코드를 모두 검증하였다.
그리고 컴파일 이후 런타임 단계에 실행되도록 Object 타입으로 바꿔버리고 캐스트를 강제로 추가해 문제 없이 동작하는 코드로 변경하였다.
타입 매개변수 제한을 걸어버린 경우
제네릭 타입이나 제네릭 메소드에 매개변수 제한을 extends 키워드를 붙여 사용할 수 있다.
AnimalHospital.java - 컴파일 시점
public class AnimalHospital<T extends Animal> {
public static <T extends Animal> void checkup(T t) {
System.out.println("동물의 이름 = " + t.getName());
System.out.println("동물의 몸무게 = " + t.getSize());
t.sound();
}
public T bigger(T t1, T t2) {
return t1.getSize() > t2.getSize() ? t1 : t2;
}
}
- 제네릭 메소드로 사용한 checkup 메소드가 있다.
- 제네릭 타입으로 사용한 bigger 일반 메소드가 있다.
Main.java - 컴파일 시점
AnimalHospital<Dog> animal = new AnimalHospital<>();
Dog dog = new Dog("강아지", 15);
Dog littleDog = new Dog("작은강아지", 8);
AnimalHospital.checkup(dog);
Dog biggerDog = animal.bigger(dog, littleDog);
System.out.println(biggerDog);
동물의 이름 = 강아지
동물의 몸무게 = 15
멍
Animal{name='강아지', size=15}
다음으로 런타임 시점에서 코드를 바라보자.
AnimalHospital.class - 런타임 시점
public class AnimalHospital {
public static void checkup(T t) {
System.out.println("동물의 이름 = " + t.getName());
System.out.println("동물의 몸무게 = " + t.getSize());
t.sound();
}
public Animal bigger(Animal t1, Animal t2) {
return t1.getSize() > t2.getSize() ? t1 : t2;
}
}
Main.class - 런타임 시점
AnimalHospital animal = new AnimalHospital();
Dog dog = new Dog("강아지", 15);
Dog littleDog = new Dog("작은강아지", 8);
AnimalHospital.checkup(dog);
Dog biggerDog = (Dog) animal.bigger(dog, littleDog);
System.out.println(biggerDog);
- 이 코드는 컴파일러가 알아서 만들어주는 코드다.
- Dog biggerDog 선언하는 부분을 보자. animal 인스턴스를 가져와 bigger 메소드 호출 시 반환이 Animal 이기에 강제로 Dog타입으로 캐스트하여 할당한다.
자바의 제네릭은 개발자가 직접 캐스트하는 코드를 컴파일러가 대신 처리한다.
이러한 타입들을 제거하는 과정이 Tyep Erasure 이다.
타입 이레이저 방식의 한계
자바는 호환성이 높은 코드로. 제네릭 하위호환을 맞추도록 개발되어 왔다.
.class 런타임코드 생성에서는 <String>, <Animal> 같은 제네릭 타입을 모두 제거하고 다른 타입으로 대체한다.
문제는 대체하는 타입에서 Object인 경우 의도치 않은 동작을 할 수 있다.
예제 소스
public class InstanceBox<T> {
public boolean instance(Object param) {
return param instanceof T;
}
public void create() {
return new T();
}
}
- 이것 모두 컴파일러에서 Object 로 변환되어 사용할 수 없다고 IDE 에서 표기한다.
모든 객체는 Object 객체를 생성해줄 것이고, 타입 검사는 모두 참으로 반환해줄 것이기 때문