다형성이란?
상위 클래스가 동일한 메시지로 하위 클래스들을 서로 다르게 동작시키는 객체 지향 원리
자바에서는 한 타입의 참조 변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현.
class TV {
protected int size;
public TV(int size) {
this.size = size;
}
protected int getSize() {
return size;
}
}
class ColorTV extends TV {
protected int resolution;
public ColorTV(int size, int resolution) {
super(size);
this.resolution = resolution;
}
public void printProperty() {
System.out.println(size + " 인치 " + resolution + " 해상도");
}
class SmartTV extends TV {
String text;
public SmartTV(int size) {
super(size);
}
void caption() {
}
}
Upcasting
부모 클래스 변수로 자식 클래스 객체를 참조할 수 있다.
이것을 업캐스팅(upcasting, 상형 형변환)이라고 한다.
public static void main(String[] args) {
TV myColorTV = new ColorTV(32, 1024); // 조상 타입 참조변수로 자손 타입 인스턴스 참조
TV mySmartTV = new mySmartTV(32, 1024); // 조상 타입 참조변수로 자손 타입 인스턴스 참조
}
이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, Tv와 ColorTv클래스가 서로 상속관계에 있을 경우, 다음과 같이 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하는 것도 가능하다.
Tv타입의 참조변수로는 SmartTV인스턴스 중에서 Tv클래스의 멤버들 (상속받은 멤버포함)만 사용할 수 있다. 따라서 생성된 SmartTv인스턴스의 멤버 중에서 Tv클래스에 정의되지 않은 멤버, text와 caption()은 참조변수 t로 사용이 불가능하다. 즉, t.text또는 t.caption()와 같이 할 수 없다는 것이다. 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
조상타입의 참조 변수로 자손 타입의 인스턴스를 참조할 수 있다. 반대로 자손 타입의 참조 변수로 조상 타입의 인스턴스를 참조할 수는 없다.
SmartTv s = new Tv(); // 에러. 허용 안 됨.
조상 타입의 참조 변수로 자손 타입의 인스턴스를 참조할 수 있다. 반대로 자손 타입의 참조 변수로 조상 타입의 인스턴스를 참조할 수는 없다.
참조 변수의 형변환
서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손 타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조 변수로의 형변환만 가능하다.
FireEngine f = new FireEngine();
Car c = (Car)f; // OK. 조상인 Car타입으로 형변환(생략가능)
FireEngine f2 = (FireEngine)c; // OK. 자손인 FireEngine타입으로 형변환(생략불가)
Ambulance a = (Ambulance)f; // 에러. 상속관계가 아닌 클래스 간의 형변환 불가
Car c = (Car)f; // f의 값(객체의 주소)을 c에 저장.
// 타입을 일치시키기 위해 형변환 필요(생략가능)
f = (FireEngine) c; // 조상타입을 자손타입으로 형변환 하는 경우 생략 불가
참조 변수의 형변환은 그저 리모컨(참조 변수)을 다른 종류의 것으로 바꾸는 것뿐이다.
위와 같이 조상 타입으로의 형변환을 생략할 수 있는 이유는 조상타입으로 형변환하면 다룰 수 있는 멤버의 개수가 줄어들기 때문에 항상 안전하기 때문이다. 반대로 실제 객체가 가진 기능보다 리모컨의 버튼이 더 많으면 안 된다.
서로 상속관계에 있는 타입 간의 형변환은 양방향으로 자유롭게 수행될 수 있으나,
참조 변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 먼저 확인하는 것이 중요하다.
instanceof 연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다.
연산의 결과로 boolean값인 true와 false중의 하나를 반환한다.
값이 null인 참조 변수에 대해 instanceof연산을 수행하면 false를 결과로 얻는다.
void doWork(Car c) {
if (c instanceof FireEngine) { // 1. 형변환이 가능한지 확인
FireEngine fe = (FireEngine)c; // 2. 형변환
fe.water();
...
}
}
instanceof연산자로 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환한 다음에 작업을 해야 한다.
어떤 타입에 대한 instanceof 연산의 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
instanceof를 지양하라
- instanceof를 사용하는 경우 새로운 메서드를 만들어주기 위해 사용되고 있는 모든 함수를 찾아가서 고쳐야 한다.
- 이는 객체의 확장이 어려워진다는 문제점이 있다. 객체의 확장에는 열려있고, 변화에는 닫혀있도록 해야 한다는 개방-폐쇄 원칙(OCP)을 위반한다.
- 각 타입에게 책임을 부여하면 되는 일이 하나의 메서드에게 모든 책임이 가중되는 일이 발생한 것이다.
- 한 클래스는 하나의 책임만 가져야 한다는 객체지향 프로그래밍의 원칙 중 하나인 단일 책임원칙(SRP) 이 위반된 것이라고 볼 수 있다.
- 다형성을 적용한 성능이 instanceof를 검사하는 성능보다 빠르다.
- instanceof의 경우 알맞은 타입을 찾을 때까지 컴파일 시에 모든 타입을 돌며 검사해야 한다.
매개변수의 다형성
참조 변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.
class Product {
int price; // 제품의 가격
int bonusPoint; // 제품구매 시 제공하는 보너스 점수
}
class Tv extends Produce{}
class Computer extends Produce{}
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;
}
Product클래스는 Tv와 Computer클래스의 조상이며, Buyer클래스는 제품(Product)을 구입하는 사람을 클래스로 표현했다.
구입할 대상이 필요하므로 매개변수로 구입할 제품을 넘겨받았다.
buy(Tv t)는 제품을 구입하면 제품을 구입한 사람이 가진 돈에서 제품의 가격을 빼고, 보너스 점수는 추가하는 작업을 하도록 작성되었다. 그런데 buy(Tv t)로는 Tv밖에 살 수 없기 때문에 아래와 같이 다른 제품들도 구입할 수 있는 메서드가 추가로 필요하다. 이렇게 되면, 제품의 종류가 늘어날 때마다 Buyer클래스에는 새로운 buy메서드를 추가해주어야 할 것이다. 그러나 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.
여러 종류의 객체를 배열로 다루기
조상 타입의 참조 변수로 자손 타입의 객체를 참조하는 것이 가능하다.
Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
조상 타입의 참조 변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
class Buyer { // 고객, 물건을 사는 사람
int money = 1000; // 소유금액
int bonusPoint = 0; // 보너스점수
Product[] cart = new Product[10]; // 구입한 제품을 저장하기 위한 배열(카트)
int i = 0; // Product배열 cart에 사용될 index
void buy(Product p) {
if(money < p.price) {
System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
return;
}
money -= p.price; // 가진 돈에서 구입한 제품의 가격을 뺀다.
bonusPoint += p.bonusPoint; // 제품의 보너스 점수를 추가한다.
cart[i++] = p; // 제품을 Product[] cart에 저장한다.
System.out.println(p + "을/를 구입하셨습니다.");
}
}
모든 제품 클래스의 조상인 Product클래스 타입의 배열을 사용함으로써 구입한 제품을 하나의 배열로 간단하게 다룰 수 있게 된다.
'Server > 자바의정석' 카테고리의 다른 글
[Java 입문] 디폴트 메서드 & Static 메서드, 내부 클래스 (1) | 2022.09.29 |
---|---|
[Java 입문] 추상(abstract) & 인터페이스(Interface) (0) | 2022.09.28 |
[Java 입문] 제어자(modifier), 캡슐화 (0) | 2022.09.28 |
[Java 입문] Super (0) | 2022.09.28 |
[Java 입문] 상속 (0) | 2022.09.27 |