[java] 14. 다형성


다형성



이번 시간에는 상속과 함께 객체지향개념의 중요한 특징 중의 하나인 다형성에 대해서 배워보겠습니다.

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

자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였습니다.

다시 말하면, 부모클래스 타입의 참조변수로 자식클래스의 인스턴스를 참조할 수 있도록 한 것입니다.

예제를 들어보겠습니다.


class Tv{
	boolean power;
	int channel;
	
	void power() {power=!power;}
	void channelUp(){++channel;}
	void channelDown(){--channel;}
}


class CaptionTv extends Tv{
	String next;
	void caption(){/* 내용 생략 */}
}



Tv클래스와 CaptionTv 클래스가 위와 같이 정의 되어 있을 때,

Tv클래스와 CaptionTv 클래스는 상속관계에 있고,

이 두클래스의 인스턴스를 생성하고 사용하기 위해서는 다음과 같이 해야합니다.


Tv t=new Tv();

CationTv c=new CationTv();



지금까지 우리는 생성된 인스턴스를 다루기 위해서, 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했습니다.

즉, Tv인스턴스를 다루기 위해서는 Tv타입의 참조변수를 사용하고,

CaptionTv 인스턴스를 다루기 위해서는 CaptionTv타입의 참조변수를 사용했습니다.

하지만, Tv클래스와 CaptionTv클래스처럼 서로 상속관계에 있는 경우

부모클래스의 참조변수로 자식 클래스의 인스턴스를 참조하도록 하는 것도 가능합니다.


Tv t=new CationTv(); //부모 타입의 참조변수로 자식 인스턴스를 참조



그러면 같은 타입의 참조변수로 참조하는 것과 부모 타입의 참조변수로 참조하는 것은 어떤 차이가 있을까요?


CaptionTv c=new CaptionTv();

Tv t=new CaptionTv();



위의 코드에서 CaptionTv 인스턴스 2개를 생성하고,

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

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

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

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

즉, t.text 또는 t.caption() 와 같이 Tv클래스에 정의 되지 않은 멤버를 사용할 수 없습니다.

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

그렇다면 반대로 아래와 같이 자식타입의 참조변수로 부모타입의 인스턴스를 참조하는 것은 가능할까요?


CaptionTv c=new Tv();



그것은 불가능합니다!

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

주의하시길 바랍니다!

그렇다면, 인스턴스의 타입과 일치하는 참조변수를 사용하면 인스턴스의 멤버들을 모두 사용할 수 있을텐데

왜 부모타입의 참조변수를 사용해서 인스턴스의 일부 멤버만을 사용하도록 할까요?

이에 대한 답은 앞으로 배울 것입니다!



참조변수의 형변환



기본형 변수와 같이 참조 변수도 형변환이 가능합니다.

단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자식타입의 참조변수를 부모타입의 참조변수로,

부모 타입의 참조변수를 자식타입의 참조변수로의 형변환만 가능합니다.


자식타입->부모타입(Up-casting) :형변환 생략가능

부모타입->자식타입(Down-casting) :형변환 생략불가



참조변수간의 형변환 역시 캐스트 연산자를 사용하며, 괄호 ()안에 변환하고자 하는 타입의 클래스명을 적어주면 됩니다.

예제를 보면서 자세히 알아보겠습니다.


class Car{
	String color;
	int door;
	
	void drive(){ //운전하는 기능
		System.out.println("drive,Brrrr"); 
	}
	
	void stop(){
		System.out.println("stop!!!");
	}
}


class FireEngine extends Car{ //소방차
	void water(){ //물 뿌리는 기능
		System.out.println("water!!!");
	}
}


class Ambulance extends Car{
	void srien(){ //사이렌 울리는 기능
		System.out.println("siren~~~");
	}
}



Car 클래스는 FireEngine 클래스와 Ambulance 클래스의 부모입니다.

하지만 자바에서는 부모와 자식관계만 존재하기에 FireEngine 클래스와 Ambulance 클래스는 아무런 관계가 없습니다.

따라서 Car 타입의 참조변수와 FireEngine타입의 참조변수,

그리고 Car타입의 참조변수와 Ambulance 타입의 참조변수 간에 서로 형변환은 가능하지만,

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


FireEngine f;
Ambulance a;

a=(Ambulance)f; //에러

f=(FireEngine)a; //에러




Car car=null;
FireEngine fe=new FireEngine();
FireEngine fe2=null;


car=fe; //car=(Car)fe; 에서 형변환 생략됨. 업캐스팅

