JAVA/Basic

[JAVA-basic] 다형성

쏭식 2021. 8. 24. 09:30

다형성

객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미한다.

좀 더 구체적으로, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.

 

다음 예시를 살펴보자.

class Tv{     // 조상
    boolean power;  
    int channel;
    void power() { power = !power; }
    void channelUp() { ++channel; }
    void channelDown() { --channel; }
}
class CaptionTv extends Tv{     // 자손
    String text;
    void caption() { }
}

원래 이 두 클래스의 인스턴스를 생성한다고 하면,

Tv t = new Tv() ;

CaptionTv c = new CaptionTv() ;

지금까지 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다.

 

이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만,

Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우,

다음과 같이 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조하도록 하는 것도 가능하다.

Tv t = new CaptionTv() ;

 

그러면 서로 타입이 일치하는 경우와 일치하지 않은 경우하고 무슨 차이가 있는지 살펴보자.

CaptionTv c = new CaptionTv() ;
Tv t = new CaptionTv() ;

참조변수 c와 t가 생성된 인스턴스를 하나씩 참조하도록 하였다.

이 경우 실제 인스턴스가 CaptionTv타입이라 할지라도,

참조변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다.

 

Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들만 사용할 수 있다.

따라서 생성된 CaptionTv인스턴스의 멤버중에서 Tv클래스에 정의 되지 않은 멤버,

text와 caption()은 참조변수 t로 사용이 불가능하다.

 

둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

 

여기서 주의할 점이 있다.

만일 반대로 CaptionTv c = new Tv() ; 이렇게 했다면 이것은 에러가 발생한다.

그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.

참조변수 c로는 text와 caption()이 정의되어 있으므로 사용하려 할 수 있다.

 

하지만 c가 참조하고 있는 인스턴스는 Tv타입이고, Tv타입의 인스턴스에는

text와 caption()이 존재하지 않기 때문에 이들을 사용하려 하면 문제가 발생한다.

 

그래서 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은

존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는 것이다.

참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

참조변수의 형변환

기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스사이에서만 가능하기 때문에

자손타입의 참조변수를 조상타입의 참조변수로,

조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능하다.

 

자손타입에서 조상타입으로 형변환하는 것(업캐스팅)은 생략이 가능하나,

조상타입에서 자손타입으로 형변환하는 것(다운캐스팅)은 생략이 불가능하다.

 

형변환의 예시를 살펴보자.

class Car{    }
class FireEngine extends Car{   }
class Ambulance extends Car{   }

Car클래스는 FireEngine과 Ambulance클래스의 조상이다.

Car타입의 참조변수와 FireEngine타입의 참조변수 그리고

Car타입의 참조변수와 Ambulance타입의 참조변수 간에는 서로 형변환이 가능하지만,

FireEngine타입의 참조변수와 Ambulanc타입의 참조변수 간에는 서로 형변환이 가능하지 않다.

그 이유는 서로 상속관계가 아니기 때문이다.

 

 

 

이어서 예시를 살펴보자.

Car car = null;
FireEngine fe = new FireEngine() ;
FireEngine fe2 = null;
car = fe;    //car = (Car)fe ; 에서 형변환 생략됨. 업캐스팅
fe2 = (FireEngine)Car ; // 형변환을 생략 불가. 다운캐스팅

Car타입의 참조변수 c를 Car타입의 조상인 Object타입의 참조변수로 형변환을 한다고 가정하면,

참조변수로 다룰 수 있는 멤버의 개수가 실제 가지고 있는 것보다 멤버의 개수가 더 적을 것이므로

형변환을 생략할 수 있지만,

자손인 FireEngine타입으로 형변환 하는 것은 다룰 수 있는 멤버의 개수를 늘리는 것이므로

명시적으로 형변환이 이루어져야만 한다.

 

조상클래스 Car
자손클래스 FireEngine
메인 클래스

메인 클래스 중간에 car.water(); 에서 에러가 발생한다. 

왜냐하면 car이 지금 참조하고 있는 인스턴스는 FireEngine이지만 실제 타입은 Car이기 때문에

Car클래스의 멤버가 아닌 water()는 사용할 수 없다.

 

따라서 밑에 fe2 = (FireEngine)car ; 를 통해 water를 사용할 수 있게 해주었다.

car에는 FireEngine인스턴스의 주소가 저장되어 있으므로 fe2에도 FireEngine인스턴스의 주소가 저장된다.

car와는 달리, fe2는 FireEngine타입이므로 FireEngine인스턴스의 모든 멤버들을 사용할 수 있다.

 

여기서 주의할 점이 있는데 다음 예시를 살펴보자.

에러발생

fe = (FireEngine)car; 에서 컴파일은 되었지만 실행시 에러가 발생하였다.

연산자를 이용해서 조상타입의 참조변수를 자손타입의 참조변수로 형변환한 것이기 때문에 문제가 없어 보이지만,

문제는 참조변수 car가 참조하고 있는 인스턴스가 Car타입의 인스턴스라는데 있다.

전에 배운 것 처럼 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다.

 

위의 예제에서 Car car = new FireEngine(); 과 같이 변경하면 에러가 발생하지 않을 것이다.

여기서 알 수 있는 점은 다음과 같다.

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 
참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다. 

instanceof연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.

주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.

 

instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.

다음 예시를 살펴보자.

instanceof연산자

이 메소드가 호출될 때, 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만,

메소드 내에서는 정확히 어떤 인스턴스인지 알 길이 없다.

따라서 instanceof연산자를 이용해서 참조변수 c가 가리키는 인스턴스의 타입을 체크하고,

적절히 형변환한 다음에 작업을 해야 한다.

 

조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기 때문에,

실제 인스턴스와 같은 타입의 참조변수로 형변환을 해야만 인스턴스의 모든 멤버들을 사용할 수 있다.

 

위의 예시를 봐보자.

참조변수 car가 가리키는 인스턴스 타입은 Car클래스이다.

따라서 c instanceof FireEngine에서 false라는 값을 반환한다.

참조변수가 가리키는 인스턴스의 자손타입으로 형변환을 허용하지 않기 때문이다.

또한, FireEngine fe = (FireEngine)c ; 도 성립될 수 없다.

그 이유는 조상타입의 인스턴스를 자손타입의 참조변수로 참조하려고 하고 있기 때문이다.

 

또 다른 예시를 살펴보자.

instanceof메소드(2)

참조변수 fe가 조상클래스인 Car로 형변환은 가능해서 if문 안에있는 실행문들은 모두 실행이 된다.

왜냐하면 FireEngine클래스는 Car클래스의 자손 클래스이므로 조상의 멤버들을 상속받았기 때문에 

FireEngine인스턴스는 Car인스턴스를 포함하고 있는 셈이기 때문이다.

 

하지만, "hello"는 출력이 되지만 12번째 줄에서 에러가 발생한다.

왜냐하면 앞서 설명한 것과 같이 참조변수가 가리키는 인스턴스의 자손타입으로 형변환을 시도하였기 때문이다.

또한, 자손타입의 참조변수로 조상타입의 인스턴스를 참조하려고 하였기 때문이다.