1. 상속이란?
- 객체 지향 프로그래밍의 중요한 특징 중 하나가 상속(inheritance)입니다.
- 상속은 우리가 일반적 으로 알 듯 무엇인가를 물려받는다는 의미입니다.
- B 클래스가 A 클래스를 상속받으면 B 클래스는 A 클래스의 멤버 변수와 메서드를 사용할 수 있습니다.
- 객체 지향 프로그램은 유지 보수하기 편하고 프로그램을 수정하거나 새로운 내용을 추가하는 것이 유연한데, 그 기반이 되는 기술이 바로 상속입니다.
1-1 클래스의 상속
- 클래스 간 상속을 표현할 때는 상속받는 클래스에서 상속하는 클래스로 화살표가 갑니다.
- 부모 클래스(parent class)를 '상위 클래스', 자식 클래스(child class)를 '하위 클래스'라고 부릅니다.
클래스 상속 문법
- 자바 문법에서 상속을 구현할 때는 extends 예약어를 사용합니다.
- extends 예약어는 '연장, 확장하다'라는 의미입니다.
- A가 가지고 있는 속성이나 기능을 추가로 확장하여 B 클래스를 구현한다는 뜻으로, 일반적인 클래스 A에서 더 구체적인 B 클래스가 됩니다.

- 위 코드는 'B 클래스가 A 클래스를 상속받는다'라고 합니다.

- 포유류는 사람보다 일반적인 개념입니다.
- 사람은 포유류의 특징과 기능을 기본으로 더 많거나 다른 특징과 기능을 가지고 있습니다.
1-2 상속을 사용하여 고객 관리 프로그램 구현하기
예제 8-1 Customer 클래스 구현하기
public class Customer {
// 멤버 변수
// 고객 아이디
private int customerID;
// 고객 이름
private String customerName;
// 고객 등급
private String customerGrade;
// 보너스 포인트
int bonusPoint;
// 적립 비율
double bonusRatio;
// 디폴트 생성자
public Customer() {
// 기본 등급
customerGrade = "SILVER";
// 보너스 포인트 기본 적립 비율
bonusRatio = 0.01;
}
// 보너스 포인트 적립, 지불 가격 계산 메서드
public int calcPrice(int price) {
// 보너스 포인트 계산
bonusPoint += price * bonusRatio;
return price;
}
// 고객 정보를 반환하는 메서드
public String showCustomerInfo() {
return customerName + "님의 등급은 " + customerGrade +
"이며, 보너스 포인트는 " + bonusPoint + "입니다.";
}
}
- 모든 멤버 변수를 반드시 private로 선언할 필요는 없습니다.
- 필요에 따라 멤버 변수나 메서드를 외부에 노출하지 않을 목적일 때 private로 선언합니다.
- 위에서 사용한 멤버 변수

- 위에서 사용한 메서드

예제 8-2 VIPCustomer 클래스 구현하기
// VIPCustomer 클래스는 Customer 클래스를 상속받음
public class VIPCustomer extends Customer {
// VIP 고객 상담원 아이디
private int agentID;
// 할인율
double saleRatio;
public VIPCustomer() {
// 상위 클래스에서 private 변수이므로 오류 발생
customerGrade = "VIP";
bonusRatio = 0.05;
saleRatio = 0.1;
}
public int getAgentID() {
return agentID;
}
}
- 상위 클래스에서 customerGrade는 private 변수이므로 외부 클래스에서는 이 변수를 사용할 수 없습니다.
- VIP 고객에게 제공하는 혜택인 할인율과 세일 가격을 어떻게 적용할지 구현하지 않았습니다.
상위 클래스 변수를 사용하기 위한 protected 에약어
- 상위 클래스에 작성한 변수나 메서드 중 외부 클래스에서 사용할 수 없지만 하위 클래스에서는 사용할 수 있도록 지정하는 예약어가 바로 protected입니다.
- protected는 상속된 하위 클래스를 제외한 나머지 외부 클래스에서는 private와 동일한 역할을 합니다.
예제 8-3 protected 변수 선언
public class Customer {
// 멤버 변수
// 고객 아이디
protected int customerID;
// 고객 이름
protected String customerName;
// 고객 등급
protected String customerGrade;
// 보너스 포인트
int bonusPoint;
// 적립 비율
double bonusRatio;
......
// protected 예약어로 선언한 변수를 외부에서 사용할 수 있도록
// get( ), set( ) 메서드 추가
int getCustomerID() {
return customerID;
}
void setCustomerID(int customerID) {
this.customerID = customerID;
}
String getCustomerName() {
return customerName;
}
void setCustomerName(String customerName) {
this.customerName = customerName;
}
String getCustomerGrade() {
return customerGrade;
}
void setCustomerGrade(String customerGrade) {
this.customerGrade = customerGrade;
}
}
- protected 예약어로 선언한 변수는 외부 클래스에는 private 변수처럼 get( ) 메서드를 사용해 값을 가져올 수 있고, set( ) 메서드를 사용해 값을 지정할 수 있습니다.
- Customer 클래스를 상속받은 VIPCustomer 클래스는 protected로 선언한 변수를 상속받게 되고, 나머지 public 메서드도 상속받아 사용할 수 있습니다.
예제 8-4 상속 클래스 테스트하기
public class CustomerTest1 {
public static void main(String[] args) {
Customer customerYu = new Customer();
// customerID와 customerName은 protected 변수이므로 set( ) 메서드 호출
customerYu.setCustomerID(10010);
customerYu.setCustomerName("유중혁");
customerYu.bonusPoint = 1000;
System.out.println(customerYu.showCustomerInfo());
VIPCustomer customerKim = new VIPCustomer();
// customerID와 customerName은 protected 변수이므로 set( ) 메서드 호출
customerKim.setCustomerID(10020);
customerKim.setCustomerName("김독자");
customerKim.bonusPoint = 10000;
System.out.println(customerKim.showCustomerInfo());
}
}