fe2=(FrieEngine)car; //형변환을 생략불가. 다운캐스팅



참고로 업캐스팅을 할 때 형변환을 생략할 수 있는 이유를 들어보겠습니다.

Car 타입의 참조변수 c가 있다고 가정합시다.

참조변수 c가 참조하고 있는 인스턴스는 아마도 Car인스턴스이거나 자식인 FireEngine 인스턴스 일것입니다.

Car 타입의 참조변수 c를 Car타입의 조상인 Object타입의 참조변수로 형변환 하는 것은

참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않습니다.

그래서 형변환을 생략할 수 있습니다.

하지만 다운 캐스팅에서 Car타입의 참조변수 c를 자식인 FireEngine타입으로 변환하는 것은

참조변수가 다룰 수 있는 멤버의 개수를 늘이는 것으로,

실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능이 있습니다.

그래서 자식타입으로의 형변환은 생략할 수 없으며,

형변환을 수행하기 전에 instanceof 연산자를 사용해서 참조변수가 첨조하고 있는

실제 인스턴스의 타입을 확인하는 것이 안전합니다.

예를 들어 Car 클래스의 참조변수 c가 FireEngine 클래스의 인스턴스를 가리키고 있다면

다운캐스팅이 가능하지만,

Car c=new FireEngine(); //참조변수 c에 FireEngine 인스턴스 저장

FireEngine fe=(FireEngine)c; //OK (다운 캐스팅)



Car 클래스의 참조변수 c가 Car 클래스의 인스턴스를 가리키고 있다면

다운캐스팅은 불가능합니다.

왜냐하면 자식클래스의 참조변수는 부모클래스의 인스턴스를 참조할 수 없기 때문입니다.


Car c=new Car(); //참조변수 c에 Car 인스턴스 저장

FireEngine fe=(FireEngine)c; //불가



형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에

참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않습니다.

단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 조절하는 것 뿐입니다.

다음은 업캐스팅과 다운캐스팅에 관한 예제입니다.


image



위의 예제에서 car=fe; 하면

참조변수 fe가 참조하고 있는 인스턴스를 참조변수 car가 참조하도록 하는 것입니다.

이 때 두 참조변수의 타입이 다르므로 참조변수 fe가 형변환되어야 하지만 생략되었습니다.

이제는 참조변수 car를 통해서도 FireEngine인스턴스를 사용할 수 있지만,

fe와 달리 car는 Car타입이므로 Car 클래스의 멤버가 아닌 water()는 사용할 수 없습니다.



그리고 fe2=(FireEngine)car; 하면

참조변수 car가 참조하고 있는 인스턴스를 참조변수 fe2가 참조하도록 합니다.

이 때 두 참조변수의 타입이 다르므로 참조변수 car를 형변환 하였습니다.

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

이제 참조변수 fe2를 통해서도 FireEngine 인스턴스를 사용할 수 있지만,

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

다음 예제는 컴파일은 성공하지만 실행시 에러가 발생합니다.


image



에러가 발생한 곳은 fe=(FireEngine)car 이며,

아까 말했듯이 부모타입의 인스턴스를 자식 타입의 참조변수로 참조하는 것은 허용되지 않기 때문입니다.

컴파일 시에는 참조변수간의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못합니다.

그래서 컴파일 시에는 문제가 없었지만, 실행시에는 에러가 발생하여 실행이 비정상적으로 종료가 된 것입니다.



instanceof 연산자



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

연산의 결과는 boolean 값인 true와 false 중의 하나를 반환합니다.


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




void doWork(Car c){
	if(c instanceof FireEngine){ //c가 참조하고 있는 인스턴스가 FireEnge 타입이면,
		FireEngine fe=(FireEngine)c; //형변환 가능
		fe.water();

	}else if(c instanceof Ambulance){//c가 참조하고 있는 인스턴스가 Ambulance 타입이면,
		Ambulance a=(Ambulance)c; //형변환 가능
		a.siren();	
	}
}



위의 코드는 Car타입의 참조변수 c를 매개변수로 하는 메서드입니다.

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

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

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

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

다음은 instanceof 연산자에 대한 예제입니다.


image



위의 예제를 보면 생성된 인스턴스는 FireEngine타입인데도,

Object타입과 Car타입의 instanceof 연산에서도 true를 결과로 얻었습니다.

그 이유는 FireEngine 클래스는 Object클래스와 Car클래스의 자손 클래스이므로 조상의 멤버들을 상속받았기 때문에,

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

