객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 구체적으로는 부모클래스 타입의 참조변수로 자식클래스의 인스턴스를 참조할 수 있도록 하였다는 의미이다.
class Tv {
boolean power; // 전원상태
int channel;
void power() { power = !power;}
void channellUp() { ++channel; }
void channelDown() { --channel; }
}
class CaptionTv extends Tv {
String text; // 캡션을 보여주기 위한 문자열
void caption() { /* 내용생략 */ }
}
클래스 Tv와 CaptionTv는 서로 상속관계에 있으며, 이 두 클래스의 인스턴스를 생성하고 사용하기 위해서는 다음과 같이 할 수 있다.
Tv t = new Tv();
CaptionTv = new CaptionTv();
이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우, 다음과 같이 부모 클래스 타입의 참조변수로 자식 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
Tv t = new CaptionTv(); // 부모 타입의 참조변수로 자식 인스턴스를 참조
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();
위의 코드에서 CaptionTv인스턴스를 2개 생성하고, 참조변수 c와 t가 생성된 인스턴스를 하나씩 참조하도록 하였다. 이 경우 실제 인스턴스가 CaptionTv타입이라 할지라도, 참조변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다.
Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서, 생성된 CaptionTv인스턴스의 멤버 중에서 Tv클래스에 정의되지 않은 멤버, text와 caption()은 참조변수 t로 사용이 불가능하다. 즉, t.text, t.caption()와 같이 사용할 수 없다는 것이다.
둘다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
추가로, 자식타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 불가능하다.
CaptionTv c = new Tv();
그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.
자식타입의 참조변수로 부모타입의 인스턴스를 참조할 수 없다.
참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
https://www.youtube.com/watch?v=87YsO_vBG0c
위의 유튜브 영상이 다형성 이해에 많은 도움이 되었다.
기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자식타입의 참조변수를 부모타입의 참조변수로, 부모타입의 참조변수를 자식타입의 참조변수로의 형변환만 가능하다.
자식타입 -▶ 부모타입(Up-Casting) : 형변환 생략가능
자식타입 ◀- 부모타입(Down-Casting) : 형변환 생략불가
참조변수간의 형변환 역시 캐스트연산자를 사용하며, 괄호()안에 변환하고자 하는 타입의 이름(클래스명)을 적어주면 된다.
Car클래스와 이를 상속받은 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 = (FireEngine)car; // 형변환 생략 불가능. 다운캐스팅
Car타입의 참조변수 c가 있다고 가정하자. 참조변수 c가 참조하고 있는 인스턴스는 아마도 Car인스턴스이거나 자식인 FireEngine인스턴스일 것이다.
Car타입의 참조변수 c를 Car타입의 부모인 Object타입의 참조변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는다. 그래서 형변환을 생략할 수 있도록 한 것이다.
하지만, Car타입의 참조변수 c를 자식인 FireEngine타입으로 변환하는 것은 참조변수가 다룰 수 있는 멤버의 개수를 늘리는 것이므로, 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다.
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
주로 조건문에서 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 연산의 결과로 true와 false 중 하나를 반환한다.
void doWork(Car c) {
if (c instanceof FireEngine {
FireEngine fe = (FireEngine)c;
fe.water();
...
} else if (c instanceof Ambulance) {
Ambulance a = (Ambulance)c;
a.siren();
...
}
...
}
위의 코드는 Car타입의 참조변수 c를 매개변수로 하는 메서드이다. 이 메서드가 호출될 때, 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만 메서드 내에서는 정확히 어떤 인스턴스인지 알 길이 없다. 그래서 instacnceof연산자를 이용해서 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환한 다음에 작업을 해야한다.
public class InatanceofTest {
public static void main(String[] args) {
FireEngine fe = new FireEngine();
if(fe instanceof FireEngine) {
System.out.println("This is a FireEngine instance");
}
if(fe instanceof Car) {
System.out.println("This is a Car instance.");
}
if(fe instanceof Object) {
System.out.println("This is a Object instance");
}
System.out.println(fe.getClass().getName()); //클래스 이름 출력
}
}
class Car {}
class FireEngine extends Car {}
생성된 인스턴스는 FireEngine타입인데도, Object타입과 Car타입의 instanceof연산에서도 true를 결과로 얻었다. 그 이유는 FireEngine클래스가 Object클래스와 Car클래스의 자식 클래스이므로 부모의 멤버들을 상속받았기 때문에, FireEngine인스턴스는 Object인스턴스와 Car인스턴스를 포함하고 있는 셈이기 때문이다.
요약하면, 실제 인스턴스와 같은 타입의 instanceof연산 이외에 부모타입의 instanceof연산에도 true를 결과로 얻으며, instanceof연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무 문제가 없다는 것이다.
멤버변수가 부모 클래스와 자식 클래스에 중복으로 정의된 경우, 부모타입의 참조변수를 사용했을 때는 부모 클래스에 선언된 멤버변수가 사용되고, 자식타입의 참조변수를 사용했을 때는 자식 클래스에 선언된 멤버변수가 사용된다.
중복 정의되지 않은 경우, 부모타입의 참조변수를 사용했을 때와 자식타입의 참조변수를 사용했을 때의 차이는 없다. 중복되지 않은 경우 하나뿐이므로 선택의 여지가 없기 때문이다.
public class BindingTest {
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();
System.out.println("p.x = " + p.x);
p.method();
System.out.println("c.x = " + c.x);
c.method();
}
}
class Parent {
int x = 100;
void method() {
System.out.println("Parent Method");
}
}
class Child extends Parent {
int x = 200;
void method() {
System.out.println("Child Method");
}
}
타입은 다르지만, 참조변수 p와 c모두 Child인스턴스를 참조하고 있다.
이 때 메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라 달라진다.
참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.
예를 들어, Product클래스와 이의 자식 클래스인 Tv, Audio, Computer클래스가 있다고 가정하자.
물건을 구입하는 기능을 가진 메서드를 가지고 있는 Buyer클래스가 있을 때, 제품의 종류가 늘어날 때마다 Buyer클래스에 새로운 buy메서드를 추가해주어야 할 것이다.
그러나 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.
void buy(Product P) {
money = money - p.price;
bonusPoint = bonusPoint + p.bonusPoint;
}
매개변수가 Product타입의 참조변수라는 것은, 메서드의 매개변수로 Product클래스의 자식타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
(Product클래스에 price와 bonusPoint가 선언되어 있어야 한다.)
앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product p)메서드의 매개변수로 받아들여질 수 있다.
package Ex;
class Product {
int price; //제품의 가격
int bonusPoint;
Product(int price) {
this.price = price;
bonusPoint = (int)(price/10.0); //보너스 점수는 제품 가격의 10%
}
}
class Tv extends Product {
Tv() {
// 부모클래스의 생성자 Product(int Price)를 호출한다.
super(100); //Tv의 가격을 100만원으로 한다.
}
// Object클래스의 toStirong()을 오버라이딩한다.
public String toString() { return "Tv"; }
}
class Computer extends Product {
Computer() { super(200); }
public String toString() { return "Computer"; }
}
class Buyer { //고객, 물건을 사는 사람
int money = 1000; //소유금액
int bonusPoint = 0; //보너스 점수
void buy(Product p) {
if(money<p.price) {
System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
System.out.println(p + "을/를 구입하셨습니다.");
}
}
public class PolyArgumentTest {
public static void main(String[] args) {
Buyer b = new Buyer();
b.buy(new Tv());
b.buy(new Computer());
System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
System.out.println("현재 보너스 점수는 " + b.bonusPoint + "점입니다.");
}
}
부모타입의 참조변수 배열을 사용하면, 공통의 부모를 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
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();
이 때 배열의 크기를 3으로 지정했기 때문에, 4개 이상의 제품을 적을 수 없다. 그렇다고 해서 배열의 크기를 무조건 크게 설정할 수도 없다. 이때 쓰는 것이 Vector클래스이다.
Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경쓰지 않아도 된다.
Vector클래스는 동적으로 크기가 관리되는 객체배열이다.
Vector item = new Vector();
참고서적
자바의 정석(저자: 남궁성)
다형성의 개념이 쉽지 않았다. 참조변수 타입 개념과 상속 개념이 잘잡혀있어야 따라갈 수 있는 부분이다. 공부하면서 참조변수와 상속의 개념에 대해 더 확고히 알게 된 것 같다.
자바의 정석 끝으로 가고 있다. 얼른 다 배우고 복습을 거쳐 코딩테스트도 자바로 해보고싶다.
다음은 추상클래스에 대해 공부하겠다.