- VIPCustomer가 Costomer를 상속했기 때문에 아이디, 고객 이름의 메서드를 사용할 수 있습니다.
2. 상속에서 클래스 생성과 형 변환
- 하위 클래스가 생성될 때는 상위 클래스의 생성자가 먼저 호출됩니다.
- 상속 관계에서 클래스의 생성 과정을 살펴보면 하위 클래스가 상위 클래스의 변수와 메서드를 사용할 수 있는 이유와 하위 클래스가 상위 클래스의 자료형으로 형 변환을 할 수 있는 이유를 이해할 수 있습니다.
2-1 하위 클래스가 생성되는 과정
- 상속을 받은 하위 클래스는 상위 클래스의 변수와 메서드를 사용할 수 있습니다.
- CustomerTest 예제를 살펴보면 VIPCustomer 클래스로 선언한 customerKim 인스턴스는 상속받은 상위 클래스의 변수를 자기 것처럼 사용할 수 있습니다.
- 변수를 사용할 수 있다는 것은 그 변수를 저장하고 있는 메모리가 존재한다는 뜻입니다.
예제 8-5 상속에서 클래스 생성 과정(1)
public class Customer {
......
// 디폴트 생성자
public Customer() {
// 기본 등급
customerGrade = "SILVER";
// 보너스 포인트 기본 적립 비율
bonusRatio = 0.01;
// 상위 클래스 생성할 때 콘솔 출력문
System.out.println("Customer( ) 생성자 호출");
}
......
}
- Customer( ) 생성자에 출력문을 넣으면, Customer 인스턴스가 생성되면 이 호출문이 출력될 것입니다.
예제 8-6 상속에서 클래스 생성 과정(2)
// VIPCustomer 클래스는 Customer 클래스를 상속받음
public class VIPCustomer extends Customer {
......
public VIPCustomer() {
// 상위 클래스에서 private 변수이므로 오류 발생
customerGrade = "VIP";
bonusRatio = 0.05;
saleRatio = 0.1;
// 하위 클래스 생성할 때 콘솔 출력문
System.out.println("VIPCustomer( ) 생성자 호출");
}
......
}
예제 8-7 상속에서 클래스 생성 과정(3)
public class CustomerTest2 {
public static void main(String[] args) {
// 하위 클래스 생성
VIPCustomer customerKim = new VIPCustomer();
customerKim.setCustomerID(10020);
customerKim.setCustomerName("김독자");
customerKim.bonusPoint = 10000;
System.out.println(customerKim.showCustomerInfo());
}
}

- 출력 화면을 보면 상위 클래스의 Customer( ) 생성자가 먼저 호출되고 그 다음에 VIPCustomer( )가 호출되는 것을 알 수 있습니다.
- 상위 클래스를 상속받은 하위 클래스가 생성될 때는 반드시 상위 클래스의 생성자가 먼저 호출됩니다.
- 상위 클래스 생성자가 호출될 때 상위 클래스의 멤버 변수가 메모리에 생성되는 것입니다.
- 아래는 VIPCusotmer가 생성될 때 힙 메모리 구조를 그려본 것입니다.