요약하면, 실제 인스턴스와 같은 타입의 instanceof 연산 이외에 부모타입의 instanceof 연산에도 true를 결과로 얻으며,

instanceof 연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무런 문제가 없다는 뜻입니다.



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



부모 타입의 참조변수와 자손 타입의 참조변수의 차이점이 사용할 수 있는 멤버의 개수에 있다고 배웠습니다.

여기서 한 가지 더 알아야할 것이 있습니다.

부모클래스에 선언된 멤버변수와 같은 이름의 멤버변수를 자식 클래스에 중복으로 정의했을 때,

부모타입의 참조변수로 자식 인스턴스를 참조하는 경우와

자식타입의 참조변수로 자식 인스턴스를 참조하는 경우는 서로 다른 결과를 얻습니다.

메서드의 경우 부모 클래스의 메서드를 자식의 클래스에서 오버라이딩한 경우에도

참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드가 호출되지만,

멤버변수의 경우 참조변수의 타입에 따라 달라집니다.

결론부터 말하면, 멤버변수가 부모클래스와 자식클래스에 중복으로 정의된 경우,

부모타입의 참조변수를 사용했을 때는 부모 클래스에 선언된 멤버변수가 사용되고,

자식타입의 참조변수를 사용했을 때는 자식 클래스에 선언된 멤버변수가 사용됩니다.

다음 예제를 보시죠~


image



전에 배웠던 것과 같이 부모클래스의 멤버와 자식클래스의 멤버의 이름이 중복되었을때,

참조변수 super와 this를 사용해서 구별할 수 있습니다.


image



매개변수의 다형성



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

아래와 같이 Product, Tv, Computer, Audio, Buyer 클래스가 정의되어 있다고 합시다.



image



Product 클래스는 Tv, Audio, Computer 클래스의 부모이며,

Buyer 클래스는 제품을 구입하는 사람을 클래스로 표현한 것입니다.

이 때 Buyer클래스에 다음과 같이 물건을 구입하는 기능의 메서드를 추가해보겠습니다.


image



buy(Tv t)는 제품을 구입하면 제품을 구입한 사람이 가진 돈에서 제품의 가격을 빼고,

보너스점수는 추가하는 작업을 하도록 작성되었습니다.

그런데 buy(Tv t)로는 Tv밖에 살 수 없기 때문에 아래와 같이 다른 제품들도 구입할 수 있는 메서드가 추가로 필요합니다.


image



하지만, 이렇게 되면 제품의 종류가 늘어날 때마다 Buyer 클래스에는 새로운 buy메서드를 계속 추가해야합니다.

그러나 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있습니다.


image



매개변수가 Product타입의 참조변수라는 것은, 메서드의 매개변수로 Product 클래스의 자식타입의 참조변수면

어느 것이나 매개변수로 받아들일 수 있다는 뜻입니다.

그리고 Product 클래스에 price와 bonusPoint가 선언되어 있고

참조변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있기에 이와같이 할 수 있습니다.

다음은 매개변수 다형성을 이용한 예제입니다.


image


image


image



출력 결과 입니다.


image



여러 종류의 객체를 배열로 다루기



부모타입의 참조변수로 자식타입의 객체를 참조하는 것이 가능하므로,

Product 클래스가 Tv, Computer, Audio 클래스의 조상일 때, 다음과 같이 할 수 있다는 것을 이미 배웠습니다.


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



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


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



이처럼 부모타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를

배열로 묶어서 다룰 수 있습니다.

이러한 특징을 이용해서 아까 위에서 구현한 Buyer 클래스에 구입한 제품을 저장하기 위한

Product 배열을 추가해보겠습니다.


image



구입한 제품을 담기 위해 Buyer 클래스에 Product 배열인 item을 추가해주었고,

buy 메서드에 item[i++]=p; 문장을 추가함으로써 물건을 구입하면, 배열 item에 저장되도록 했습니다.

하지만 위의 예제처럼 저렇게 배열의 크기를 10으로 설정하면 11개 이상의 제품을 구입할 수 없는 것이 문제입니다.

그렇다고 배열의 크기를 무조건 크게 설정할 수만도 없습니다.

이 때 이용되는 것이 Vector클래스 입니다.



Vector 클래스





Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어서,

이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있습니다.

그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경 쓰지 않아도 됩니다.


public class Vector extends AbstractList
				implements List, Cloneable, java.io.Serializable{
	protected Object elementData[];
}




image




다음은 Vector 클래스를 사용하여 다시 구현한 예제입니다.


image


image


image


image



출력결과입니다.


image