Generic 에 대해 잘 알고 있다고 생각했는데 새로운 사실을 알게되어 간단하게 정리.
Generic 은 jdk 1.5 에 도입되었으며, 하위 버전 호환성을 위해 타입 소거하는 방식을 채택했다.
여기서 타입 소거란, 런타임 때 타입을 지워 타입 정보를 알 수 없게 하는 것을 말한다.
예를 들면,
public <T> List<T> getListOf(T t1, T t2) {
...
}
이런 코드가 런타임에는
public List getListOf(Object t1, Object t2) {
...
}
이렇게 바뀐다는 이야기이다.
Generic 이 도입되기 전, java 에서는 List<T> 대신 List 와 같은 것들이 사용되었다.
따라서 타입 소거를 하지 않으면 하위 버전의 호환성이 깨지기 때문에 도입했다고 한다.
(참고로 kotlin 에서는 타입 소거를 하지 않는다.)
이 내용을 이해하고 아래의 코드를 보자.
public class GenericTest {
public void test() {
String test = "test";
Wrapper<Integer> wrapped = wrapping(test);
Integer body = wrapped.getBody(); // CCE! (1)
System.out.println("body = " + body);
String mapped = mapping(wrapped, String.class);
System.out.println("mapped = " + mapped); // "test"
}
private <T> Wrapper<T> wrapping(String sample) {
return new Wrapper<>((T) sample); // why not CCE? (2)
}
private <S, T> T mapping(Wrapper<S> wrapped, Class<T> destinationType) {
ModelMapper modelMapper = new ModelMapper();
S body = wrapped.getBody(); // why not CCE? (3)
return modelMapper.map(body, destinationType);
}
public static class Wrapper<T> {
private final T body;
public Wrapper(T body) {
this.body = body;
}
public T getBody() {
return body;
}
}
}
Wrapper 클래스를 만듬"test" 라는 문자열을 Integer 안에 집어넣음 (정수로 변환될 수 없는 문자열)자, 이제 하나씩 보자.
public void test() {
String test = "test";
Wrapper<Integer> wrapped = wrapping(test); // (1)
Integer body = wrapped.getBody(); // CCE! (2)
System.out.println("body = " + body);
...
}
private <T> Wrapper<T> wrapping(String sample) {
return new Wrapper<>((T) sample); // (3)
}
public static class Wrapper<T> {
private final T body;
public Wrapper(T body) {
this.body = body;
}
public T getBody() {
return body;
}
}
이 코드를 실행하면 (2) 번 부분에서
ClassCastException이 발생한다.
wrapping() 에서 명시적으로 타입 변환을 해주었다.new Wrapper<>((T) sample)T 는 Integer 이다.아니다.
런타임 때는 타입이 소거되며, 제네릭 타입은 실제 사용될 때까지 Object 타입으로 이동한다.
즉, wrapping() 메서드는 런타임 때는 실제로 아래와 같이 동작하는 것이다.
public void test() {
String test = "test";
Wrapper wrapped = wrapping(test); // (1)
Integer body = wrapped.getBody(); // CCE! (2)
System.out.println("body = " + body);
...
}
private Wrapper wrapping(String sample) {
return new Wrapper<>(sample); // (3)
}
<T> 와 (T) 는 모두 소거된다.(T) 가 의미있었지만, 런타임 때는 소거되므로 아무런 의미를 갖지 못 한다."test" 가 Wrapper 안에 들어있다.간단하다. Integer body 변수 안에 getBody() 의 결과를 넣으려고 하기 때문이다.
"test" 를 Integer 에 넣으려고 시도하며, 이 때 ClassCastException 이 발생하게 된다!그렇다면 CCE 를 회피하려면 어떻게 하면 될까?
getBody() 의 결과를 Object 로 받으면 된다.
아래와 같은 코드가 될 것이다.
public void test() {
String test = "test";
Wrapper<Integer> wrapped = wrapping(test); // (1)
Object body = wrapped.getBody(); // "NOT" CCE! (2)
System.out.println("body = " + body);
...
}
private <T> Wrapper<T> wrapping(String sample) {
return new Wrapper<>((T) sample); // (3)
}
이렇게 작성하면 컴파일 타임에도, 런타임에도 오류가 발생하지 않는다.
그리고 system.out 으로 "body = test" 가 정상 출력 된다.
참고로 getBody() 의 결과를 String 으로 받으려고 하면 컴파일 타임에서 오류가 발생한다.
public class GenericTest {
public void test() {
String test = "test";
Wrapper<Integer> wrapped = wrapping(test);
String mapped = mapping(wrapped, String.class);
System.out.println("mapped = " + mapped); // "test"
}
private <T> Wrapper<T> wrapping(String sample) {
return new Wrapper<>((T) sample);
}
private <S, T> T mapping(Wrapper<S> wrapped, Class<T> destinationType) {
ModelMapper modelMapper = new ModelMapper();
S body = wrapped.getBody(); // why not CCE? (1)
return modelMapper.map(body, destinationType);
}
public static class Wrapper<T> {
private final T body;
public Wrapper(T body) {
this.body = body;
}
public T getBody() {
return body;
}
}
}
이 코드를 실행하면 에러 없이
system.out으로"mapped = test"가 출력된다.
mapping 메서드에서 S 는 Integer, T 는 String 이다.getBody() 를 실행해 S 에 넣는다.결과적으로 같은 이유 때문이다.
<S>, <T> 는 모두 소거된다. (사라진다.)S 와 T 는 Object 로 소거된다.즉,
private <S, T> T mapping(Wrapper<S> wrapped, Class<T> destinationType) {
ModelMapper modelMapper = new ModelMapper();
S body = wrapped.getBody(); // why not CCE? (1)
return modelMapper.map(body, destinationType);
}
...
public T getBody() {
return body;
}
이 코드가
private Object mapping(Wrapper wrapped, Class destinationType) {
ModelMapper modelMapper = new ModelMapper();
Object body = wrapped.getBody(); // why not CCE? (1)
return modelMapper.map(body, destinationType);
}
...
public Object getBody() {
return body;
}
이렇게 바뀐다는 것이다.
따라서 아무 문제없이 동작한다.
"test" 는 Object 형태로 계속 돌아다니다가 최종적으로 다시 String 으로 변환될 것이다.
다시 정리해보자.
public class GenericTest {
public void test() {
String test = "test";
Wrapper<Integer> wrapped = wrapping(test);
// Integer body = wrapped.getBody(); // CCE! (1)
// System.out.println("body = " + body);
String mapped = mapping(wrapped, String.class);
System.out.println("mapped = " + mapped); // "test"
}
private <T> Wrapper<T> wrapping(String sample) {
return new Wrapper<>((T) sample); // why not CCE? (2)
}
private <S, T> T mapping(Wrapper<S> wrapped, Class<T> destinationType) {
ModelMapper modelMapper = new ModelMapper();
S body = wrapped.getBody(); // why not CCE? (3)
return modelMapper.map(body, destinationType);
}
public static class Wrapper<T> {
private final T body;
public Wrapper(T body) {
this.body = body;
}
public T getBody() {
return body;
}
}
}
Wrapper<> 안에 들어가는 타입은 아무런 의미를 갖지 않는다!타입 소거를 기억하자.
<T> 와 (T) 는 모두 소거된다. (사라진다.)(T) 가 의미있지만, 런타임 때는 소거되므로 아무런 의미를 갖지 못 한다.T 는 Object 로 소거된다.이러한 타입 소거로 인한 제네릭의 특징을 비 구체화(non-reify) 라고 한다.
p.s. 제네릭 하면 공변과 불공변이라는 개념도 있는데, 이 부분은 다음에 정리해보겠다.