- 위와 같이 상위 클래스의 변수가 메모리에 먼저 생성되기 때문에 하위 클래스에서도 이 값들을 모두 사용할 수 있습니다.
2-2 부모를 부르는 예약어, super
- super 예약어는 하위 클래스에서 상위 클래스로 접근할 때 사용합니다.
- 하위 클래스는 상위 클래스의 주소, 즉 참조 값을 알고 있는데, 이 참조 값을 가지고 있는 예약어가 바로 super입니다.
- super는 상위 클래스의 생성자를 호출하는 데도 사용합니다.
상위 클래스 생성자 호출하기
- 하위 클래스 생성자만 호출했는데 상위 클래스 생성자가 호출되는 이유는 하위 클래스 생성자에서 super( )를 자동으로 호출하기 때문입니다.
- super( )를 호출하면 상위 클래스의 디폴트 생성자가 호출됩니다.

super 예약어로 매개변수가 있는 생성자 호출하기
예제 8-8 Customer 클래스에 새로운 생성자 추가하기
public class Customer {
......
public Customer(int customerID, String customerName) {
this.customerID = customerID;
this.customerName = customerName;
customerGrade = "SILVER";
bonusRatio = 0.01;
System.out.println("Customer(int, String) 생성자 호출");
}
......
}
- 기존의 생성자는 삭제하거나 주석 처리 한 후 새로운 생성자를 추가합니다.
- Customer 클래스의 디폴트 생성자를 없애고 새로운 생성자를 작성하면, Customer 클래스를 상속받은 VIPCustomer 클래스에서 오류가 발생합니다.
- VIPCustomer 클래스의 생성자도 수정해줍니다.
예제 8-9 명시적으로 상위 클래스 생성자 호출하기
// VIPCustomer 클래스는 Customer 클래스를 상속받음
public class VIPCustomer extends Customer {
......
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, int) 생성자 호출");
}
......
}
- 새로운 생성자는 고객 ID, 고객 이름, 상담원 ID를 매개변수로 받아옵니다.
- super 예약어는 상위 클래스 생성자를 호출하는 역할을 합니다.
상위 클래스의 멤버 변수나 메서드를 참조하는 super
- 상위 클래스에 선언한 멤버 변수나 메서드를 하위 클래스에서 참조할 때도 super를 사용합니다.
- this를 사용하여 자신의 멤버에 접근했던 것과 비슷합니다.
2-3 상위 클래스로 묵시적 클래스 형 변환
- 상속을 공부하면서 이해해야 하는 중요한 관계가 클래스 간의 형 변환입니다.
- 개념 면에서 보면 상위 클래스인 Customer가 VIPcustomer보다 일반적인 개념이고, 기능 면에서 보면 VIPCustomer가 Customer보다 기능이 더 많습니다.
- 상속받은 클래스는 상위 클래스 기능을 모두 사용할 수 있고 추가로 더 많은 기능을 구현하기 때문입니다.
- VIPCustomer는 VIPCustomer형이면서 동시에 Customer형이기도 합니다.

- VIPCustomer 클래스로 인스턴스를 생성할 때 이 인스턴스의 자료형을 Customer형으로 클래스 형 변환하여 선언할 수 있습니다.
형 변환된 vc가 가리키는 것

