A는 B를 상속했다.
그렇다면 A는 A의 기능 + B의 기능을 이용할 수 있다.
그럼 난 앞으로 B는 무시하고, A만 사용하면 핵이득이다.
하지만, 상속 관계는 이렇게 원하는 대로 사용되지 않는다.
그리고 바로 이 점이 객체 지향의 꽃인 다형성을 제대로 이용하는 것이다.
그 전에 상속을 아직 이해하지 못했다면 다음을 참고해보자.

class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Parent parent = new Child(); // ** 업캐스팅 **
}
부모 객체를 생성하는데,
부모 변수가 자식 인스턴스를 참조한다.
아니, 부모 기능을 상속 받는 자식 객체를 만들었는데 왜 저렇게 사용하는거야?
다형성을 이해하려면, 바로 이 업캐스팅을 이해해야 한다.
public void method extendMethod() {
Parent parent = new Child();
}
상위 타입 변수에 하위 타입의 인스턴스를 참조하게 하는 것.
하위 인스턴스를 높여서(업) 참조시킨다(캐스팅).
이것이 바로 업캐스팅이다.

class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Child child = new Child();
Parent parent = (Parent) child; // ** (Parent) 생략 가능
Parent parent = child; // 가능
}
상속 과정에서 자식 객체는
반드시 부모의 생성자를 필요로 하는데
이 말은 자식은 객체가 만들어질 때 부모가 함께 만들어진다는 것과 같다.
그렇기 때문에 이미 부모의 정보를 가지고 있는 상태에서
부모의 타입으로 업캐스팅 하는 것은 전혀 문제가 되지 않는다.
자바는 그래서 업캐스팅이 생략 가능하도록 설계했다.
이렇게 부모가 자신 뿐만 아니라 자식 타입까지 참조할 수 있는 것을 다형적 참조라고 한다.

class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Parent parent = new Child(); // 업캐스팅
parent.childMethod(); // 불가
}
상속에서 자식 객체에서 메서드를 실행할 경우,
먼저 자식 객체를 조회하고 자식 객체에 메서드가 없다면
자식 객체를 생성하면서 생성한 부모 객체를 조회한다.
그렇다면 이 업캐스팅 코드에서, childMethod()를 실행하면 어떻게 될까?
부모 객체는 자식 객체와 달리 자기 자신만 조회 가능하고 자식 객체는 조회가 불가능하다.
그러면 어떻게 해야 자식 메서드를 수행할 수 있을까?

class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Parent parent = new Child();
Child child = (Child) parent; // ** 다운 캐스팅 **
}
자식 메서드를 수행하는 방법이 다운캐스팅이다.
하위 타입 변수에 상위 타입의 인스턴스를 참조하게 하는 것.
상위 인스턴스를 낮춰서(다운) 참조시킨다(캐스팅).
이것이 바로 다운캐스팅이다.
Parent parent = new Child();
Child child = (Child) parent; // ** 다운 캐스팅 **
parent는 Child 인스턴스를 참조하므로
parent 내부에는 Child 인스턴스에 대한 정보가 존재한다.
다만 Parent라는 부모 타입으로 선언함으로써 접근을 제한했을 뿐이다.
따라서 다운 캐스팅이 가능하다.

그럼 다운 캐스팅은 왜 쓰는걸까?
다형성을 활용하기 위해 업캐스팅을 수행한 이후,
어쩔 수 없이 자식 메서드의 접근하기 위한 일종의 보험 장치로써 존재한다.
다형성을 활용하더라도 뒤에 설명하는
오버라이딩 메서드 외 다른 메서드에 접근하기 위한 장치가 필요하기 때문이다.

업캐스팅은 생략이 가능할 정도로 안전했다.
하지만 다운 캐스팅은 위험하다. 왜?
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Parent parent = new Parent();
Child child = (Child) parent; // ** 런타임 오류 **
child.childMethod(); // 불가
}
Java는 다형성 지원을 위해 실제 객체의 타입을 런타임에 결정한다.
컴파일 시점에는 참조 변수의 타입만을 확인한다.
따라서 프로그램은 그대로 실행되는데 실행 도중,
실제 객체에 자식 객체의 정보가 없기 때문에 ClassCastException이 발생한다.
런타임 오류는 매우 위험하므로 다운 캐스팅은 신중하게 사용해야 한다.

Parent parent = new Child();
Child child = (Child) parent; // ** 다운 캐스팅 **
child.childMethod();
위 처럼 다운 캐스팅을 할 때마다 자식 객체를 생성하는 것은 매우 불편하다.
Parent parent = new Child();
((Child) parent).childMethod(); // ** 일시적 다운 캐스팅 **
이렇게 일시적 다운 캐스팅을 통하여 코드를 간소화할 수 있다.

class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extend Parent{
public void childMethod() {
// 메서드 바디
}
}
public void method extendMethod() {
Parent parent = new Child(); // 업캐스팅
Child child = (Child) parent; // ** 다운 캐스팅 **
// parent.childMethod(); 불가
child.childMethod(); // ** 가능 **
}
다운 캐스팅을 함으로써
자식 객체의 메서드를 다시 사용할 수 있게 됐다.
아니, 처음부터 업캐스팅이고 다운캐스팅이고 하지 말고
그냥 부모로부터 상속받은 자식 객체 타입을 생성하면 됐잖아.
왜 생고생을 하는거야?

