java.lang.ClassCastException

객체지향을 공부하다보면 객체지향의 꽃이라고 불리는 다형성을 공부하게된다. 이때 핵심은 클래스간에 상속관계가 형성되었을 경우 부모타입 참조변수로 자식 타입으로 생성된 인스턴스를 참조할 수 있다는 것이다. 이 부분을 공부하다보니 참조변수의 형변환을 할 수 있는 내용에 대해서 헷갈려서 실제로 코드로 작성해보고 테스트해보면서 개인적으로 추후에 해당 에러를 접하게 될 것 같아서 미리 정리해보려고 한다.

// 부모 클래스
class Car{

    protected String color;
    protected int door;

    public void drive(){
        System.out.println("Go~~~~");
    }
}

// Car 클래스를 상속받은 자식클래스
class FireEngine extends Car{

    private int waters ;

    @Override
    public void drive(){
        System.out.println("brrrrrrrr~~~");
    }

    public void water(){
        System.out.println("WaterPark");
    }
}

다음과 같이 상속관계에 있는 클래스를 정의해두고 시작하려고 한다.

 

// 메인 클래스
class CarTest{
    public static void main(String[] args) {
        Car c = new Car(); 
        FireEngine c2 = new FireEngine(); 
    }
}

다형성, 참조변수의 형변환을 배우기 이전까지는 다음과 같이 생성할 인스턴스 타입과 참조변수의 타입을 일치시켜서 진행을 해오게 된다. 여기까지는 받아들이는데 큰 문제는 없었다. 그래서 다형성 파트에 들어서면 다음과 같이 코드를 볼 수 있다.

class CarTest{
    public static void main(String[] args) {
        Car c = new Car(); // 상속 관계 부모타입 인스턴스 생성
        Car c2 = new FireEngine(); // 자식타입 인스턴스 생성
    }
}

즉, 참조변수의 타입과 인스턴스의 타입이 불일치하게 된다. 좀 더 풀어서 부모타입의 참조변수로 자식타입 인스턴스를 참조하는 코드를 볼 수 있다. 사실 여기까지도 공부하는데 큰 어려움은 없다. 이 부분은 앞에서 클래스 간에 관계 중 상속에 대해서 이해했다면 어렵지는 않다.

 

상속관계에 있을 경우 자식 클래스는 부모 클래스로부터 멤버를 상속받게 된다. 그렇기에 자식 클래스로 생성된 인스턴스는 부모의 멤버뿐이나라 자신이 선언한 멤버들까지 사용할 수 있게된다. 그러나 부모 인스턴스는 자식 인스턴스에만 있는 멤버는 사용할 수 없다. 

 

이 부분을 생각하고 보면 어려움은 없다. 단순 참조변수의 타입을 변환함에 따라 생성된 인스턴스의 사용할 수 있는 멤버를 결정할 수 있게된다. 즉, 부모타입의 참조변수로 자식 타입의 인스턴스를 참조할 경우 부모타입의 멤버만 사용할 수 있는 것이다. 이 부분에 대해서는 자바의 정석 p355에 있는 그림을 통해서 쉽게 이해할 수 있다.

한 번 더 정리하면 참조변수타입으로는 생성된 인스턴스에서 사용할 수 있는 멤버를 결정할 수 있는 것이다. 그러나 이때 주의할 점은 생성되는 인스턴스가 무엇인지를 고려해야한다. 쉽게말해 생성된 인스턴스는 해당 인스턴스가 가질 수 있는 모든 멤버를 갖고 있다. 이때 참조변수 타입에 의해 해당 타입에 맞는 멤버들만 선택적으로 사용할 수 있게 된다.

 

문제는 다음부터이다. 이 부분이 많이 헷갈리고 직감적으로 와닿지가 않았다.. 우선 참조변수의 형변환에 대해서 잠깐 살펴보자

 