- Customer vc = new VIPCustomer( ); 문장이 실행되면 VIPCustomer 생성자가 호출되므로 클래스 변수가 위와 같이 메모리에 만들어지지만, 클래스의 자료형이 Customer로 한정되었습니다.
- 클래스가 형 변환되었을 때는 선언한 클래스형에 기반하여 멤버 변수와 메서드에 접근할 수 있습니다.
- 이 vc 참조 변수가 가리킬 수 있는 변수와 메서드는 Customer 클래스의 멤버 뿐입니다.
- 하위 클래스의 인스턴스가 상위 클래스로 형 변환되는 과정이 묵시적으로 이루어진다는 것을 이해하면 됩니다.
3. 메서드 오버라이딩
3-1 상위 클래스 메서드 재정의하기
- 상위 클래스에서 정의한 메서드가 하위 클래스에서 구현할 내용과 맞지 않을 경우에 하위 클래스에서 이 메서드를 재정의하는 것을 메서드 오버라이딩(method overriding)이라고 합니다.
- 오버라이딩을 하려면 반환형, 메서드 이름, 매개 변수 개수, 매개변수 자료형이 반드기 같아야 합니다.
VIP 고객 클래스의 제품 가격 계산 메서드 재정의하기
예제 8-8 calcPrice( ) 메서드 재정의하기
// VIPCustomer 클래스는 Customer 클래스를 상속받음
public class VIPCustomer extends Customer {
// VIP 고객 상담원 아이디
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, int) 생성자 호출");
}
public int getAgentID() {
return agentID;
}
// 재정의한 메서드
@Override
public int calcPrice(int price) {
// 보너스 포인트 적립
bonusPoint += price * bonusRatio;
// 할인된 가격을 계산하여 반환
return price - (int)(price * saleRatio);
}
}
- 하위 클래스 VIPCustomer에서 calcPrice( ) 메서드를 재정의했습니다.
- 상위 클래스의 calcPrice( ) 메서드와 매개변수의 자료형 및 개수가 같고, 반환형도 int형으로 같습니다.
- Alt + Shift + s → v를 누르면 오버라이드 창이 열립니다.
- @Override 애노테이션은 '이 메서드는 재정의된 메서드입니다'라고 컴파일러에 명확히 알려주는 역할을 합니다.
예제 8-9 calcPrice( ) 테스트하기
public class OverridingTest1 {
public static void main(String[] args) {
Customer ctYu = new Customer(10010, "유중혁");
ctYu.bonusPoint = 1000;
VIPCustomer ctKim = new VIPCustomer(10020, "김독자", 777);
ctKim.bonusPoint = 10000;
int price = 10000;
System.out.println(ctYu.getCustomerName() + "님이 지불해야 하는 금액은 "
+ ctYu.calcPrice(price) + "원입니다.");
System.out.println(ctKim.getCustomerName() + "님이 지불해야 하는 금액은 "
+ ctKim.calcPrice(price) + "원입니다.");
}
}

- 유중혁 고객은 일반 등급이므로 정가 그대로 10,000원을 지불하지만, 김독자 고객은 VIP 등급이므로 10% 할인을 받아 9,000원을 지불합니다.
3-2 묵시적 클래스 형 변환과 메서드 재정의
예제 8-10 클래스 형 변환과 재정의 메서드 호출하기
package inheritance;
public class OverridingTest1 {
public static void main(String[] args) {
// VIP 고객 생성
Customer ctLee = new VIPCustomer(10030, "이지혜", 1000);
ctLee.bonusPoint = 1000;
System.out.println(ctLee.getCustomerName() + "님이 지불해야 하는 금액은 "
+ ctLee.calcPrice(10000) + "원입니다.");
}
}

- 멤버 변수와 메서드는 선언한 클래스형에 따라 호출됩니다.
- 이 예제에서는 재정의된 메서드가 호출되었습니다.
- 상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정됩니다.
- 다시 말해 선언한 클래스형이 아닌 생성된 인스턴스의 메서드를 호출하는 것입니다.
- 인스턴스의 메서드가 호출되는 기술을 '가상 메서드(virtual method)'라고 합니다.
3-3 가상 메서드
- 자바의 클래스는 멤버 변수와 메서드로 이루어져 있습니다.
- 클래스를 생성하여 인스턴스가 만들어지면 멤버 변수는 힙 메모리에 위치합니다.
- 변수가 사용하는 메모리와 메서드가 사용하는 메모리는 다릅니다.
- 변수는 인스턴스가 생성될 때마다 새로 생성되지만, 메서드는 실행해야 할 명령 집합이기 때문에 인스턴스가 달라도 같은 로직을 수행합니다.
- 같은 객체의 인스턴스를 여러 개 생성한다고 해서 메서드도 여러 개 생성되지는 않습니다.
예제 8-11 메서드 호출하기
public class TestA {
int num;
void aaa() {
System.out.println("aaa( ) 출력");
}
public static void main(String[] args) {
TestA a1 = new TestA();
a1.aaa();
TestA a2 = new TestA();
a2.aaa();
}
}

