업캐스팅 & 다운캐스팅

설명

하나의 데이터 타입을 다른 타입으로 바꾸는 것을 타입 변환 혹은 형변환이라고 합니다.

 

기본적으로 자바에서는 대입 연산자 = 에서 변수와 값 서로 양쪽의 타입이 일치하지 않으면 할당이 불가능합니다.

프로그램에서 값의 대입이나 연산을 수행할 때는 같은 타입끼리만 가능하기 때문입니다.

long d = 10.233; // ERROR

 

그래서 다음과 같이 캐스팅 연산자를 사용하여 강제적으로 타입을 지정하여 변수에 대입하도록 해야 합니다.

long d = (long)10.233;

 

상속 관계의 클래스는 크게 부모클래스와 자식클래스로 구분할 수 있습니다.

기본형 타입을 서로 형변환 할 수 있듯이, 자바의 상속 관계에 있는 부모와 자식클래스 간에는 서로 간의 형변환이 가능합니다.

 

클래스는 Reference 타입으로 분류되니 이를 참조형 캐스팅(업캐스팅 / 다운캐스팅) 이라고 부릅니다.

한 가지 주의해야 할 점은, 같은 부모 클래스를 상속받고 있더라도 형제 클래스끼리는 아예 타입이 다르기 때문에 참조 형변환이 불가능합니다.

업캐스팅

업캐스팅은 자식 클래스 객체를 부모 클래스 타입으로 변환하는 것을 말합니다.

class Parent {
    String name;
    int age;
}

class Child extends Parent {
    /*
    String name;
    int age;
    */
    int number;
}

Parent p = new Parent(); 
Child c = new Child();

Parent p2 = (Parent)c; // 명시적 업캐스팅 - 자식에서 부모로
Parent p2 = c; // 암시적 업캐스팅 - 위에 업캐스팅과 같음

 

업캐스팅은 캐스팅 연산자 괄호를 생략할 수 있으며, 부모 클래스로 캐스팅된다는 것은 멤버의 개수 감소를 의미합니다.

이는 곧 자식 클래스에서만 있는 속성과 메서드는 실행하지 못한다는 것을 의미합니다.

Animal animal = (Animal) dog; // 명시적 캐스팅
Animal animal = dog;          // 암시적 캐스팅 (생략 가능)

// 그리고 Dog 에서만 정의한 Jump() 메서드도 실행이 불가능

 

마지막으로 업캐스팅의 가장 큰 특이점은 업캐스팅을 하고 메소드를 실행할 때, 만일 자식 클래스에서 오버라이딩한 메서드가 있을 경우, 부모 클래스의 메서드가 아닌 오버라이딩 된 메서드가 실행됩니다.

 

오버라이딩 된 메서드 실행

class Animal {
    void sound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 업캐스팅
        animal.sound(); // 출력: Bark
    }
}

 

Dog 객체를 Animal 타입으로 업캐스팅 했지만 animal.sound() 호출 시, 실제 객체는 Dog 이므로 Dog의 sound()가 실행됩니다. ( 다형성 )

업캐스팅을 하는 이유

이렇게 업캐스팅을 하는 방법과 특징, 주의점은 알았지만 정작 왜 하는지는 모호할 수 있습니다.

업캐스팅을 사용하는 이유는 공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위해서 입니다.

// 부모 클래스
class Animal {
    void sound() {
        System.out.println("Some generic animal sound");
    }
}

// 자식 클래스 1
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Bark");
    }
}

// 자식 클래스 2
class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow");
    }
}

// 자식 클래스 3
class Cow extends Animal {
    @Override
    void sound() {
        System.out.println("Moo");
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 Animal 객체 생성
        Animal dog = new Dog(); // 업캐스팅
        Animal cat = new Cat(); // 업캐스팅
        Animal cow = new Cow(); // 업캐스팅

        // 배열로 공통적으로 관리
        Animal[] animals = {dog, cat, cow};

        // 공통된 메서드 호출
        for (Animal animal : animals) {
            animal.sound(); // 각 동물의 소리를 출력
        }
    }
}

 