class CarTest{
    public static void main(String[] args) {

        Car c = null;
        FireEngine f = new FireEngine(); // 참조변수 타입 == 인스턴스 타입
        FireEngine f2 = null;

        f.water();

        /*
             - 현재 f 참조변수는 자식타입 참조변수로 자식 인스턴스를 참조하고 있다.
             - 참조변수는 인스턴스의 주소를 참조하고 있다.
             - 부모타입 참조변수에 자식타입 참조변수가 가르키던 주소를 할당하여 인스턴스를 참조하게한다.
             - 이를 쉽게 풀어보면
             		FireEngine f = [new FireEngine()] == 0x0001 이라고 가정해보자;
             		f = 0x0001이 되며 c = f => c = 0x0001이된다.
             		Car c = 0x0001과 같으며 이는 위에서 살펴본 자식인스턴스를 
                    부모타입참조변수로 참조하는 것과 같다.
             - 원래는 이와같이 형변환 연산자를 사용해야하지만 기본형변수에서도 봤던 것 
               작은 타입을 큰 타입으로 변환 시 생략이 가능하다.
             - 참조형의 경우는 크다는 것이 멤버가 맞다고 착각할 수 있지만, 
                여기서는 크다는 것이 인스턴스의 멤버를 변환한 참조변수타입이 
                모두 사용할 수 있는지를 생각해보면 된다.      
         */
        c = (Car)f;
        c = f;
//        c.water() 부모타입 참조변수이기에 water() 사용할 수 x

//        f2 = c; 반대로 부모타입참조변수를 자식 타입으로 할 시 반대시 형변환을 명시적으로 해야한다.
        f2 = (FireEngine) c;
        f2.water(); // 자식타입참조변수로 형변환했기에 water() 사용가능

    }
}

다음에서는 우선 인스턴스 타입 자체가 자식타입인 상태에서 참조변수의 타입을 변환해 본 것이다. 이처럼 인스턴스 타입이 자식일 경우는 참조변수의 타입을 부모타입과 자식타입으로 보다 참조할 수 있다. 이때, 단순 위에서도 설명한 것과 같이 참조형타입에 따라 인스턴스에서 사용할 수 있는 멤버의 수가 지정될 뿐 크게 문제될 것은 없다. 물론 상속관계에 있을 경우만 가능하다. 그렇지 않을 경우는 형변환이 성립하지 않는다. 그리고 한가지는 부모타입으로 자식타입참조변수를 형변환할경우 형변환이 일어나더라도 참조하고 있는 인스턴스 멤버를 사용하는데 전혀 문제가 되지 않기에 자동형변환이 이루어진다.  . 이 부분은 상속에서 살펴 본 부분과 같다. 그러나 부모타입을 자식타입으로 형변환 할 경우는 변환하고자 하는 자식타입을 명시해주어야한다. 이 부분은 다형성과 관련하여 여러 클래스가 상속을 받을 수 있으며 그렇기에 형변환할 자식은 여러 클래스일 수 있기에 자동형변환이 이루어질 수 없는 것이 아닌가 생각해본다.

 

여기까지 공부하고 난 후 다음과 같은 궁금증이 들었다. 지금까지는 자식인스턴스에 한 해서 진행했다. 그런데 이 때, 인스턴스타입이 부모라면 형변환에 있어서 어떻게 될까?

class CarTest{
    public static void main(String[] args) {

        Car c = new Car(); //부모타입참조변수 = 부모타입 인스턴스
        FireEngine f = null;

        c.drive(); // 전혀 지장 x

        f = (FireEngine) c; // 여기까지는 문제 없다
        f.water(); // 자식타입참조변수이기에 사용가능
        f.drive();
    }
}

 

우선 소스코드를 작성하는 단계에서 전혀문제가 없었다. 컴파일단계에서 문제가 있다면 분명 표시를 했을텐데 그렇지 않았다. 그러나 실행을해보면 다음과 같이 java.lang.ClassCastException  예외가 발생한다. 클래스변환예외임을 예외종류를 보고 직감할 수 있다. 즉 컴파일 시에는 참조변수간의 타입만 체크하기 때문에 인스턴스의 타입을 알 지 못한다. 그러나 실제로 실행하는 과정에서 인스턴스가 생성될 때 부모인스턴스를 자식 참조변수로 참조할 수 없기에  형변환하는데 있어서 예외가 발생한 것이다. 이처럼 이런 상속관계와 형변환을 하는 부분에서 확실히 알고 넘어가지 않는다면 추후에 다형성을 활용하는 측면에서 충분히 마주칠 수 있는 예외라 생각하여 정리해두었다.

 

이와같은 예외를 사전에 방지할 수 있는 방법으로 실제 인스턴스의 종류를 알 수 있는 instance of 연산자를 활용하여 사전에 방지할 수 있을 것이다.