- 위 코드의 실행 과정
1) main( ) 함수가 실행되면 지역 변수는 스택 메모리에 위치합니다.
2) 각 참조 변수 a1과 a2가 가리키는 인스턴스는 힙 메모리에 생성됩니다.
3) 메서드의 명령 집합은 메서드 영역(코드 영역)에 위치합니다.
4) 우리가 메서드를 호출하면 메서드 영역의 주소를 참조하여 명령이 실행됩니다.
- 인스턴스가 달라도 동일한 메서드가 호출됨
가상 메서드의 원리
- 일반적으로 프로그램에서 메서드를 호출한다는 것은 그 메서드의 명령 집합이 있는 메모리에 위치를 참조하여 명령을 실행하는 것입니다.
- 가상 메서드의 경우에는 '가상 메서드 테이블'이 만들어지고, 가상 메서드 테이블은 각 메서드 이름과 실제 메모리 주소가 짝을 이루고 있습니다.
예제 8-12 클래스형에 기반하여 지불 금액 계산하기
public class OverridingTest3 {
public static void main(String[] args) {
int price = 10000;
// Customer 인스턴스 생성
Customer ctYu = new Customer(10010, "유중혁");
System.out.println(ctYu.getCustomerName() + "님이 지불해야 하는 금액은 "
+ ctYu.calcPrice(price) + "원입니다.");
// VIPCustomer 인스턴스 생성
VIPCustomer ctKim = new VIPCustomer(10020, "김독자", 777);
System.out.println(ctKim.getCustomerName() + "님이 지불해야 하는 금액은 "
+ ctKim.calcPrice(price) + "원입니다.");
// VIPCustomer 인스턴스를 Customer 형으로 변환
Customer vc = new VIPCustomer(10030, "이지혜", 1000);
System.out.println(vc.getCustomerName() + "님이 지불해야 하는 금액은 "
+ vc.calcPrice(10000) + "원입니다.");
}
}
- 정리
1 ) 상위 클래스(Customer)에서 선언한 calcPrice( ) 메서드가 있고 이를 하위 클래스(VIPCustomer)에서 재정의한 상태에서 하위 클래스 인스턴스(vc)가 상위 클래스로 형변환이 됩니다.
2 ) vc.calcPrice( )가 호출되면, vc 변수를 선언할 때 사용한 자료형(Customer)의 메서드가 호출되는 것이 아니라 생서된 인스턴스(VIPCustomer)의 메서드가 호출됩니다.
- 이를 가상메서드라 하며, 자바의 모든 메서드는 가상 메서드입니다.
4. 다형성
4-1 다형성이란?
- 다형성(polymorphism)이란 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말합니다.
- 쉽게 말해 같은 코드에서 여러 실행 결과가 나오는 것을 의미합니다.
예제 8-13 다형성 테스트하기
class Animal {
public void move() {
System.out.println("동물이 움직입니다.");
}
}
class Human extends Animal{
public void move() {
System.out.println("사람이 두 발로 걷습니다.");
}
}
class Tiger extends Animal{
public void move() {
System.out.println("호랑이가 네 발로 걷습니다.");
}
}
class Eagle extends Animal{
public void move() {
System.out.println("독수리가 하늘을 납니다.");
}
}
public class AnimalTest1 {
public static void main(String[] args) {
AnimalTest1 aTest = new AnimalTest1();
aTest.moveAnimal(new Human());
aTest.moveAnimal(new Tiger());
aTest.moveAnimal(new Eagle());
}
// 매개변수의 자료형이 상위 클래스
public void moveAnimal(Animal animal) {
// 재정의된 메서드가 호출됨
animal.move();
}
}

- 테스트를 하기 위해 AnimalTest1 클래스에 moveAnimal( ) 메서드를 만들었습니다.
- 이 메서드는 어떤 인스턴스가 넘어와도 모두 Animal형으로 변환합니다.
- 예로 매개변수가 전달되는 부분에 Human 인스턴스가 전달되었다면 다음 코드 처럼 형 변환이 됩니다.

