본문 바로가기

프로그래밍/JAVA

[JAVA] 다형성(polymorphism)

JAVA의 정석(2nd Edition) (남궁 성 著) 265p~287p 를 참조해 코드를 작성했으며


개인적인 공부 내용을 적은 것이므로 오류가 있을 수 있습니다.''


0. 들어가기에 앞서


다형성은 상속과 깊은 관계가 있으므로, 충분히 해당 내용이 뭔지 숙지해야 한다.

(링크 : http://whatisthenext.tistory.com/34)

 

다형성 : 하나의 메소드나 or 하나의 클래스가 다양한 방법으로 동작하는 것을 의미한다.


1. 다형성


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

자바에서는 이를 한 타입의 참조변수로 여러 타입의 객체를 "참조"할 수 있도록 지원한다.


[ 예 제 1 ]


A타입의 B인스턴스가 생성이 된 상황.

클래스 B를 클래스 A의 데이터타입으로 인스턴스화 (new) 했을 때 

클래스 A에 존재하는 멤버만이 (여기서는 메서드 x) 클래스 B의 멤버가 된다.


obj.x의 메서드소 재는 부모클래스A이므로 실행이 됨.

그러나 obj.y는 부모클래스A에 없으므로 오류를 발생.

그런데 A클래스의 x메서드를 B클래스에서 재정의(오버라이딩)한다면 출력값은 재정의된 x가 출력이 된다.

클래스 B를 클래스 A의 데이터타입으로 인스턴스화 (new) 했을 때 클래스 A에 존재하는 멤버만이 (여기서는 메서드 x) 클래스 B의 멤버가 된다. 동시에 클래스 B에서 오버라이딩한 멤버 ( 메서드 x )는 그대로 동작한다.


이번에는 B2를 또 상속시키고 obj2.x의 메서드를 실행시키면

부모클래스 A의 오버라이딩 된 메서드 x를 실행시킨다. (즉 "B2.x"가 출력이 됨)


[ 예 제 2 ]


상속을 이용해서 클래스 TV와 클래스 CaptionTV는 서로 상속관계가 되었다.

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

위 방식은 우리의 전통적인 방식이다. 

Tv 인스턴스를 다루기 위해서 Tv 타입의 참조변수(t)를 이용하고

CaptionTv 인스턴스를 다루기 위해서 CaptionTv 타입의 참조변수(c)를 사용했다.

Tv t = new CaptionTv();

하지만 위에처럼 조상 클래스 타입의 참조변수(Tv t) 자손 클래스의 인스턴스를 참조하는 것이 가능!!



CaptionTv c = new CaptionTv(); CaptionTv 인스턴스 생성
Tv t = new CaptionTv(); // CaptionTv 인스턴스 생성 (Tv타입의 t가 참조변수)

<출처 : 자바의 정석 (남궁성 저)>


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

Tv 타입의 참조변수(t)로 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(power, channel 등)만 사용가능. 자손 클래스의 멤버인 text와 catpion()은 사용불가하다.


2.  CaptionTv c = new Tv ( ) ; 은 허용되지 않는다.


자손타입의 참조변수(c)로 조상타입의 인스턴스(t)를 참조하는 것은 존재하지 않는 멤버를 사용하고자

할 가능성이 있으므로 허용하지 않는다.

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


3. 그렇다면 왜 제한된 멤버만 사용할 수 있으면서 왜 그런 짓을 할까?


[ 인터페이스와 다형성 ]


어떠한 클래스 (C)가 어떠한 인터페이스 (I)를 구현하고 있다면  클래스의 인스턴스는 데이터 타입이 인터페이스일 수 있다. (클래스 C가 인터페이스 I를 구현하고 있기 때문에) 




인터페이스 I1는 A메서드를 정의하도록 강제함.

인터페이스 I2는 B메서드를 정의하도록 강제함.

클래스 A에서는 I1, I2의 메서드 A, B를 구현하고 있음.


obj는 데이터타입이 A이기 때문에 모든 멤버를 사용할 수 있음


obj I1는 데이터타입이 I1이기 때문에 메서드 A만을 가지고 있는 클래스인 것처럼 동작하게 된다.

obj I1.B()는 I2에서 정의하고 있기 때문에 에러를 발생한다.


obj I2는 데이터타입이  I2이기 떄문에 메서드 B만을 가지고 있는 클래스인 것처럼 동작하게 된다.

obj I2.A()는 에러를 발생하지만 objI2.B()는 에러를 발생시키지 않는다.


이렇게 하는 이유는 사용자가 불필요한 기능을 제공하는 걸 방지하기 위해서이다.

인스턴스 obj I2의 데이터 타입을 I2로 한다는 것은 인스턴스를 외부에서 제어할 수 있는 조작 장치를 I2의 멤버로 제한한다는 의미가 된다.




2. 참조변수의 형변환


서로 상속관계에 있는 클래스사이에서 참조변수 형변환이 가능하다.


자손타입 → 조상타입 (Up-casting) : 형변환 생략가능

자손타입 ← 조상타입 (Down-casting) : 형변환 생략불가 

위 상속관계를 도식으로 나타내보면 아래와 같은 그림이 된다.


Car car = null;
FIreCar f2 = new FireCar();
FireCar fe2 = null;

car = fe ; (자손타입 -> 조상타입) 생략가능

(자손타입) (상타입)


fe2 = (FireEngine) car; (조상타입 <- 자손타입) 생략불가

(조상타입) (자손타입)


헷갈리지 말아야 할 것이

car = fe는 자손타입(fe) 을 조상타입(car)를 대입! 하는 것이다.

fe2 = car는 조상타입(car)을 자손타입(fe2)에 대입! 하는 것이다. 

즉, B라는 값을 A에 대입한다는 느낌을 가져야 한다.



위에서도 언급했듯이, 조상타입의 참조변수(car)가 자손타입(FireEngine)으로 변환하는 것은

참조변수(car)가 다룰 수 있는 멤버의 갯수를 "늘리는 것"이다. 따라서 실제 인스턴스 멤버 개수보다

참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 수 있다.

그래서 자손타입으로의 형변환은 생략할 수 없다. 


3. Instanceof 연산자


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


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

연산의 결과로 boolean (true, flase) 중의 하나를 반환한다.


true : 참조변수(c)가 검사한 타입(FireCar 또는 Ambulance)으로 형변환이 가능하다는 것을 뜻한다.

false : 참조변수(c)가 검사한 타입(FireCar 또는 Ambulance)으로 형변환이 불가능하다는 것을 뜻한다.


4. 참조변수와 인스턴스의 연결


<메서드의 경우 >

조상클래스의 메서드를 자손클래스에서 오버라이딩하는 경우에는

참조변수 타입에 상관없이 항상 오버라이딩 된 메서드가 호출되지만


<조상, 자손 클래스에 중복으로 정의된 멤버변수의 경우>

조상타입의 참조변수 사용 => 조상 클래스에 선언된 멤버변수 사용

자손타입의 참조변수 사용 => 자손 클래스에 선언된 멤버변수 사용


멤버변수가 조상 클래스와 조상 클래스에서 중복으로 정의되어 있는 경우를 살펴보자



Parent(조상)과 Child(자손)에서 멤버 변수의 이름이 x로 똑같다. (중복정의)

이 경우 조상타입의 참조변수 → 조상 클래스에 선언된 멤버변수를 사용

  자손타입의 참조변수 → 자손 클래스에 선언된 멤버변수를 사용




1. 조상의 참조변수(p), 자손의 참조변수(c)는 자손(child)의 인스턴스를 가리키고 있음

2.  p.x는 조상 클래스의 멤버변수(p.x = 100), c.x는 자손 클래스의 멤버변수(c.x = 200)를 가리킨다.

3. 그러나, p.method는 자손 클래스의 메서드를 호출한다.



위 예제와 다르게 자손클래스(Child) 내용을 지워버렸다.

그다음 결과값을 살펴보면 조상클래스의 메서드와 멤버변수를 사용한다.


ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

 p.x = 

c.x =

 x =

x = 

super.x =  

super.x =  

this.x 

this.x 

위 예제를 보고 이 결과값들을 적어보자.

자손클래스 Child에 선언된 인스턴스변수 x와 조상 클래스 Parent로부터 상속받은 인스턴스 변수 x를

구분하는데 참조변수 super와 this가 사용된다.


즉, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있다.


위 문제의 정답은 아래와 같다.

 p.x = 100

c.x = 200

 x = 200

x = 200

super.x = 100 

super.x = 100

this.x = 200

this.x = 200



5. 매개변수의 다형성


참조변수의 다형적인 특징은 메서드의 "매개변수"에도 적용된다.



이런 식으로 코드를 작성한다면 제품의 종류가 늘어날 때마다 

Buyer클래스에는 새로운 buy 메서드를 추가해주어야 할 것이다.



매개변수의 다형성을 적용하면 이렇게 간단한 메서드로 처리할 수 있다.



Tv와 Computer가 상속을 받았으니 buy(Product p) 메서드에 매개변수로 

TV인스턴스와 Computer 인스턴스를 제공하는 것이 가능하다.


6. 여러 종류의 객체를 하나의 배열로 다루기


Product p1 = new TV();
Product p2 = new Computer();
Product p3 = new Audio();

Prodcut 클래스가 TV, Computer, Audio클래스의 조상일 때

위의 코드를 Product 타입의 참조변수 배열로 처리하면 아래와 같다.

Product p[] = new Product[3];
p[0] = new TV();
p[1] = new Computer();
p[2] = new Audio();


조상타입의 참조변수(Product) 배열을 사용하면

공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.


7. 내가 헷갈리는 부분


1.

class CastingTest2{
public static void main(String args[]){
Car car = new Car(); // 참조변수 car가 참조하고 있는 인스턴스가 Car타입의 인스턴스
Car car2 = null;
FireCar fe = null;

car.drive();
fe = (FireCar)car;
fe.drive();
car2 = fe;
car2.drive();
}
}

fe = (FireCar) car;가 컴파일에러가 발생하는 이유는

Car car(조상타입의 참조변수)를 자손타입의 참조변수(FireCar fe)로 형변환 한 것이기 떄문에 

문제가 없어보이지만, 문제는 참조변수 car가 참조하고 있는 인스턴스가 Car타입의 인스턴스라는 데 있다.


즉, 조상타입의 인스턴스를 자손타입의 참조변수(fe)로 참조하는 것이 허용되지 않는다.

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



2. 자손타입의 참조변수로 조상타입의 인스턴스를 왜 참조할 수 없을까?


CaptionTv c = new CaptionTv();

Tv t = new CaptionTv();

위 코드와 아래 코드를 구분할 줄 알아야 한다.

위 코드에서 t(조상) 참조변수를 이용해 CaptionTv의 멤버를 사용할 수 있다. (단, text, caption을 제외하고)

즉, 조상타입의 참조변수로 자손타입의 인스턴스 사용이 가능하다!


CaptionTv c = new Tv(); 

그러나 자손타입의 참조변수(c)로 조상타입의 인스턴스를 참조하는 것은 안된다.

=> 인스턴스 TV의 멤버 개수(5개)보다 참조변수 c가 사용할 수 있는 멤버 개수(7개)가 더 많기 때문.


대신 "형변환"을 하면 가능하다.

Person person = new Person(); // 조상 person
Player "오승환" = new Player(); // 자손 오승환

Person = 오승환; 조상 = 자손 ( 자손 → 조상 ) 형변환 생략 가능
오승환 = (Player) Person; 자손 = 조상 ( 조상 → 자손 ) 형변환 생략할 수 없음



3. 조상타입의 참조변수를 사용해서 인스턴스의 일부 멤버만 사용할 수 있는데

왜 굳이 이렇게 해야할까?