상속
클래스를 정의할 때 이미 구현된 클래스를 상속받아서 속성이나 기능이 확장되는 클래스를 구현함.
상속하는 클래스 : 상위 클래스, parent class, base class, super class
상속받는 클래스 : 하위 클래스, child class, derived class, subclass
클래스 상속 문법
class B extends A{
//코드내용~
//super()이나 등등.. 작성
}
상속을 사용하는 이유 : 코드의 중복을 줄일 수 있고, 적은 양으로 새로운 클래스를 구현할 수 있다는 장점이 있다.
상위 클래스는 하위 클래스보다 일반적인 의미를 가짐. ex) 포유류
하위 클래스는 상위 클래스보다 구체적인 의미를 가짐. ex) 사람
class Mammal{//부모
}
class Human extends Mammal{//자식
}
extends 뒤에는 단 하나의 class만 사용할 수 있음. 자바는 single inheritage 만을 지원함.
상속을 활용한 고객 관리 프로그램을 구현해 보자.
package com.kh.inheritance;
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 + "입니다.";
}
}
위에서 사용한 메서드들을 알아보자.
메서드 | 설명 |
Customer() | 기본 생성자이다. 고객 한명이 새로 생성되면 customerGrade는 Silver이고, bonusRatio는 1%로 지정한다. |
calcPrice(int price) | 제품에 대해 지불해야 하는 금액을 계산하여 반환한다. 할인되지 않는 경우 가격을 그대로 반환한다. 그리고 가격에 대해 보너스 포인트 비율을 적용하여 보너스 포인트를 적립한다. |
showCustomerInfo() | 고객 정보를 출력. 고객이름, 등급, 현재 적립된 포인트를 return해 준다. |
여기에서 따로 VIPCustomer 클래스를 만들고 싶다고 할 때,
원래라면 아래와 같이 코드들을 전부 작성해 주어야 한다.
package com.kh.inheritance;
public class VIPCustomer {
//이 아래부분은 Customer Class와 아주 많이 겹치는 부분이다.
private int customerID;
private String customerName;
private String customerGrade;
int bonusPoint;
double bonusRatio;
private int agentID;
double saleRatio;
public VIPCustomer() {
customerGrade = "VIP";
bonusRatio = 0.05;
saleRatio = 0.1;
}
public int calcPrice(int price) {
bonusPoint+=price*bonusRatio;
return price - (int)(price * saleRatio);
}
public int getAgentID() {
return agentID;
}
public String showCustomerInfo() {
return customerName + " 님의 등급은 " + customerGrade +
" 이며, 보너스 포인트는 "+bonusPoint + " 입니다.";
}
}
하지만 멤버 변수와 생성자, 메서드들이 Customer 클래스와 대부분이 동일하다. 이럴 때는 바로 상속(Extends)를 이용해 주어야 한다.
위의 코드들을 수정해서 아래와 같이 다시 작성해 보자. 덤으로 작성하지 않아도 되는 부분을 주석처리 해 보자.
package com.kh.inheritance;
public class VIPCustomer extends Customer{
//이 아래부분은 Customer Class와 겹치는 부분이다.
// private int customerID;
// private String customerName;
// private String customerGrade;
// int bonusPoint;
// double bonusRatio;
private int agentID;
double saleRatio;
public VIPCustomer() {
customerGrade = "VIP";//*****이 부분에서 오류 발생*****
bonusRatio = 0.05;
saleRatio = 0.1;
}
// public int calcPrice(int price) {
// bonusPoint+=price*bonusRatio;
// return price - (int)(price * saleRatio);
// }
public int getAgentID() {
return agentID;
}
// public String showCustomerInfo() {
// return customerName + " 님의 등급은 " + customerGrade +
// " 이며, 보너스 포인트는 "+bonusPoint + " 입니다.";
// }
}
상위 클래스 변수를 사용하기 위한 protected 예약어
위 코드를 보면 작성하지 않아도 되는 코드들이 많아진 것을 볼 수 있다.
하지만 customerGrade = "VIP"; 부분에서 에러가 나타나는데,
이렇게 에러가 나타나는 이유는 부모 멤버 변수에서 접근 제어자를 protected로 사용해 주지 않아서 그렇다.
Customer클래스에서 private 접근 제어자를 사용해 버리면 외부 클래스에서 이 멤버 변수를 사용할 수 없다.
protected 접근 제어자로 바꾸어 주면 오류 없이 코드가 잘 작성이 된다.
protected int customerID;
protected String customerName;
protected String customerGrade;
여기에서 private 접근 제어자와 protected 접은 제어자의 차이를 볼 수 있다.
private 접근 제어자는 외부에서 사용할 수 없지만, 하위 클래스에서도 사용이 불가능한 반면, protected 접근 제어자는 외부에서 사용할 수 없지만, 하위 클래스에서는 사용이 가능하다.
상속받은 하위 클래스에서는 public처럼 사용할 수 있는 것이다.
즉, protected는 상속된 하위 클래스를 제외한 나머지 외부 클래스에서는 private와 동일한 역할을 한다.
protected 접근 제어자로 선언한 멤버변수는 하위 클래스가 아닌 외부 클래스에서 private와 마찬가지로 get(), set() 메소드를 사용해야 멤버 변수에 접근할 수 있다.
그럼 이제 메인 함수(외부 클래스)에서 방금 만든 클래스들을 사용해 주기 위해 Customer에서 get(), set() 메소드들을 생성해 주고, 메인 함수를 작성해 보자.
package com.kh.inheritance;
public class CustomerTest {
public static void main(String[] args) {
Customer customerLee = new Customer(); //Customer클래스에서 정의한 디폴트 생성자 사용
customerLee.setCustomerID(10010);
customerLee.setCustomerName("이순신");
customerLee.bonusPoint = 1000;
System.out.println(customerLee.showCustomerInfo());
VIPCustomer customerKim = new VIPCustomer();//상속받은 VIPCustomer()에서 정의한 디폴트 생성자 사용.
customerKim.setCustomerID(10020);
customerKim.setCustomerName("김유신");
customerKim.bonusPoint=10000;
System.out.println(customerKim.showCustomerInfo());
}
}
실행 결과
이순신 님의 등급은 Silver 이며, 보너스포인트는 1000입니다.
김유신 님의 등급은 VIP 이며, 보너스포인트는 10000입니다.
김유신이라는 이름의 인스턴스에서는 agentID와 saleRatio 멤버 변수가 추가로 존재하는 것을 확인할 수 있고, 이는 Customer 클래스로부터 상속받아 확장된 VIPCustomer 클래스 객체의 인스턴스이기 때문이다.
상속에서 클래스 생성과 형 변환
하위 클래스가 생성될 때는 상위 클래스가 먼저 생성이 된다.
방금 작성한 코드에서 하위 클래스의 생성자와 상위 클래스의 생성자에 출력문을 작성해 보자.
//Customer 클래스의 생성자
public Customer() {
customerGrade ="Silver";
bonusRatio = 0.01;
System.out.println("Customer() 생성.");
}
//VIPCustomer 클래스의 생성자
public VIPCustomer() {
customerGrade = "VIP";
bonusRatio = 0.05;
saleRatio = 0.1;
System.out.println("VIPCustomer() 생성.");
}
출력 결과.
Customer() 생성.
이순신 님의 등급은 Silver 이며, 보너스포인트는 1000입니다.
Customer() 생성.
VIPCustomer() 생성.
김유신 님의 등급은 VIP 이며, 보너스포인트는 10000입니다.
부모로부터 상속받은 자식 클래스에서의 생성자는 부모 클래스의 생성자에 구현되어있는 출력문도 출력이 되는 것을 볼 수 있다.
상위 클래스의 Customer()생성자가 먼저 호출되고 그 다음에 VIPCustomer() 생성자가 호출되는데, 상위 클래스를 상속받은 하위 클래스가 생성될 때는 반드시 상위 클래스의 생성자가 먼저 호출이 된다.
그리고 상위 클래스 생성자가 호출된 때 상위 클래스의 멤버 변수가 메모리에 생성되는 것이다.
출력 후의 힙 메모리(스택 구조)의 상태는 아래와 같다.
customerID |
customerName |
customerGrade |
bonusPoint |
bonusRatio |
agentID |
salesRatio |
부모를 부르는 예약어, super
super 예약어는 하위 클래스에서 상위 클래스로 접근할 때 사용한다.
VIPCustomer 기본 생성자를 구현할 때, super() 메소드가 생략되어 있지만,
하위 클래스에서 매개변수가 다른 새로운 생성자를 추가할 때, super 메소드를 사용해서 구현하여야 한다.
#Customer 클래스의 기본 생성자를 제거하면 super()로 기본 생성자를 불러올 수 없어 에러 메시지가 나타난다.
public Customer(int customerID, String customerName) {
this.customerID = customerID;
this.customerName = customerName;
customerGrade = "SILVER";
bonusRatio = 0.01;
System.out.println("Customer(int, String) 생성자 호출.");
}
위처럼 디폴트 생성자가 아닌 생성자를 구현하면, 하위 클래스에서도 매개변수가 존재하는 생성자에 super()메소드를 사용해서 구현한다.
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) 생성.");
}
이제 메인함수로 가서 홍길동 이라는 VIPCustommer 객체 인스턴스를 매개변수 3개를 입력하고 만들어 보자.
VIPCustomer customerHong = new VIPCustomer(10030,"홍길동",12345);
customerHong.bonusPoint = 10000;
System.out.println(customerHong.showCustomerInfo());
실행 결과
Customer(int, String) 생성자 호출.
VIPCustomer(int, String, int) 생성.
홍길동 님의 등급은 VIP 이며, 보너스포인트는 10000입니다.
메소드 상속
public String showVIPInfo() {
return super.showCustomerInfo() + "담당 상담원의 아이디는 "+this.agentID+ " 입니다.";
}
실행 결과
홍길동 님의 등급은 VIP 이며, 보너스포인트는 10000입니다.담당 상담원의 아이디는 12345 입니다.
상위 클래스로부터 묵시적 형 변환 (업캐스팅)
상위 클래스 형으로 변수를 선언하고 하위 클래스 인스턴스를 생성할 수 있음.
하위 클래스는 상위 클래스 타입을 내포하고 있으므로 상위 클래스로 묵시적 형변환이 가능함.
반대로 하위 클래스 형으로 변수를 선언하고 상위 클래스 인스턴스를 생성하면 오류가 나타난다.
Customer vc = new VIPCustomer();
이렇게 선언된 객체 인스턴스 vc는 Customer 클래스의 멤버 변수와 메소드만 사용이 가능하다. (하지만 VIP 멤버변수의 메모리는 생성되어 있다.)
//업캐스팅
Customer customer = new VIPCustomer(12313,"주종훈",1331);
//다운캐스팅
VIPCustomer customer2 = (VIPCustomer) customer;
메소드 오버라이딩
상위 클래스에 정의된 메소드 중 하위 클래스와 기능이 맞지 않거나 추가 기능이 필요한 경우 같은 이름과 매개변수로 하위 클래스에서 재정의함.
Override 어노테이션을 작성하고, 매개변수의 종류나 개수를 다르게 하게 되면 에러가 나타난다. 말 그대로 재정의 하는것인데 다른 메소드를 정의하는 꼴이 되어서 그런 것이다.
매개변수의 종류가 다를 시 Override 어노테이션을 제거하게 되면 , 에러 문구는 사라지게 되지만 이는 재정의하는 것이 아닌 오버로딩 즉, 새로운 메서드를 생성하게 된다.
@Override //정상적인 메소드 오버라이딩.
public int calcPrice(int price) {
bonusPoint+=price*bonusRatio;
return price - (int)(price * saleRatio);
}
// @Override -> 이 함수가 error인 이유 : Override는 재정의인데 매개변수가 달라버리면 Overloading이 되어버림.
// public int calcPrice(int price, int price2) {
// bonusPoint+=price*bonusRatio;
// return price - (int)(price * saleRatio);
// }
// 아래처럼 재정의 어노테이션(@Override)을 제거하게 되면 말 그대로 오버로딩하게되는 새로운 메서드 생성.
// public int calcPrice(int price, int price2) {
// bonusPoint+=price*bonusRatio;
// return price - (int)(price * saleRatio);
// }
새로운 메인함수를 하나 생성하고 테스트해 보자.
package com.kh.inheritance;
public class OverridingTest {
public static void main(String[] args) {
Customer customerLee = new Customer(10010,"이순신");
customerLee.bonusPoint = 1000;
VIPCustomer customerKim = new VIPCustomer(10020,"김유신",12345);
customerKim.bonusPoint = 10000;
int price = 10000;
System.out.println(customerLee.getCustomerName()
+" 님이 지불해야 하는 금액은 "+customerLee.calcPrice(price)+" 입니다.");
System.out.println(customerKim.getCustomerName()
+" 님이 지불해야 하는 금액은 "+customerKim.calcPrice(price)+" 입니다.");
}
}
실행 결과
이순신 님이 지불해야 하는 금액은 10000 입니다.
김유신 님이 지불해야 하는 금액은 9000 입니다.
김유신은 VIP라서 더 싼 가격에 구입할 수 있게 구현 된 것을 볼 수 있다.
묵시적 클래스 형 변환과 매서드 재정의 - 가상 메소드
Customer vc = new VIPCustomer();
vc.calcPrice(10000);
위 실행 결과를 알기 위해서는 가상 메서드에 대해 알아야 한다.
상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정된다. 다시 말해 선언한 클래스형이 아닌 생성된 인스턴스의 메서드를 호출하는 것이다.
가상 메소드란 위에서 말했듯 프로그램에서의 어떤 객체의 변수나 메서드의 참조는 그 타입에 따라 이루어 진다. 가상 메서드의 경우는 타입과 상관없이 실제 생성된 인스턴스의 메소드가 호출되는 원리이다.
vc의 타입은 Customer이지만, 실제 생성된 인스턴스인 VIPCustomer 클래스의 calcPrice() 메소드가 호출된다.
Customer customerHong = new VIPCustomer(10030,"홍길동", 2000);
System.out.println(customerHong.getCustomerName()
+" 님이 지불해야 하는 금액은 "+customerHong.calcPrice(price)+" 입니다.");
홍길동 님이 지불해야 하는 금액은 9000 입니다.
가상 메소드의 원리
가상 메소드의 경우에는 '가상 메소드 테이블'이 만들어지는데, 가상 메소드 테이블은 각 메소드 이름과 실제 메모리 주소가 짝을 이루고 있다. 어떤 메소드가 호출되면 이 테이블에서 주소 값을 찾아서 해당 메소드의 명령을 수행한다.
'교육 | 외부활동 > 이론_JAVA' 카테고리의 다른 글
KH자바수업_10_추상 클래스 (2) | 2022.11.17 |
---|---|
KH자바수업_9_다형성 (0) | 2022.11.14 |
KH자바수업_7_다차원 배열, ArrayList (0) | 2022.11.11 |
KH자바수업_6_배열 (0) | 2022.11.10 |
KH자바수업_5_클래스와 객체 2 (0) | 2022.11.09 |