다형성
객체지향개념에서 다형성이란 '여러가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
즉, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 했다는 뜻이다.
조상클래스 Tv가 있고, 이를 상속받는 자손클래스 CaptionTv가 있다고 했을 때, 평소에 이 두 클래스의 인스턴스를 생성하고 사용하기 위해서는
Tv c = new Tv(); 또는 CaptionTv c = new CaptionTv();
위와 같은 방식으로 인스턴스의 타입과 일치하는 타입의 참조변수만 사용했다. 이처럼 인스턴스의 타입과 참조변수의 타입이 일치하도록 하는 것이 보통이지만 위의 두 클래스처럼 서로가 상속관계에 있을 경우 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조하는 것도 가능하다.
Tv t = new CaptionTv(); /조상클래스 타입의 참조변수로 자손클래스 타입의 인스턴스 참조
인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것에는 어떤 차이가 있는지 알아보도록 하겠다.
CaptionTv c = new CaptionTv(); / 인스턴스를 같은 타입의 참조변수로 참조
Tv t = new CaptionTv(); / 인스턴스를 조상타입의 참조변수로 참조
위의 경우에서 t는 인스턴스가 CaptionTv타입이지만, CaptionTv인스턴스의 모든 멤버를 사용할 수는 없다. Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들만 사용할 수 있으며, CaptionTv클래스에는 있지만 Tv클래스에는 정의되지 않은 멤버들의 사용은 불가능하다.
참조변수의 형변환
참조변수의 형 변환은 기본형 변수와 달리 서로 상속관계에 있는 클래스 사이에서만 가능하다.
자손타입 -> 조상타입(업캐스팅) : 형변환 생략 가능
조상타입 -> 자손타입(다운캐스팅) : 형변환 생략 불가
참조변수간의 형변환도 기본형 변수의 변환처럼 캐스트연산자'( )'를 사용하며, 괄호 안에 반환하고자하는 타입의 이름(클래스 명)을 적어주면 된다.
형변환을 생략할 수 있는 경우
Car타입의 참조변수 c가 있다고 가정했을 때, c가 참조하고 있는 인스턴스는 Car인스턴스이거나 자손클래스인 FireEngine의 인스턴스일 것이다.
Car타입의 참조변수 c를 Car타입의 조상인 Object타입의 참조변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이므로 문제가 되지 않아 형변환을 생략할 수 있다.
형변환을 생략할 수 없는 경우
Car타입의 참조변수 c를 자손인 FireEngine타입으로 변환하는 것은 참조변수가 다룰 수 있는 멤버의 개수를 늘리는 것이므로, 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다.
따라서 자손타입으로의 형변환은 생략할 수 없으며, 형변환 수행 이전에 instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.
*
만약 조상클래스인 Car의 인스턴스를 참조하고 있는 car참조변수가 자손클래스인 FireEngine의 참조변수 fe를 참조하도록 fe=(FireEngine)car로 다운캐스팅한다면 에러가 발생한다.
겉보기에는 조상타입의 참조변수를 다운캐스팅하여 자손클래스의 참조변수로 형변환한 것으로 문제가 없어보인다.
하지만 car이 참조하고 있는 인스턴스는 조상클래스 Car타입이고, 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다.
- 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행 가능
- 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않음
- 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요
instanceof 연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를사용한다.
주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수가 위치하고, 오른쪽에는 타입(클래스명)이 위치한다. 그리고 연산의 결과로는 true와 false를 반환하며, 연산결과로 true가 나온다면 참조변수가 검사한 타입으로 형변환이 가능하다는 뜻이다.
if(c instanceof FireEngine){ }
생성된 인스턴스는 FireEngine 타입인데도 Car과 Object타입의 instanceof연산에서 true를 얻은 것은 그들의 자손클래스여서 위 인스턴스들의 멤버를 모두 가지고 있기 때문이다.
즉, 실제 인스턴스와 같은 타입의 instanceof 연산 외에 조상타입의 instanceof연산에도 true결과를 얻으며, 연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무 문제가 없다는 것.
참조변수와 인스턴스의 연결
위 예제를 보면 타입은 다르지만 참조변수 p와 c 모두 Child 인스턴스를 참조하고 있다. 그리고 Parent와 Child클래스는 서로 다른 멤버들을 정의하고 있다.
이때 위에서 확인한 것 처럼 조상타입의 참조변수 p와 자손타입의 참조변수 c로 Child인스턴스 멤버들을 사용하는 것의 차이를 알 수 있다.
메소드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child클래스에 정의된 메소드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.
참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우 뿐이다.
매개변수의 다형성
int price와 int bonusPoint라는 멤버를 가지는 Product라는 조상클래스가 있고, 그 아래에 Buyer, Tv, Computer, Audio의 자손 클래스가 있다. 여기서 Buyer클래스는 물건을 사는 사람을 표현하는 클래스로 안에는 구매 기능의 메소드가 들어가게 된다.
구매할 대상이 필요하기 때문에 void buy(Tv t)와 같이 매개변수를 가지는 메소드를 생성하게된다. 하지만 이처럼 Tv타입으로 매개변수를 생성하면 Tv 외의 구매품에 대해서는 구매기능을 사용할 수 없기 때문에 Computer c, Audio a와 같은 매개변수를 가지는 메소드를 또 만들어줘야 한다.
물건이 계속 늘어난다면 그에 맞는 매개변수를 가지는 buy메소드를 계속 생성해야하는 번거로움이 생기게 된다. 이러한 번거로움은 매개변수의 다형성을 적용해서 아래와 같이 하나의 메소드로 간단하게 처리 가능하다.
class Buyer extends Product{
void buy(Product p){
money = money - p.price; //수중에 남은 돈= 수중의 돈 - 물건의 가격
bonusPoint = bonusPoint + p.bonusPoint; //보너스 포인트= 기존 보너스 포인트+제품 구매로 생 긴 보너스포인트
}
}
매개변수가 Product타입의 참조변수라는 것은 메소드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받을 수 있다는 뜻이다.
또한 Product클래스에 price, bonusPoint가 선언되어 있기 때문에 참조변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있다.
Buyer b = new Buyer();
Tv t = new Tv();
b.buy(t);
Tv클래스는 Product클래스의 자손이기 때문에 위 코드와 같이 buy(Product p)메소드에 매개변수로 t의 인스턴스를 제공하는 것이 가능하다.
위 예제는 고객(Buyer)이 buy(Product p)메소드를 이용해서 Tv와 Computer를 구입하고, 고객의 잔고와 보너스 점수를출력하는 예제다.
여러 종류의 객체를 배열로 다루기
조상타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로, 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();
이렇게 조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
또는 묶어서 다루고 싶은 객체들의 상속관계를 따져 가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성해서 객체들을 저장하면 된다.
먼저 이렇게 조상클래스 Product와 그 자손클래스들 Tv, Computer, Audio를 생성했다.
이제 이들을 사용해서 Buyer클래스와 메인 메소드를 작성할 것이다.
Vector 클래스
위의 예제에서 Product배열로 제품들을 저장했지만 배열의 크기에 제약을 받게된다. 이러한 어려움을 해결할 수 있는게 Vector클래스다.
Vector클래스에는 내부적으로 Object타입의 배열을 가지고 있어서 이 배열에 객체를 추가하거나 제거할 수 있도록 작성되어 있으며, 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스 개수를 신경쓰지 않아도 된다.
Vector() : 10개의 객체를 저장할 수 있는 Vector인스턴스 생성, 10개 이상 인스턴스 저장시 자동으로 크기 증가
boolean add(Object o) : Vector에 객체 추가. 성공하면 true, 실패하면 false값 리턴
boolean remove(Object o) : Vector에 저장되어 있는 객체 제거. 성공하면 true, 실패하면 false값 리턴
boolean isEmpty(Object o) : Vector가 비어있는지 검사. 비어있으면 true, 비어있지 않으면 false값 리턴
Object get(int index) : 저장된 위치의 객체 반환(반환타입이 Object이므로 형변환 필요)
int size() : Vector에 저장된 객체의 개수 반환
위의 Product배열을 Vector클래스를 사용해서 대체한 예제
remove메소드를 사용해서 반품 기능을 넣을 수도 있음