Collection<Integer> collection = (Collection<Integer>) new HashMap<Integer, Integer>();
위 코드를 그대로 작성하면 아래와 같은 경고 메시지를 띄울 뿐 컴파일 에러를 발생 시키지 않는다.
Unchecked cast: 'java.util.HashMap<java.lang.Integer,java.lang.Integer>' to 'java.util.Collection<java.lang.Integer>'
Collection과 HashMap은 서로 아무런 관계가 없는 객체임에도 명시적 형변환이 가능한 것 처럼 표시된다. 하지만 이러한 형변환은 런타임 환경에서 아래와 같은 에러를 발생시키게 된다.
Exception in thread "main" java.lang.ClassCastException: class java.util.HashMap cannot be cast to class java.util.Collection (java.util.HashMap and java.util.Collection are in module java.base of loader 'bootstrap')
원시타입이나 일반 객체들의 잘못된 형변환은 컴파일 타임에서 잘 잡아주면서 왜? 위와 같은 잘못된 형변환은 잡아주지 못하는 것일까
이를 이해하기 위해서는 자바의 타입 시스템이 어떻게 동작하는 지를 알아야한다.
자바는 unchecked casting을 허용한다.
이게 무슨 말이냐 하면 자바 컴파일러는 컴파일 타임에 타입을 엄격하게 체크하지 않아 Collection<Integer>와 HashMap<Integer, Integer>는 서로 다른 타입이지만 HashMap을 Collection으로 강제 캐스팅할 수 있는지 여부를 명확하게 판단하지 못하고 경고만 발생시키면서 코드를 허용한다는 뜻이다.
명확하게 판단하지 못한 두 객체의 실제 타입에 대한 검사는 런타임에서 진행해 위와 같은 상황에서는 컴파일러가 에러를 찾지 못하는 일이 발생하게 된다.
깐깐하다고 생각했던 컴파일러가 타입을 명확하게 판단하지 못한다니 왜 이런 일이 발생한것일까
그 이유는 Collection 객체가 인터페이스이기 때문이다. 자바의 인터페이스는 어떤 클래스든지 해당 인터페이스를 구현할 가능성이 있다. 컴파일러는 클래스가 특정 인터페이스를 구현하지 않은 경우에도 해당 객체가 런타임에 인터페이스를 구현할 수 있다고 가정할 수 있다. 그렇기 때문에 컴파일러는 런타임에 객체가 실제로 어떤 타입의 인스턴스인지를 정확히 알 수 없다는 점을 배제하지 않고 형변환을 허용하게 된다.
Object obj = new Object();
Runnable runnable = (Runnable) obj; // 컴파일 타임 오류 없음, 런타임 오류 발생 가능
위와 같은 경우를 생각해보자. Object 클래스는 Runnable 인터페이스를 구현하지 않지만 컴파일러는 명시적 형변환을 허용한다. 그 이유는 컴파일러는 obj가 Runnable을 구현하는 서브클래스일 가능성을 배제하지 않기 때문이다. 하지만 런타임에 실제 객체 타입이 Runnable과 호환되지 않으면 ClassCastException이 발생하게된다.
이와는 반대로
Object obj = new String();
Integer number = (Integer) obj; // 컴파일 타임 오류
두 클래스 간의 형변환은 클래스 계층 구조에 명확한 관계가 존재하는지 여부를 컴파일 타임에 검증할 수 있어 컴파일러는 두 클래스 간에 형변환이 가능한지를 바로 알 수 있다. 이 경우, String과 Integer는 서로 상속 관계가 없기 때문에 컴파일 타임에 에러가 발생한다.
'Language > Java' 카테고리의 다른 글
Java native 키워드로 Rust코드와 연동하기 (2) | 2024.09.19 |
---|---|
Java Modifier(제어자) (1) | 2024.09.17 |
Java Reflection(3) (1) | 2024.09.15 |
Java Reflection(2) (0) | 2024.08.25 |
Java Reflection(1) (0) | 2024.08.21 |