- Animal에서 상속받은 클래스가 매개변수로 넘어오면 모두 Animal형으로 변환되므로 animal.move( ) 메서드를 호출할 수 있습니다.
- 가상 메서드 원리에 따라 animal.move( ) 메서드가 호출하는 메서드는 Animal의 move가 아닌 매개변수로 넘어온 실제 인스턴스의 메서드입니다.
- animal.move( ) 코드는 변함이 없지만 어떤 매개변수가 넘어 왔느냐에 따라 출력문이 달라집니다.
- 이것이 바로 다형성임
4-2 다형성의 장점
- 다른 동물이 새로 추가되는 경우를 생각해보면, 새로운 동물도 Animal 클래스를 상속받아 구현하면 모든 클래스를 자료형 하나로 쉽게 관리할 수 있을 것입니다.
- 상위 클래스에서 공통 부분의 메서드를 제공하고, 하위 클래스에서는 그에 기반한 추가 요소를 덧붙여 구현하면 코드 양도 줄어들고 유지보수도 편리합니다.
- 필요에 따라 상속받은 모든 클래스를 하나의 상위 클래스로 처리할 수 있고 다형성에 의해 각 클래스의 여러 가지 구현을 실행할 수 있으므로 쉽게 확장할 수 있습니다.
- 다형성을 잘 활용하면 유연하면서도 구조화된 코드를 구현하여 확장성 있고 유지보수하기 좋은 프로그램을 개발할 수 있습니다.
다형성을 활용해 VIP 고객 클래스 완성하기
예제 8-14 고객 관리 프로그램 완성하기(1)
public class Customer {
protected int customerID;
protected String customerName;
protected String customerGrade;
int bonusPoint;
double bonusRatio;
public Customer() {
// 고객 등급과 보너스 포인트 적립률 지정 함수 호출
initCustomer();
}
public Customer(int customerID, String customerName) {
this.customerID = customerID;
this.customerName = customerName;
// 고객 등급과 보너스 포인트 적립률 지정 함수 호출
initCustomer();
}
// 멤버 변수의 초기화 부분
// 생성자에서만 호출하는 메서드이므로 private로 선언
private void initCustomer() {
customerGrade = "SILVER";
bonusRatio = 0.01;
}
public int calcPrice(int price) {
bonusPoint += price * bonusPoint;
return price;
}
public String showCustomerInfo() {
return customerName + "님의 등급은 " + customerGrade + "이며, 보너스 포인트는 "
+ bonusPoint + "입니다.";
}
int getCustomerID() {
return customerID;
}
void setCustomerID(int customerID) {
this.customerID = customerID;
}
String getCustomerName() {
return customerName;
}
void setCustomerName(String customerName) {
this.customerName = customerName;
}
String getCustomerGrade() {
return customerGrade;
}
void setCustomerGrade(String customerGrade) {
this.customerGrade = customerGrade;
}
}
- initCustomer( ) 메서드는 클래스의 멤버 변수를 초기화하는데, Customer 클래스를 생성하는 두 생성자에서 공통으로 사용하는 코드이므로 메서드로 분리하여 호출합니다.
예제 8-15 고객 관리 프로그램 완성하기(2)
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;
}
// 지불 가격 메서드 재정의
public int calcPrice(int price) {
bonusPoint += price * bonusPoint;
return price - (int)(price * saleRatio);
}
// 고객 정보 출력 메서드 재정의
public String showCustomerInfo() {
return super.showCustomerInfo() + " 담당 상담원 번호는 " + agentID + "입니다.";
}
public int getAgentID() {
return agentID;
}
}
- calcPrice( ) 메서드와 showCustomerInfo( ) 메서드를 재정의했습니다.
- 일반 고객 클래스에서 calcPrice( ) 메서드는 정가를 그대로 반환했지만, VIP 고객 클래스에서는 할인율을 반영한 지불 가격을 반환합니다.
- 일반 고객 클래스에서 showCustomerInfo( ) 메서드는 고객 등급과 이름만 출력했지만, VIP 고객 클래스에서는 담당 상담원 번호까지 출력합니다.
예제 8-16 고객 관리 프로그램 완성하기(3)
public class CustomerTest {
public static void main(String[] args) {
Customer customerYu = new Customer();
customerYu.setCustomerID(10010);
customerYu.setCustomerName("유중혁");
customerYu.bonusPoint = 1000;
System.out.println(customerYu.showCustomerInfo());
Customer customerKim = new VIPCustomer(10020, "김독자", 12345);
customerKim.bonusPoint = 1000;
System.out.println(customerKim.showCustomerInfo());
System.out.println("=============== 할인율과 보너스 포인트 계산 ===============");
int price = 10000;
int yuPrice = customerYu.calcPrice(price);
int kimPrice = customerKim.calcPrice(price);
System.out.println(customerYu.getCustomerName() + "님이 "
+ yuPrice + "원 지불하셨습니다.");
System.out.println(customerYu.showCustomerInfo());
System.out.println(customerKim.getCustomerName() + "님이 "
+ kimPrice + "원 지불하셨습니다.");
System.out.println(customerKim.showCustomerInfo());
}
}

