상속에서 중요한 것 중에 클래스 간의 형 변환이 있다.
이전 예시인 Customer와 VIPCustomer 관계를 생각해볼 때
하위 클래스인 VIPCustomer는 상위 클래스인 Customer보다 더 많은 기능을 갖고 있음
상속받은 클래스는 상위 클래스 기능을 모두 사용 + 본인의 기능 사용이기 때문이다.
따라서 VIPCustomer는 VIPCustomer형인 동시에 Customer형이라고 할 수 있다.
그래서 다음과 같이 형변환이 가능해짐
Customer customerKim = new VIPCustomer(2020, "김유신", 111);
이때 이 customerKim은 정체가 뭘까?
VIPCustmer 생성자가 호출되므로 VIPCustomer의 변수, 메서드가 메모리에 생성됨.
그런데 클래서 자료형이 Customer로 제한되어 있음.
이 경우에 customerKim 참조 변수가 가리킬 수 변수와 메서드는 Customer 클래스의 멤버로 제한이 됨
이런 클래스 형 변환이 왜 필요하지? -> 다음 내용의 '오버라이딩'과 '다형성'에서 그 이유를 찾을 수 있음..!
- 메서드 오버라이딩
상위 클래스에서 정의한 메서드가 하위 클래스에서의 내용과 맞지 않을 경우 이를 하위 클래스에서 재정의 할 수 있음
- 오버라이딩의 조건
반환형, 메서드 이름, 매개변수 개수, 매개변수 자료형이 반드시 같아야 함.
그러지 않으면 자바 컴파일러가 이 메서드를 기존 메서드와 다른 메서드로 인식 해버림
- 예시
VIPCustomer 클래스에서 calcPrice() 메서드를 재정의 해보자.
테스트
package first_project;
public class Customer {
protected int customerId;
protected String customerName;
protected String customerGrade;
int bonusPoint;
double bonusRatio;
public int getCustomerId() {
return customerId;
}
public void setCustomerId(int customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getCustomerGrade() {
return customerGrade;
}
public void setCustomerGrade(String customerGrade) {
this.customerGrade = customerGrade;
}
public Customer(int customerId, String customerName) {
this.customerId = customerId;
this.customerName = customerName;
customerGrade = "Silver";
bonusRatio = 0.01;
//System.out.println("Customer(int, String) 생성자 호출됨");
}
public int calcPrice(int price) {
bonusPoint += price* bonusRatio;
return price;
}
public String showCustomerInfo() {
return customerName+ "님의 등급은 "+customerGrade+"이며, 보너스 포인트는 "+bonusPoint+"입니다.";
}
}
package first_project;
public class VIPCustomer extends Customer {
private int agentId;
double saleRatio;
public VIPCustomer(int customerId, String customerName, int agentId) {
super(customerId, customerName);
customerGrade = "VIP";
bonusRatio = 0.05;
saleRatio = 0.1;
this.agentId = agentId;
//System.out.println("VIPCustomer(int, String) 생성자 호출됨");
}
@Override
public int calcPrice(int price) {
bonusPoint += price* bonusRatio;
return price - (int)(price* saleRatio);
}
public int getAgentId() {
return agentId;
}
}
package first_project;
public class CustomerTest {
public static void main(String[] args) {
Customer customerLee = new Customer(1010, "이순신");
customerLee.bonusPoint = 1000;
VIPCustomer customerKim = new VIPCustomer(2020, "김유신", 111);
customerKim.bonusPoint = 10000;
int price = 10000;
System.out.println(customerLee.getCustomerName()+"님의 지불 금액은 "+ customerLee.calcPrice(price)+"원입니다. ");
System.out.println(customerKim.getCustomerName()+"님의 지불 금액은 "+ customerKim.calcPrice(price)+"원입니다. ");
}
}
결과: 오버라이딩 메서드에 의해 서로 다른 가격이 산출되었다.
이순신님의 지불 금액은 10000원입니다.
김유신님의 지불 금액은 9000원입니다.
- 다음과 같은 경우에는 어떻게 될까?
묵시적 형변환에 의해 VIPCustomer형에서 Customer형으로 변환된 경우. 한번 예상해보자.
Customer vc = new VIPCustomer(3030, "메롱이", 2000);
vc.bonusPoint = 1000;
System.out.println(vc.getCustomerName()+"님의 지불 금액은 "+ vc.calcPrice(price)+"원입니다. ");
결과
메롱이님의 지불 금액은 9000원입니다.
Customer형으로 선언했으니 당연히 Customer 클래스 내에 있는 calcPrice()를 호출할 것이라 예상했지만,
예상과는 다르게 VIPCustomer 클래스의 calcPrice(), 즉 재정의된 메서드가 호출되었다.
이유는??
상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재하면, 호출되는 메서드는 인스턴스에 의해 결정됨.
즉 선언한 클래스가 아니고 생성된 인스턴스의 메서드를 호출함.
이렇게 인스턴스의 메서드가 호출되는 기술을 가상 메서드라고 함
- 가상 메서드
클래스를 생성하여 인스턴스가 만들어지면 멤버 변수는 힙 메모리에 위치하게 됨
반면 메서드는 사용하는 메모리가 다름.
변수는 인스턴스가 생성될 때마다 새로 생성되지만 메서드는 명령 집합이기 때문에 인스턴스가 달라도 같은 로직을 수행하기 때문이다.
메서드 명령 집합은 메서드(코드) 영역에 위치하게 됨
- 가상 메서드의 원리
일반적으로 프로그램에서 메서드 호출은 그 메서드 명령 집합이 있는 메모리 위치를 참조하여 명령을 실행함을 의미.
근데 가상 메서드는 '가상 메서드 테이블'을 만듦. 이 테이블은 각 메서드 이름과 실제 메모리 주소가 짝을 이루고 잇어서 어떤 메서드가 호출되면 이 테이블을 통해 주소값을 찾고 해당 메서드의 명령을 수행함.
- 이를 토대로 Customer 클래스와 VIPCustomer 클래스를 살펴보자.
calcPrice() 메서드는 두 클래스에서 서로 다른 메서드 주소를 갖고 있음. 이렇게 재정의된 메서드는 실제 인스턴스에 해당하는 메서드가 호출됨.
반면 showCustomerInfo()와 같이 재정의 되지 않은 메서드는 메서드 주소가 같으며 상위 클래스의 메서드가 호출됨.
댓글