그렇다.
그냥 시작부터 자식 객체를 생성하면,
자식 객체에서 부모 객체의 기능까지 몽땅 사용이 가능하다.
하지만, 우리는 지금부터 소개하는 오버라이딩 기능을 통하여
다형성을 이용하기 위해서
굳이 부모 타입으로 자식 인스턴스를 참조시키는 업캐스팅을 하려고 하는 것이다.
class Smartphone {
public void call() {
// 전화 기능
}
}
class SktSmartphone extends Smartphone {
@Override
public void call() {
// Skt 전화 기능
}
}
class UplusSmartphone extends Smartphone {
@Override
public void call() {
// Uplus 전화 기능
}
}
스마트폰이라는 부모 클래스가 있다.
Skt, U+ 클래스가 각각 스마트폰 부모 클래스를 상속하고
전화 기능인 call() 메서드를 오버라이딩한다.
그리고 U+ 유저인 나는, U+ 클래스를 참조하여 사용한다.
public void smartphoneMethod() {
Smartphone smartphone = new UplusSmartphone();
smartphone.call();
}

또, 또. 또 업캐스팅 한다. 그냥 유플러스 타입을 만들라구요.
진정하고 계속해서 살펴보자.
우리는 이전에 업캐스팅의 사례에서
부모 객체에 메서드가 없는 경우
자식 객체를 조회하지 못하고 예외가 발생한다고 알아봤다.
그렇다면 부모의 메서드를
자식이 오버라이딩한 위 스마트폰 코드는 어떻게 될까?
부모 타입이니 부모 객체를 조회하고
부모 객체에 call() 메서드가 있으니 부모의 call()이 실행될까?
아니다. 이전과는 달리
call() 메서드를 오버라이딩한 자식 객체의 메서드가 실행된다.
..어쩌라고?

부모 타입인데도 불구하고
오버라이딩한 메서드가 우선 실행되는 것의 파급력을 알아보기 위해
오버라이딩과 업캐스팅이 없는 순수한 세상에 돌아가보자.
각 통신사의 전화 기능은 달라서 하나의 부모로부터 상속 받는 것은 불가능하다고 가정한다.
class SktSmartphone {
public void call() {
// Skt 전화 기능
}
}
class UplusSmartphone {
public void call() {
// Uplus 전화 기능
}
}
public void smartphoneMethod() {
// Skt 전화를 이용하는 경우
SktSmartphone sktSmartphone = new SktSmartphone();
sktSmartphone.call();
// U+ 전화를 이용하는 경우
UplusSmartphone uplusSmartphone = new UplusSmartphone();
uplusSmartphone.call();
}
각 통신사의 기능을 이용해야 하는 경우 해당 통신사의 객체를 생성하고,
각 객체의 메서드를 추가로 또 실행해주어야 한다.
만약 통신사가 100개라고 가정한다면 총 100개의 객체를 생성하고
각 통신사의 100개의 메서드를 실행해야 한다.
업캐스팅과 오버라이딩이 없으니, 간소화할 수 있는 방법은 없다.

class Smartphone {
public void call() {
// 전화 기능
}
}
class SktSmartphone extends Smartphone {
@Override
public void call() {
// Skt 전화 기능
}
}
class UplusSmartphone extends Smartphone {
@Override
public void call() {
// Uplus 전화 기능
}
}
public void smartphoneMethod() {
SktSmartphone sktSmartphone = new SktSmartphone();
UplusSmartphone uplusSmartphone = new UplusSmartphone();
// ** 업캐스팅으로 배열 생성 가능 **
Smartphone[] smartphones = {sktSmartphone, uplusSmartphone};
// ** 배열을 활용한 코드 간소화 가능 **
for (smartphone : smartphones) {
smartphoneCall(smartphone);
}
}
private void smartphoneCall(Smartphone smartphone) {
// ** Smartphone 부모 객체로 업캐스팅 **
smartphone.call(); // ** 업캐스팅 됐지만 오버라이딩한 자식 메서드 실행 **
}
만약 통신사가 100개였다면 각 통신사의 전화 메서드를 100개를 직접 수행해줬어야 했다.
하지만, 상속을 통한 오버라이딩, 그리고 업캐스팅을 통하여 그룹화가 가능해졌고
for을 활용하여 코드 간소화까지 이어질 수 있게 됐다.
이것이 바로 다형성의 힘이다.

부모 클래스를 상속한 후 업캐스팅으로 굳이 자식의 기능을 사용하지 못하게 제한했었다.
하지만 이제 우리는 더 이상 그것이 쓸모 없는 구조라 생각하지 않는다.
오버라이딩과 함께 다형성을 알게 됐으니까.
업캐스팅을 통하여 다형성을 만족하도록 설계했고,
그것을 바탕으로 코드를 간소화하면서 활용할 수 있었다.
그리고 다형성을 이유로 쓰지 못했던
자식 객체의 메서드도 사용하기 위해 다운 캐스팅까지 알아봤다.
이 복잡한 과정을 통해서 조금이나마 객체 지향과 다형성에 대해 이해할 수 있겠다.