- 출력 결과를 보면 10,000원짜리 상품을 구입했을 때 등급에 따라 다른 할인율과 포인트 적립이 이루어지는 것을 알 수 있습니다.
- 상속 관계에 있는 상위 클래스와 하위 클래스는 같은 상위 클래스 자료형으로 선언되어 생성할 수 있지만 재정의된 메서드는 각각 호출될 뿐만 아니라 이름이 같은 메서드가 서로 다른 역할을 구현하고 있음을 알 수 있습니다.
5. 다형성 활용하기
- 앞에서 배운 상속과 다형성을 활용하면 프로그램 유지보수하는 데 매우 편리합니다.
- 배열을 함께 사용하면 여러 하위 클래스 자료형을 상위 클래스 자료형으로 한꺼번에 관리할 수 있음
5-1 일반 고객과 VIP 고객의 중간 등급 만들기
예제 8-17 새로운 고객 등급 추가하기
public class GoldCustomer extends Customer{
double saleRatio;
public GoldCustomer(int customerID, String customerName) {
super(customerID, customerName);
customerGrade = "GOLD";
bonusRatio = 0.02;
saleRatio = 0.1;
}
// 재정의한 메서드
public int calcPrice(int price) {
bonusPoint += price * bonusRatio;
return price - (int)(price * saleRatio);
}
}
- GoldCustomer 클래스는 지불 가격과 보너스 포인트를 계산하는 calcPrice( ) 메서드만 재정의 했습니다.
- 위처럼 삭속을 사용하면 새로운 기능이 추가되더라도 쉽게 구현할 수 있습니다.
배열로 고객 5명 구현하기
예제 8-18 배열을 활용한 고객 관리 프로그램 구현하기
public class CustomerTest {
public static void main(String[] args) {
ArrayList<Customer> customerList = new ArrayList<Customer>();
Customer ctYu = new Customer(10010, "유중혁");
Customer ctKong = new Customer(10020, "공필두");
Customer ctLee = new GoldCustomer(10110, "이길영");
Customer ctJung = new GoldCustomer(10120, "정희원");
Customer ctKim = new VIPCustomer(10210, "김독자", 777);
// ArrayList의 add 속성을 사용해 객체 배열에 고객 추가
customerList.add(ctYu);
customerList.add(ctKong);
customerList.add(ctLee);
customerList.add(ctJung);
customerList.add(ctKim);
System.out.println("==================== 고객 정보 출력 ====================");
for(Customer customer : customerList) {
System.out.println(customer.showCustomerInfo());
}
System.out.println("================ 할인율과 보너스 포인트 계산 ================");
int price = 10000;
// 다형성 구현
for(Customer customer : customerList) {
int cost = customer.calcPrice(price);
System.out.println(customer.getCustomerName() + "님이 "
+ cost + "원 지불하셨습니다.");
System.out.println(customer.showCustomerInfo());
}
}
}

- 만약 재정의한 메서드가 가상 메서드 방식에 의해 자동으로 호출되지 않는다면 if-else if문을 사용하여 각 자료형에 적합한 코드를 따로 구현해야할 것입니다.
- 새로운 등급의 고객이 추가로 필요한 경우에는 또 다른 조건을 구현해야 하므로 코드의 유지보수가 어려워집니다.
- 위와 같은 영우 상속과 다형성을 잘 활용하면 복잡한 코드를 간결하게 줄일 수 있고 확장성 있는 프로그램을 구현할 수 있습니다.
5-2 상속은 언제 사용할까?
상속을 항상 사용하는 것이 좋을까?
- 당연히 그렇지 않습니다.
- 'IS-A 관계(is a relationship; inheritance)'란 일반적인 개념과 구체적인 개념의 관계입니다.
- 상속은 IS-A 관계에서 사용하는 것이 가장 효율적입니다.
- 일반 클래스를 점차 구체화하는 상황에서 상속을 사용하는 것입니다.
- 상속을 사용하면 많은 장점이 있지만, 하위 클래스가 상위 클래스형에 종속되기 때문에 이질적인 클래스 간에는 상속을 사용하지 않는 것이 좋습니다.
- 'HAS-A 관계(has a relationship; association)'란 한 클래스가 다른 클래스를 소유한 관계
- 재사용할 수 있는 코드가 있다고 해서 무조건 상속을 받는 것은 아니므로, 상속을 코드 재사용의 개념으로 이해하면 안 됩니다.
- 상속을 사용하면 클래스 간의 결합도가 높아져서 상위 클래스의 변화가 하위 클래스에 미치는 영향이 큽니다.
- 상속은 '일반적인 클래스'와 '구체적인(확장되는) 클래스'의 관계에서 구현하는 것이 맞습니다.
6. 다운 캐스팅과 instanceof
6-1 하위 클래스로 형 변환, 다운 캐스팅
- 다운 캐스팅(down casting)이란 상위 클래스로 형 변환되었던 하위 클래스를 다시 원래 자료형으로 형 변환하는 것입니다.
6-2 instanceof
- instanceof는 다운 캐스팅을 하기전에 상위 클래스로 형 변환된 인스턴스의 원래 자료형을 확인해야 변환할 때 오류를 막을 수 있는데 이를 확인하는 예약어입니다.
- instanceof는 아래와 같이 사용할 수 있습니다.