Dog, Car, Cow 객체를 각각 Animal 타입으로 업캐스팅하여 배열에 저장합니다.

배열의 타입은 Animal 이므로 모든 자식 클래스를 공통적으로 처리할 수 있으며, 부모 클래스 Animal의 메서드 sound()를 호출하면, 각 자식 클래스에서 오버라이드된 버전이 실행됩니다.

 

이렇게 하나의 타입으로 묶어 배열을 구성할 수 있어 코드량도 훨씬 줄어들고 가독성도 좋아지며 유지보수성도 좋아짐을 볼 수 있습니다.

 

여기서 또 궁금해지는 게 있습니다. 위에서 언급했던 것처럼 자식 클래스에만 있는 고유한 메서드를 실행하려면 어떻게 해야 할까요? 오버라이딩 한 메서드가 아닌 이상 업캐스팅한 부모 클래스 타입에서 자식 클래스의 고유 메서드를 실행할 수 없습니다. 따라서 업캐스팅한 객체를 다시 자식 클래스 타입으로 되돌리는 다운 캐스팅이 등장하게 되었습니다.

 

다운캐스팅

다운캐스팅은 거꾸로 부모 클래스가 자식 클래스 타입으로 캐스팅되는 것을 말합니다.

다운캐스팅은 캐스팅 연산자 괄호를 생략할 수 없으며, 다운캐스팅의 목적은 업캐스팅한 객체를 다시 자식 클래스 타입의 객체로 되돌리는데 목적을 두고 있습니다.

 

즉, 다운캐스팅의 진정한 의미는 부모 클래스로 업 캐스팅된 자식 클래스를 복후하여, 본인의 필드와 기능을 회복하기 위해 있는 것입니다.

class Animal {
    void sound() {
        System.out.println("Some generic animal sound");
    }
}

class Dog extends Animal {
    void sound() {
        System.out.println("Bark");
    }

    void wagTail() {
        System.out.println("Dog is wagging its tail");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 업캐스팅
        Dog dog = (Dog) animal;    // 다운캐스팅 ( 연산자 괄호를 절대 생략 X )

        dog.sound();               // 출력: Bark (Dog의 메서드 호출)
        dog.wagTail();             // 출력: Dog is wagging its tail (Dog 고유 메서드 호출)
    }
}

 

문득 궁금해지는 게 왜 다운캐스팅은 캐스팅 연산자를 생략할 수 없을까?인데 나름의 이유가 있습니다.

다운캐스팅은 곧 사용할 수 있는 객체 멤버 증가를 의미하는데, 멤버의 증가는 불안전합니다. 왜냐하면 실제 참조변수가 가리키는 객체가 무엇인지 모르기 때문에 어떠한 멤버가 추가되는지 알 수 없기 때문입니다. 그래서 반드시 형변환 괄호를 기재함으로써 증가된 클래스의 멤버가 무엇인지 알게 하도록 개발자한테 알려줘야 하기 때문입니다.

다운캐스팅의 주의점

캐스팅의 목적은 업캐스팅한 객체를 되돌리는 데 있다고 했습니다. 

이 의미는 업캐스팅하지 않은 생 부모 객체일 경우, 이를 그대로 다운캐스팅할 경우 오류( ClassCastException )가 발생합니다.

Unit unit = new Unit();

Zealot unit_down2 = (Zealot) unit; //! RUNTIME ERROR - Unit cannot be cast to Zealot
unit_down2.attack(); //! RUNTIME ERROR
unit_down2.teleportation(); //! RUNTIME ERROR
이런 다운캐스팅의 주의점은 컴파일 에러가 발생하지 않고 런타임 에러가 발생하는 큰 위험이 있습니다.

 

'Java' 카테고리의 다른 글

String  (0) 2024.12.17
Java - Abstract  (1) 2024.12.11
자바 공부할 때 도움이 되는 용어 설명  (0) 2024.12.10
인터페이스  (0) 2024.12.10
List, ArrayList, LinkedList  (2) 2024.12.10