다형성이란?
객체지향 개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조 변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
요약하면 조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조할 수 있다.
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와 CatptionTv는 서로 상속관계에 있으며, 이 두 클래스의 인스턴스를 생성하려면 다음과 같이 할 수 있다.
Tv t = new Tv();
CaptionTv c = new CaptionTv();
지금까지 인스턴스의 타입과 일치하는 타입의 참조 변수만을 사용했다.
이처럼 인스턴스의 타입과 참조 변수의 타입을 일치하는 것이 보통이지만,
클래스가 서로 상속관계에 있을 경우,
조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조하도록 하는 것이 가능하다.
Tv t = new CaptionTv();
같은 타입 참조 vs 조상 타입 참조
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();
CaptionTv 인스턴스 2개를 생성하고, 참조 변수 c와 t가 생성된 인스턴스를 하나씩 참조하도록 하였다. 이 경우 실제 인스턴스가 CaptionTv타입이라 할지라도, 참조 변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다.
Tv타입의 참조 변수로는 CaptionTv 인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버 포함)만 사용할 수 있다. 따라서, 생성된 CaptionTv 인스턴스의 멤버 중에서 Tv클래스에 정의되지 않은 멤버, text와 caption()은 참조 변수 t로 사용이 불가능하다.
둘 다 같은 타입의 인스턴스지만 참조 변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
반대로 자손 타입의 참조 변수로 조상 타입의 인스턴스를 참조하는 것은 불가능하다.
CaptionTv c = new Tv();
위 코드를 컴파일하면 에러가 발생한다. 그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조 변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.
참조 변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
인스턴스의 타입과 일치하는 참조 변수를 사용하면 인스턴스의 멤버들을 모두 사용할 수 있을 텐데 왜 조상 타입의 참조 변수를 사용해서 인스턴스의 일부 멤버만을 사용하도록 할까? -> 인터페이스의 다형성
참조 변수의 형 변환
서로 상속관계에 있는 클래스 사이에서 참조 변수 형 변환이 가능하다.
자손 타입 --> 조상 타입(Up-casting) : 형 변환 생략 가능
자손 타입 <-- 조상 타입(Down-casting) : 형 변환 생략 불가
class Car {
String color;
int door;
void dirve() {
System.out.println("dive, Brrrr~");
}
void stop() {
System.out.println("stop!!!");
}
}
class FireEngine extends Car {
void water() {
System.out.println("water!!!");
}
}
class Ambulance extends Car {
void siren() {
System.out.println("siren~~~");
}
}
FireEngine f;
Ambulance a;
a = (Ambulance)f;
f = (FireEngine)a;
상속관계가 아닌 클래스 간에 형 변환이 가능하지 않다.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
참조 변수 car와 fe의 타입이 서로 다르기 때문에 대입 연산이 수행되기 전에 형 변환을 수행하여 두 변수 간의 타입을 맞춰주어야 한다.
그러나, 자손 타입의 참조 변수를 조상 타입의 참조 변수에 할당할 경우 형 변환을 생략할 수 있어서 'car = fe;'와 같이 하였다. 원칙적으로 'car = (Car) fe;'와 같이 해야 한다.
반대로 조상 타입의 참조 변수를 자손 타입의 참조 변수에 저장할 경우 형 변환을 생략할 수 없으므로, 'fe2 = (FireEngine) car'와 같이 명시적으로 형 변환을 해주어야 한다.
형 변환은 참조 변수의 타입을 변환하기 때문에 인스턴스에 아무런 영향을 미치지 않는다.
단지 참조 변수의 형 변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.
서로 상속관계에 있는 타입 간의 형 변환은 양방향으로 자유롭게 수행될 수 있으나,
참조 변수가 가리키는 인스턴스의 자손 타입으로 형 변환은 허용되지 않는다.
그래서 참조 변수가 가리키는 인스턴스의 타입이 무엇이지 확인하는 것이 중요하다.
instanceof연산자
참조 변수가 참조하고 있는 인스턴스의 실제 타입인지 알려면 instanceof연산자를 사용한다.
주로 조건문에 사용되며, instanceof의 왼쪽에는 참조 변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 연산의 결과로 boolean 값인 true 또는 false를 반환한다.
instanceof를 이용한 연산 결과로 true를 얻었다는 것은 참조 변수가 검사한 타입으로 형 변환이 가능하다는 것을 뜻한다.
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();
}
}
이 메서드가 호출될 때 메서드 내에서는 정확히 어떤 인스턴스인지 알 수 없다. 그래서 instanceof 연산자를 이용해서 참조 변수 c가 가리키고 있는 인스턴스의 타입을 체크하고, 적절히 형 변환한 다음에 작업을 해야 한다.
실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상 타입의 instanceof 연산에도 true를 결과로 얻으며, instanceof 연산의 결과가 true라는 것은 검사한 타입으로의 형 변환을 해도 아무런 문제가 없다는 뜻이다.
어떤 타입에 대한 instanceof연산의 결과가 true라는 것은
검사한 타입으로 형 변환이 가능하다는 것을 뜻한다.
참조 변수와 인스턴스의 연결
조상 클래스에 선언된 멤버 변수와 같은 이름의 인스턴스 변수를 자손 클래스에 중복으로 정의했을 때, 조상 타입의 참조 변수로 자손 인스턴스를 참조하는 경우와 자손 타입의 참조 변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
멤버 변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우,
조상 타입의 참조 변수를 사용 ---> 조상 클래스에 선언된 멤버 변수가 사용되고,
자손 타입의 참조 변수를 사용 ---> 자손 클래스에 선언된 멤버 변수가 사용된다.
public class Test {
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();
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("x = " + x);
System.out.println("super.x = " + super.x);
System.out.println("this.x = " + this.x);
}
}
p.x = 100
x = 200
super.x = 100
this.x = 200
c.x = 200
x = 200
super.x = 100
this.x = 200
Parent의 x와 Child의 x를 구분하는데 참조 변수 super와 this가 사용된다.
멤버 변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버 변수에 접근할 수 있도록 해야 한다. 다른 외부 클래스에서 참조 변수를 통해 직접적으로 인스턴스 변수에 접근하지 못하게 한다.
인스턴스 변수에 직접 접근하면, 참조 변수의 타입에 따라 사용되는 인스턴스 변수가 달라질 수 있으므로 주의해야 한다.
매개변수의 다형성
public class Test {
public static void main(String[] args) {
}
}
class Product {
int price; // 제품 가격
int bonusPoint; // 제품 구매시 제공하는 보너스 점수
}
class Tv extends Product {}
class Computer extends Product {}
class Audio extends Product {}
class Buyer { // 고객 물건을 사는 사람
int money = 1000; // 소유 금액
int bonusPoint = 0; // 보너스 점수
void buy(Tv t) {
// Buyer가 가진 돈(money)에서 제품의 가격(t.price)만큼 뺀다.
money = money - t.price;
// Buyer의 보너스 점수(bonusPoint)에 제품의 보너스 점수(t.bonusPoint)를 더한다.
bonusPoint = bonusPoint + t.bonusPoint;
}
void buy(Computer c) {
money = money - c.price;
bonusPoint = bonusPoint + c.bonusPoint;
}
void buy(Audio a) {
money = money - a.price;
bonusPoint = bonusPoint + a.bonusPoint;
}
}
물건을 구입하는 메서드를 추가하려면 위와 같이 각 제품별로 메서드를 추가 구현이 필요하다.
이렇게 되면, 제품의 종류가 늘어날 때마다 Buyer클래스에는 새로운 buy 메서드를 추가해야 한다.
그러나 메서드의 매개 변수의 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.
void buy(Product p) {
money = money - p.price;
bonusPoint = bonusPoint + p.bonusPoint;
}
매개변수가 Product타입의 참조 변수라는 것은, 메서드의 매개 변수로 Product 클래스의 자손 타입의 참조 변수면 어느 것이나 매개 변수로 받아들일 수 있다는 뜻이다.
그리고 Product 클래스에 price와 bonusPoint가 선언되어 있기 때문에 참조 변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있기에 이와 같이 할 수 있다.
앞으로 다른 제품 클래스를 추가할 때 Product 클래스를 상속받기만 하면, buy(Product p) 메서드의 매개 변수로 받을 수 있다.
class Product {
int price; // 제품 가격
int bonusPoint; // 제품 구매시 제공하는 보너스 점수
Product(int price) {
this.price = price;
this.bonusPoint = (int) (price / 10.0); // 보너스 점수는 제품 가격의 10%
}
}
class Tv extends Product {
Tv() {
super(100);
}
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 Test {
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 + "점입니다.");
}
}
Tv을/를 구입하셨습니다.
Computer을/를 구입하셨습니다.
현재 남은 돈은 700만원입니다.
현재 보너스점수는 30점입니다.