- 위 코드에서 사용한 참조 변수 hAnimal은 원래 Human 형으로 생성되었는데, Animal형으로 형 변환되었습니다.
- instanceof 예약어는 왼쪽에 있는 변수의 원래 인스턴스형이 오른쪽 클래스 자료형인가를 확인합니다.
- 상위클래스는 묵시적으로 형 변환되지만, 하위 클래스로 형 변환을 할 때는 명시적으로 해야 합니다.
- 참조 변수의 원래 인스턴스형을 정확히 확인하고 다운 캐스팅을 해야 안전하며 이때 instanceof를 사용합니다.
예제 8-19 instanceof로 원래 인스턴스형 확인 후 다운 캐스팅하기
// 상위 클래스 Animal
class Animal {
public void move() {
System.out.println("동물이 움직입니다.");
}
}
// Animal을 상속받은 Human 클래스
class Human extends Animal{
public void move() {
System.out.println("사람이 두 발로 걷습니다.");
}
public void readBoo() {
System.out.println("사람이 책을 읽습니다.");
}
}
// Animal을 상속받은 Tiger 클래스
class Tiger extends Animal{
public void move() {
System.out.println("호랑이가 네 발로 걷습니다.");
}
public void hunting() {
System.out.println("호랑이가 사냥을 합니다.");
}
}
//Animal을 상속받은 Eagle 클래스
class Eagle extends Animal{
public void move() {
System.out.println("독수리가 하늘을 납니다.");
}
public void flying() {
System.out.println("독수리가 날개를 쭉 펴고 멀리 날아갑니다.");
}
}
public class AnimalTest1 {
// 배열의 자료형은 Animal로 지정
ArrayList<Animal> animalList = new ArrayList<Animal>();
public static void main(String[] args) {
AnimalTest1 aTest = new AnimalTest1();
aTest.addAnimal();
System.out.println("======= 원래 형으로 다운 캐스팅 =======");
aTest.testCasting();
}
public void addAnimal( ) {
// ArrayList에 추가되면서 Animal형으로 반환
animalList.add(new Human());
animalList.add(new Tiger());
animalList.add(new Eagle());
// 배열 요소를 Animal 형으로 꺼내서 move( )를 호출하면 재정의된 함수가 호출됨
for(Animal animal : animalList) {
animal.move();
}
}
public void testCasting() {
for(int i = 0; i < animalList.size(); i++) {
Animal animal = animalList.get(i);
if(animal instanceof Human) {
Human human = (Human)animal;
human.readBoo();
} else if(animal instanceof Tiger) {
Tiger tiger = (Tiger)animal;
tiger.hunting();
} else if(animal instanceof Eagle) {
Eagle eagle = (Eagle)animal;
eagle.flying();
} else {
System.out.println("지원되지 않는 형식입니다.");
}
}
}
}

- 각 동물 클래스를 인스턴스로 생성하여 Animal형으로 선언한 배열에 추가합니다.
- 이렇게 하면 배열에 추가되는 요소의 자료형은 모두 Animal형으로 변환됩니다.
- 호출할 수 있는 메서드는 Animal 클래스에 선언된 메서드 뿐입니다.
- 즉 자료형이 Animal형인 상태에서는 하위 클래스가 제공하는 메서드를 호출할 수 없습니다.
- 다시 원래 자료형으로 다운 캐스팅되어야 호출할 수 있습니다.