서론

  • 커뮤니티에 다음과 같은 질문이 올라왔다. 질문 내용은 다음과 같다.

 

제네릭은 런타임시 타입 소거(type erasure)를 하기 때문에, 언바운디드(unbounded) 타입 같은 경우

Object로 치환 되는데, 어떻게 클라이언트에서 선언한 타입으로 캐스팅이 되는건가요??

쉽게 풀어서 얘기하면, 다음 코드에서 리턴 타입은 Object인데 어떻게 클라이언트에서 서브 타입으로 타입 캐스팅 없이 받냐라는 내용이다.

 // 리턴 타입은 Object 라면서??!
public <T> T get(T t) {
	return t;
}

 


 

우선 타입 소거가 무엇인지 알아보자.

타입 소거 Type Erasure

 

다음 내용은 오라클 공식 문서에 나와있는 Type Erasure에 대한 설명 중 일부다.

Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to:

- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

추가적인 설명을 덧붙이자면 타입 소거란, 제네릭은 컴파일 타입에만 제약을 가하고, 런타임시 타입에 대한 정보를 버리는 것이다.

여기서 타입을 버린다는 것은 다음과 같이 교체되는 것을 의미한다.

  • bounded type -> bound type
  • unbounded type -> Object

 

그럼 실제로 타입 소거가 되는지 확인해보자.

public class Helper {

    public static <T> T unboundedType(T t) {
        return t;
    }
}

위 코드에서 타입 파라미터는 언바운디드 타입이기 때문에 T -> Object로 치환된다.

언바운디드 타입 같은 경우, 말 그대로 타입에 제한이 없기 때문에  T  =  <T extends Object>랑 같은 코드다.

 

리플렉션을 이용해서 리턴 타입을 확인해보자.

    public static void main(String[] args) throws NoSuchMethodException {
        final Class<Helper> helperClass = Helper.class;

        final Class<?> returnType = helperClass.getMethod("unboundedType", Object.class)
            .getReturnType();
        
        System.out.println("returnType.getName() = " + returnType.getName());
    }

리턴 타입이 Object로 치환된 것을 확인할 수 있다. (당연히 파라미터 타입도 Object로 치환된다)

 

그럼 이번에는 바운디드 타입으로 확인해보자.

public class Helper {

    public static <T extends Food> T boundedType(T t) {
        return t;
    }
}
    public static void main(String[] args) throws NoSuchMethodException {
        final Class<Helper> helperClass = Helper.class;

        final Class<?> returnType = helperClass.getMethod("boundedType", Food.class)
            .getReturnType();

        System.out.println("returnType.getName() = " + returnType.getName());
    }

이번엔 타입 파라미터가 Object가 아닌, Food로 치환되는 것을 확인할 수 있습니다.

사실 이유는 당연하다. 타입 파라미터는 Food의 하위 클래스로 제한되어있기 때문이다.

 

자 이제 본론에 들어가서. 다운 캐스팅에 대해서 얘기해보자.

자바 기본 문법으로 업 캐스팅은 명시적으로 하지 않아도 되지만, 다운 캐스팅은 명시적으로 해줘야한다.

 

그렇다면 Helper.unboundedType에서 Object를 리턴하므로 String 타입으로 캐스팅하려면, 다운 캐스팅이 필요하다.

public class App {

    public static void main(String[] args)  {
        String hello = Helper.unboundedType("hello");
    }
}

 

해당 클래스를 바이트코드로 컴파일 한 뒤, 디컴파일해서 보면 다음과 같이 다운캐스팅을 하는 코드가 추가된 것을 확인할 수 있다.

반면 바운디드 타입 같은 경우 런타임시 타입 파라미터를 바운드 클래스로 치환하기 때문에 타입 변경이 불필요하다.

(물론 다형성을 이용하지 않고, 하위 클래스로 타입을 받는다면, 다운 캐스팅이 발생한다)

 

Food에서는 타입이 Food로 치환되고, 클라이언트도 Food 타입으로 받기 때문에, 다운캐스팅이 발생하지 않는 것을 확인할 수 있다.

(여기서 Chicken은 Food를 상속했다. 만일 타입을 Chicken으로 받는다면 다운 캐스팅 코드가 추가된다)

 

 

정리가 끝나고, 공부한 내용으로 답변을 달고 싶었지만, 이미 다른분이 간략하게 알려주셨다.

 

Reference

- https://docs.oracle.com/javase/tutorial/java/generics/erasure.html