객체 지향 프로그래밍에는 상속성이라는 특징이 있다. 자바에서는 만들어둔 A
클래스를 상속하여 B
라는 클래스를 만들 수 있는데, 상속이라는 단어에서 알 수 있듯 B
클래스는 A
클래스가 갖고 있는 것, 즉 멤버변수와 메서드를 물려받게 된다. 이것을 A
클래스가 B
클래스에게 상속한다, 혹은 B
클래스가 A
를 상속 받는다고 표현한다.
이런 경우에 A
클래스를 부모 클래스나 상위 클래스라고 하며, B
는 자식 클래스 또는 하위 클래스라고 불린다. 추가로 super 클래스, base 클래스는 부모 클래스와 같은 의미이며, subclass와 derived class는 자식 클래스의 또다른 말이다.
상속관계의 클래스를 그림으로 표현할 때는 위와 같이 표현한다. 화살표의 방향이 하위 클래스에서 상위 클래스로 향한다는 점을 주의해야한다.
class B extend A {
}
상속을 구현할 때는 extend
예약어를 사용하는데, extend
에는 확장한다는 의미가 있다. 부모 클래스에서 속성과 기능을 확장하여 자식 클래스를 구현한다는 뜻을 담고 있는 것이다.
자식 클래스가 부모 클래스보다 좀 더 구체적인 클래스가 되며, 의미적으로도 일반화-구체화 관계에 있는 클래스에서 상속을 사용한다. 예를 들어 포유류(부모)와 사람(자식), 탈 것(부모)과 트럭(자식)과 같은 관계에서 사용하는 것이다.
Customer
클래스와, Customer
클래스를 상속받은 VIPCustomer
클래스를 구현해보았다.
Customer
클래스 (상위 클래스)package inheritance;
public class Customer {
int customerID;
String customerName;
String customerGrade;
int bonusPoint;
double bonusRatio;
public Customer() {
this.customerGrade = "SILVER";
this.bonusRatio = 0.01;
}
public Customer(int customerID, String customerName) {
this();
this.customerID = customerID;
this.customerName = customerName;
}
void saveBonusPoint(int price) {
bonusPoint += price * bonusRatio;
}
public int finalPrice(int price) {
saveBonusPoint(price);
return price;
}
public String showCustomerInfo() {
return (customerName + " 님의 등급은 " + customerGrade + "(이)며, 보너스 포인트는 " + bonusPoint + " 입니다.");
}
}
VIPCustomer
클래스 (하위 클래스)package inheritance;
public class VIPCustomer extends Customer {
private int agentID;
double saleRatio;
public VIPCustomer() {
this.customerGrade = "VIP";
this.bonusRatio = 0.05;
this.saleRatio = 0.1;
}
public VIPCustomer(int id, String name, int agentID) {
this();
this.customerID = customerID;
this.customerName = customerName;
this.agentID = agentID;
}
public int getAgentID() {
return agentID;
}
public void setAgentID(int agentID) {
this.agentID = agentID;
}
}
CustomerTest
package inheritance;
public class CustomerTest {
public static void main(String[] args) {
Customer customerLee = new Customer(10010, "Lee");
VIPCustomer customerKim = new VIPCustomer(10020, "Kim", 1004);
customerLee.bonusPoint = 1000;
customerKim.bonusPoint = 10000;
customerKim.getAgentID();
System.out.println(customerLee.showCustomerInfo());
System.out.println(customerKim.showCustomerInfo());
System.out.println("VIP 고객님의 담당자 번호는 " + customerKim.getAgentID() + " 입니다.");
}
}
Lee 님의 등급은 SILVER(이)며, 보너스 포인트는 1000 입니다.
Kim 님의 등급은 VIP(이)며, 보너스 포인트는 10000 입니다.
VIP 고객님의 담당자 번호는 1004 입니다.
VIPCustomer
클래스는 Customer
클래스를 상속 받았기 때문에, 따로 선언하지 않아도 Customer
클래스의 멤버변수와 메소드를 그대로 사용할 수 있다. 거기에 더해서 VIPCustomer
클래스에서만 사용할 멤버변수와 메서드를 추가로 선언해서 사용하는 것도 가능하다.
위에서 계속 언급 했듯 하위 클래스는 별도의 선언 없이 상위 클래스의 멤버변수와 메소드를 사용할 수 있다. 그렇다는 것은 상위 클래스의 멤버변수가 메모리에 할당되었다는 것이다.
특정 클래스의 멤버변수는 클래스의 인스턴스가 생성되어 힙메모리에 할당되었을 때 접근이 가능한데, 상속의 경우 하위 클래스의 인스턴스만 생성해도 상위 클래스의 멤버 변수를 사용할 수 있게 된다. 하위 클래스가 생성될 때 어떤 일이 발생하는 걸까?
Customer
클래스의 생성자package inheritance;
public class Customer {
int customerID;
String customerName;
String customerGrade;
int bonusPoint;
double bonusRatio;
public Customer() {
this.customerGrade = "SILVER";
this.bonusPoint = 0;
this.bonusRatio = 0.01;
System.out.println("This is Customer()");
}
public Customer(int customerID, String customerName) {
this();
this.customerID = customerID;
this.customerName = customerName;
System.out.println("This is Customer(customerID, customerName)");
}
}
VIPCustomer
클래스의 생성자package inheritance;
public class VIPCustomer extends Customer {
private int agentID;
double saleRatio;
public VIPCustomer() {
this.customerGrade = "VIP";
this.bonusRatio = 0.05;
this.saleRatio = 0.1;
System.out.println("This is VIPCostomer()");
}
public VIPCustomer(int customerID, String customerName, int agentID) {
this();
this.customerID = customerID;
this.customerName = customerName;
this.agentID = agentID;
System.out.println("This is VIPCostomer(customerID, customerName, agentID)");
}
}
CustomerTest
package inheritance;
public class CustomerTest {
public static void main(String[] args) {
System.out.println("==================== 상위 클래스 생성 ====================");
Customer customer = new Customer();
System.out.println("------------------------------------------------------");
Customer customerLee = new Customer(10010, "Lee");
System.out.println("\n==================== 하위 클래스 생성 ====================");
VIPCustomer customerVIP = new VIPCustomer();
System.out.println("------------------------------------------------------");
VIPCustomer customerKim = new VIPCustomer(10020, "Kim", 1004);
System.out.println("======================================================");
}
}
==================== 상위 클래스 생성 ====================
This is Customer()
------------------------------------------------------
This is Customer()
This is Customer(customerID, customerName)
==================== 하위 클래스 생성 ====================
This is Customer()
This is VIPCostomer()
------------------------------------------------------
This is Customer()
This is VIPCostomer()
This is VIPCostomer(customerID, customerName, agentID)
======================================================
생성되는 순간을 확인하기 위해 생성자에 System.out.println()
함수를 넣고 클래스을 생성해보면, 하위 생성자를 호출할 때, 상위 클래스의 생성자를 먼저 호출하고 그 뒤에 하위 클래스의 생성자를 호출한다는 것을 알 수 있다.
하위 클래스의 생성 이전에 상위 클래스가 생성되어 상위 클래스의 멤버 변수 공간이 메모리에 할당되기 때문에 하위 클래스에서도 상위 클래스의 멤버 변수를 사용할 수 있게 된다.
출력결과에 This is Customer()
와 This is VIPCustomer()
가 나타난 이유는 this()
에 의해 Customer()
생성자가 호출되었기 때문이다. this
가 자기자신의 참조값을 담고 있는 것처럼 상위 클래스의 참조값을 담고 있는 예약어도 존재하는데, 이 예약어의 이름은 super
이다.
super
하위 클래스는 상위 클래스의 주소를 알고 있고, super
가 이 참조값을 담고 있다. 즉, super
는 하위 클래스에서 상위 클래스에 접근하기 위해 사용하는 예약어다. 하위 클래스는 super
를 통해 상위 클래스 생성자를 호출하거나 상위 클래스의 멤버변수와 메서드를 사용할 수 있다.
super
하위 클래스의 생성자에 상위 클래스의 생성자 super()
가 별도로 명시되어있지 않다면, 파일이 바이트코드로 변환되기 전에 하위 클래스 생성자의 첫줄에 super()
가 삽입된다.
자동으로 삽입되는 상위 클래스의 생성자는 오직 매개변수가 없는 디폴트 생성자만 가능하므로, 만약 상위 클래스에 디폴트 생성자가 존재하지 않는다면, super
를 사용하여 상위 클래스의 생성자를 명시적으로 직접 적어주어야한다.
예를 들어 Customer
클래스에 디폴트 생성자가 없다면 VIPCustomer
클래스의 생성자의 첫줄에 super(customerID, customerName)
를 적으면 된다.
super
this
를 사용했던 것과 동일하게 super
를 통해 상위 클래스의 멤버변수와 메서드를 참조할 수 있다.
VIPCustomer
클래스를 예를 들자면, VIPCustomer
클래스에만 존재하는 agentID
의 경우 this
로만 접근할 수 있지만, 상위 클래스인 Customer
에 선언된 customerID
, customerName
같은 경우 this.customerID
, super.customerID
둘 중 어떤 것을 사용해도 무방하며, customerID
을 단독으로 사용해도 상관없다.
하지만 하위 클래스와 상위클래스에 같은 이름의 멤버변수나 메서드가 있다면 어떤 클래스의 것을 사용할지 this
와 super
를 구분하여 사용해야한다.
같은 이름의 메서드에 관해서는 오버라이딩에서 더 자세히 다룰 예정이다. (메서드 이름은 같고 매개변수가 다른 오버로딩과는 다르니 주의.)
protected
지금 다루고자하는 protected
예약어는 접근 지정자에서 등장했던 그 protected
가 맞다.
protected
동일 패키지의 클래스와 자식 클래스에서만 접근 가능해주는 접근 지정자
외부 패키지의 클래스로부터의 접근을 막고 싶어 public
대신 default
(접근 지정자 생략)를 사용할 수 있다. 하지만 만약 자식 클래스가 외부 패키지에 존재한다면, 자식 클래스는 부모 클래스의 속성과 기능을 물려받았음에도 불구하고 디폴트로 선언된 멤버변수와 메서드를 사용할 수 없게 된다.
이런 경우 사용하는 것이 protected
예약어이다. protected
접근 지정자로 선언할 경우, 동일 패키지의 클래스뿐만 아니라, 자식 클래스라면 외부 패키지에 위치해있어도 접근할 수 있게 해준다.
private
으로 선언한다면 자식 클래스를 불문하고, 자기자신을 제외한 모든 클래스의 접근을 막기 때문에, 자식 클래스에서 사용하게 하고 싶은 멤버변수와 메서드가 있다면 적어도 protected
로 선언해야한다.
Customer
클래스 수정하기package inheritance;
public class Customer {
protected int customerID;
protected String customerName;
protected String customerGrade;
int bonusPoint;
double bonusRatio;
public Customer() {
this.customerGrade = "SILVER";
this.bonusPoint = 0;
this.bonusRatio = 0.01;
System.out.println("This is Customer()");
}
public Customer(int customerID, String customerName) {
this();
this.customerID = customerID;
this.customerName = customerName;
System.out.println("This is Customer(customerID, customerName)");
}
protected void saveBonusPoint(int price) {
bonusPoint += price * bonusRatio;
}
public int finalPrice(int price) {
saveBonusPoint(price);
return price;
}
public String showCustomerInfo() {
return (customerName + " 님의 등급은 " + customerGrade + "(이)며, 보너스 포인트는 " + bonusPoint + " 입니다.");
}
}
업캐스팅
상위 클래스로 묵시적 형 변환하는 것
하위 클래스는 상위 클래스의 속성과 기능을 모두 갖고 있으며, 하위 클래스는 상위 클래스를 구체화 시킨 클래스이므로, 일반적으로 상위 클래스보다 기능이 많다. 따라서 하위 클래스는 상위 클래스를 내포하고 있다.
즉, VIPCustomer
클래스는 VIPCustomer
클래스 자료형이면서, Customer
클래스 자료형라고 할 수 있다. 현실 세계에서 사람(하위)을 포유류(상위)라고 할 수 있는 것과 같다. (그러나 포유류를 사람이라고 할 수는 없다.)
이러한 특성 때문에, 인스턴스는 VIPCustomer
클래스로 생성하더라도 선언은 Customer
클래스로 하는 것이 가능하다.
Customer upCastingWoo = new VIPCustomer();
VIPCustomer Woo = new VIPCustomer();
Customer upCastingWoo = Woo;
다운캐스팅
업캐스팅 된 클래스를 다시 원래 자료형으로 형변환하는 것
VIPCustomer
클래스는 Customer
클래스 자료형의 모든 기능을 포함하고 있지만 Customer
클래스는 VIPCustomer
클래스의 기능을 전부 가지고 있지 못하기 때문에 역으로 상위 클래스로 인스턴스를 생성할 때는 하위 클래스로 선언 할수는 없다.
/* 컴파일 오류 발생 */
VIPCustomer test = new Customer();
그러나, 후에 다루겠지만, 이미 업캐스팅한 인스턴스를 하위 클래스(인스턴스의 본래 클래스)로 형변환 하는 다운캐스팅은 가능하다. 업캐스팅의 경우 클래스명을 적지않아도 묵시적인 형 변환이 가능한 반면, 다운캐스팅 할 때는 변수 앞에 캐스팅할 하위 클래스 명을 명시적으로 적어야한다.
VIPCustomer downCastingWoo = (VIPCustomer)upCastingWoo;
먼저 인스턴스는 하위 클래스의 생성자에 의해 생성되므로, 메모리 공간에는 상위 클래스의 멤버변수와 하위 클래스의 멤버변수가 모두 할당되어 있다.
하지만 변수의 클래스 자료형을 기반으로 멤버변수와 메서드에 접근하기 때문에 업캐스팅한 경우, 하위 클래스만의 멤버변수와 메서드는 사용할 수 없고, 상위 클래스의 멤버변수와 메서드에만 접근할 수 있다.
오버라이딩 (Overriding)
상위 클래스의 메서드를 하위 클래스에서 재정의 하는 것
상위 클래스에 이미 정의되어 있는 메서드를 하위 클래스에서 다른 기능으로 사용하고 싶을 때, 메서드 오버라이딩을 하면 된다.
예를 들어 지불할 금액을 계산해 반환해주는 int finalPrice(int price)
메서드를 VIPCustomer
클래스에서는 Customer
클래스와 다르게 10% 할인 된 금액을 반환하는 메서드로 만들고 싶은 경우에 VIPCustomer
클래스에서 재정의 하면 되는 것이다.
오버라이딩 할 때는 메서드의 이름, 매개변수의 개수와 자료형, 반환값 자료형이 모두 동일해야한다. 하나라도 다르면 자바 컴파일러는 각각 다른 메서드로 인식하게 되므로 오버라이딩했다고 할 수 없다.
Customer
클래스package inheritance;
public class Customer {
protected int customerID;
protected String customerName;
protected String customerGrade;
int bonusPoint;
double bonusRatio;
public Customer() {
this.customerGrade = "SILVER";
this.bonusPoint = 0;
this.bonusRatio = 0.01;
}
public Customer(int customerID, String customerName) {
this();
this.customerID = customerID;
this.customerName = customerName;
}
protected void saveBonusPoint(int price) {
bonusPoint += price * bonusRatio;
}
public int finalPrice(int price) {
saveBonusPoint(price);
return price;
}
public void showCustomerInfo() {
System.out.println(customerName + " 님의 등급은 " + customerGrade + "(이)며, 보너스 포인트는 " + bonusPoint + " 입니다.");
}
}
VIPCustomer
클래스package inheritance;
public class VIPCustomer extends Customer {
private int agentID;
double saleRatio;
public VIPCustomer() {
this.customerGrade = "VIP";
this.bonusRatio = 0.05;
this.saleRatio = 0.1;
}
public VIPCustomer(int customerID, String customerName, int agentID) {
this();
this.customerID = customerID;
this.customerName = customerName;
this.agentID = agentID;
}
@Override
public int finalPrice(int price) {
saveBonusPoint(price);
return (int)(price * (1.0 - saleRatio));
}
@Override
public void showCustomerInfo() {
super.showCustomerInfo();
System.out.println(customerName + " 고객님의 담당자 번호는 " + getAgentID() + " 입니다.");
}
public int getAgentID() {
return agentID;
}
public void setAgentID(int agentID) {
this.agentID = agentID;
}
}
CustomerTest
package inheritance;
public class CustomerTest {
public static void main(String[] args) {
Customer customerLee = new Customer(10010, "Lee");
VIPCustomer customerKim = new VIPCustomer(10020, "Kim", 1004);
customerLee.bonusPoint = 1000;
customerKim.bonusPoint = 10000;
customerLee.showCustomerInfo();
customerKim.showCustomerInfo();
System.out.println();
int price = 10000;
System.out.println(customerLee.getCustomerName() + " 님이 지불해야하는 금액은 " +
customerLee.finalPrice(price) + " 원 입니다.");
System.out.println(customerKim.getCustomerName() + " 님이 지불해야하는 금액은 " +
customerKim.finalPrice(price) + " 원 입니다.");
}
}
Lee 님의 등급은 SILVER(이)며, 보너스 포인트는 1000 입니다.
Kim 님의 등급은 VIP(이)며, 보너스 포인트는 10000 입니다.
Kim 고객님의 담당자 번호는 1004 입니다.
Lee 님이 지불해야하는 금액은 10000 원 입니다.
Kim 님이 지불해야하는 금액은 9000 원 입니다.
애노테이션은 주석이라는 의미를 갖고 있는데 메서드를 오버라이딩할 때 사용했던 @Override
또한 애노테이션의 한 종류이다. 애노테이션은 @
와 함께 사용하며 컴파일러에게 특정한 정보를 제공한다. @Override
의 경우, 어떤 메소드가 재정의된 메소드인지 컴파일러에게 알려주는 것이다.
@Override
를 사용해야만 재정의가 가능한 것은 아니지만 오버라이딩된 메서드의 선언부가 다르다면 컴파일 상에서 오류가 나므로 프로그래머의 실수를 줄이기에 용이하다.
오버로딩 (Overloading)
같은 이름의 메서드를 매개변수만 다르게 해서 정의하는 것
오버라이딩과 오버로딩은 의미나 철자가 비슷하므로 헷갈리지 않도록 주의한다. 오버로딩은 반환값 자료형이 달라도 상관없다.
만약 하위 클래스를 업캐스팅한 후에 오버라이딩한 메서드를 참조한다면, 그 메서드는 상위 클래스의 메서드와 하위 클래스 중 어떤 메서드를 참조하게 될까? 결론부터 말하자면, 생성된 인스턴스 자료형(하위 클래스)을 기반으로 메서드가 호출된다.
public static void main(String[] args) {
Customer jwoo = new VIPCustomer(10000, "jwoo", 1004);
customerUp.showCustomerInfo();
}
jwoo 님의 등급은 VIP(이)며, 보너스 포인트는 0 입니다.
jwoo 고객님의 담당자 번호는 1004 입니다.
하위 클래스의 생성자에 의해 생성되므로, 등급은 VIP
로 설정되고, 메서드는 인스턴스 자료형의 메서드를 참조한다. 이처럼 인스턴스 자료형의 메서드가 호출되는 것을 가상 메서드라고 한다.
인스턴스가 생성되면 인스턴스는 메모리 공간에 할당되는데, 이 메모리 공간은 클래스의 멤버변수를 위한 공간이다. 클래스의 메서드는 멤버변수와는 다른 메모리를 사용한다. 멤버변수는 인스턴스가 생성될 때마다 새로 생성되지만(같은 클래스로 생성한 인스턴스라도 인스턴스마다의 멤버변수는 각각 다른 메모리 공간에 위치), 인스턴스가 달라도 동일한 메소드를 호출한다.
자바의 모든 메서드는 가상 메서드로, 가상 메서드 테이블이 존재한다. 가상 메서드에는 메서드의 실제 주소값과 메서드의 이름이 짝을 이루고 있는데, 어떤 메서드가 호출되면 이 테이블에서 주소값을 찾아 참조하게 된다.
오버라이드된 메서드의 경우 메서드의 이름은 같지만, 실제로 다른 기능을 하는 함수이기 때문에 다른 함수이며, 함수의 주소값 또한 당연히 다르다. 즉, Customer
클래스의 가상 메서드 테이블에서 showCustomerInfo()
라는 메소드의 이름과 짝을 이루고 있는 메서드의 메모리 주소와, Customer
클래스의 가상 메서드 테이블에서 showCustomerInfo()
와 짝을 이루고 있는 메모리 주소는 다르다는 것이다.
반면 재정의 되지 않은 saveBonusPoint(int price)
메서드의 경우, Customer
클래스와 VIPCustomer
클래스의 가상 메서드 테이블 모두 같은 메모리 주소를 가지고 있다.
이러한 원리 때문에 업캐스팅 여부와 상관없이 오버라이딩된 메서드를 호출하면 인스턴스 자료형에 맞는 메서드가 호출되는 것이다.
자바는 다중상속을 허용하고 있지 않다.
다중상속이란 여러 상위 클래스를 상속받는 것을 말하는데 그림으로 나타내면 다음과 같다.
다중 상속을 허용하게 되면 특정 상황에서 문제가 발생하게 되는데, 이를 다이아몬드 문제라고 하며, 그 특정 상황은 아래 그림과 같이 다이아몬드 형태를 띄기 때문에 붙여진 이름이다.
ParentA
와 ParentB
클래스는 GrandParent
클래스를 상속하여 구현한 클래스이다. 그리고 ParentA
와 ParentB
클래스를 다중 상속하여 Child
클래스를 정의했다. 그리고 최상위 클래스 GrandParent
에는 x()
라는 이름의 메서드가 있다고 해보자.
만약 이 x
메서드를 ParentA
와 ParentB
클래스에서 각각 오버라이딩했다면 어떨까? Child child = new Child()
를 통해 Child
인스턴스를 생성하고, child.x()
로 x
메소드를 호출했다면, ParentA
와 ParentB
클래스 중 어떤 클래스의 x
메서드가 호출되는 걸까?
바로 이러한 충돌 상황을 방지하기 위해 자바에서는 다중 상속을 막고 있으며, 다중상속을 시도하면 컴파일 오류가 날 것이다. C++
에서는 다중상속을 프로그래머의 선택으로 남겨놓고 있지만, 마찬가지로 다이아몬드 문제가 발생하면 컴파일 오류가 난다.
자바에서 다중상속 막은게 저 이유 때문이었군요.. 잘 보고 갑니다.