IS-A
관계는 일반적인 개념과 구체적인 개념의 관계이다. 예를 들어, 포유류와 사람, 고객과 VIP 고객, 탈 것과 트럭의 관계가 있다.
상속은 이러한 IS-A
관계에서 이루어지는 것이 바람직하다.
상속하면 부모 클래스의 속성과 메서드를 그대로 사용할 수 있으니까, IS-A
관계가 아니더라도 상속하여 사용하면 효율적이지 않을까? 이렇게 생각할수도 있지만, 오로지 코드를 재사용하기 위해 상속을 하는 일은 지양해야한다.
상속을 하게 되면 하위 클래스가 상위 클래스에 종속되게 되고, 서로 관련없는 클래스가 상속관계가 되면 결합도가 높아져 유지보수가 어려워지게 되기 때문이다.
HAS-A
관계는 한 클래스가 다른 클래스를 소유한 관계이다. 어떤 클래스가 한 클래스의 멤버변수인 관계를 생각하면 편하다. 예를 들어 학생과 수강과목, 학교와 학생 관계 등이 있다.
학교 클래스의 멤버 변수에 학생 클래스가 있을 수 있고(학교
has-a 학생
), 학생 클래스의 멤버 변수에 수강과목 클래스가 있다(학생
has-a 수강 과목
).
HAS-A
관계에서도 멤버변수의 접근을 통해 클래스의 코드를 재사용할 수 있으므로 단순히 코드를 재사용하고자하는 목적이라면 IS-A
관계 보다 HAS-A
관계가 더 적절하다. 그리고 HAS-A
관계의 클래스를 IS-A
관계로 혼동하여 상속하는 일이 없도록 해야한다.
지금까지 학습한 업캐스팅과 가상 메서드를 통해 다형성을 실현할 수 있다. 다형성이란 한마디로 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 의미한다. 즉, 이름이 같은 메서드가 서로 다른 역할을 하는 것이다. 아마 예제를 직접 보는 편이 더 이해하기 쉬울 것이다.
package polymorphism;
import java.util.ArrayList;
public class CustomerTest {
public static void main(String[] args) {
/* 배열의 요소는 모두 Customer 클래스형 */
ArrayList<Customer> customerList = new ArrayList<Customer>();
Customer customerLee = new Customer(10010, "Lee");
Customer customerHong = new VIPCustomer(10000, "Hong", 1);
VIPCustomer customerKim = new VIPCustomer(10020, "Kim", 2);
/* 배열에 넣을 때 업캐스팅 */
customerList.add(customerLee);
customerList.add(customerHong);
customerList.add(customerKim);
/* showCustomerInfo()는 오버라이딩 되었음 */
for (Customer customer : customerList) {
customer.showCustomerInfo();
}
System.out.println();
int price = 10000;
/* finalPrice(int price)는 오버라이딩 되었음 */
for (Customer customer : customerList) {
System.out.println(customer.getCustomerName() + " 님이 지불해야하는 금액은 " +
customer.finalPrice(price) + " 원, 적립된 보너스 포인트는 " +
customer.getBonusPoint() + " 점 입니다.");
}
}
}
Lee 님의 등급은 SILVER(이)며, 보너스 포인트는 0 입니다.
Hong 님의 등급은 VIP(이)며, 님의 담당 상담원 번호는 1 입니다.
Kim 님의 등급은 VIP(이)며, 님의 담당 상담원 번호는 2 입니다.
Lee 님이 지불해야하는 금액은 10000 원, 적립된 보너스 포인트는 100 점 입니다.
Hong 님이 지불해야하는 금액은 9000 원, 적립된 보너스 포인트는 500 점 입니다.
Kim 님이 지불해야하는 금액은 9000 원, 적립된 보너스 포인트는 500 점 입니다.
오버라이딩 된 showCustomerInfo()
와 finalPrice(int price)
메서드는 형태는 똑같지만 customer
변수로 넘어온 인스턴스 자료형마다 하는 기능이 다르다. 즉, customer.showCustomerInfo()
코드 하나가 다양한 역할을 하는 것이다. 이 것이 다형성이다.
이처럼 하나의 코드로 다양한 역할을 하지 못하고, 자료형마다 다른 기능을 정의해주어야 한다면, 코드가 복잡해질뿐만 아니라 유지보수성이나 확장성이 떨어지게 될 것이다.
public int finalPrice(int price) {
saveBonusPoint(price);
if (VIPCustomer 라면){
return (int)(price * (1.0 - saleRatio));
}
else {
return price;
}
}
만약 이런 상황에서 GoldCustomer
이라는 등급이 하나 더 생긴다고 해보자. 그러면 GoldCustomer
클래스의 정의와 더불어, 조건문을 하나 더 추가해주어야하는 일이 생긴다. GoldCustomer
클래스만 추가하고 싶을 뿐인데, 다른 부분의 코드를 건드려야하는 것이다.
하지만 다형성을 이용한다면, GoldCustomer
클래스 내에서 finalPrice()
재정의 해주기만 하면된다.
고객 등급이 하나뿐만 아니라 여러개가 추가된다고 해도, 다형성을 활용하면 코드의 특별한 수정없이 고객 등급 클래스를 추가하기만 한다면 프로그램을 계속 확장해나갈 수 있으며, 유지보수 또한 편리하다.
다운캐스팅
업캐스팅 된 클래스를 다시 원래 자료형으로 형변환하는 것
/* Animal이 상위 클래스, Human이 하위 클래스인 경우 */
Animal animal = new Human(); // 업캐스팅
Human human = (Human)human; // 다운캐스팅
Customer customerVIP = new VIPCustomer(); // 업캐스팅
VIPCustomer vip = (VIPCustomer)customerVIP; // 다운캐스팅
다운캐스팅은 업캐스팅과 다르게 변환하고자하는 클래스 명을 괄호안에 명시적으로 적어준다. (Human)
과 (VIPCustomer)
가 명시적으로 적어준 부분이다.
instanceof
다운캐스팅을 하기 전, 인스턴스의 원래 자료형을 확인하는 예약어
위의 예제코드의 경우, 인스턴스의 원래 자료형을 알고있기 때문에 별도의 확인 절차 없이 바로 다운캐스팅을 했지만, 인스턴스의 원래 자료형을 모르거나 알더라도 프로그램의 오류를 막기 위해 인스턴스의 자료형을 확인하는 절차는 필요하다. 이 때 쓰이는 예약어가 바로 instanceof
이다.
Animal animal = new Human();
if (animal instanceof Human) {
Human human = (Human)animal;
}
animal instanceof Human
은 animal
이 가리키고 있는 인스턴스의 자료형이 Human
클래스 자료형이 맞으면 true
를 아니면 false
를 반환해 준다.
/* Tiger와 Human이 Animal 클래스의 하위 클래스인 경우 */
Animal animal = new Tiger();
Human human = (Human)animal;
만약 이와 같이 코드를 작성한다면 어떤 일이 벌어질까? 일단 컴파일 당시에는 형 변환하고자 하는 자료형과 변수 human
의 자료형이 일치하는 지만 보기 때문에 컴파일 오류가 발생하지 않는다. animal
을 Human
클래스 자료형으로 변환하여 Human
클래스 자료형 변수에 넣고 있기 때문이다.
하지만 animal
인스턴스의 자료형은 Human
이 아닌 Tiger
이기 때문에 실행 오류가 발생한다. 그러므로 instanceof
로 인스턴스의 자료형을 확인하는 절차가 있는 것이 안전하다.