Java에서의 상속과 포함 관계
상속이란 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해서 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 재사용성을 높이고 중복을 제거하여 생산성과 유지보수에 도움이 된다.
자바에서 상속을 구현하는 방법은 다음과 같다.
// 상속 받는 클래스(자식 클래스) 이름 뒤에 상속해주는 클래스(부모 클래스) 이름을
// extends 키워드 뒤에 명시
class 자식 클래스 extends 부모 클래스 { ... }
클래스 간의 상속 관계를 그림으로 표현한 상속 계층도(class hierarchy)로 나타내면 아래와 같다.
자식 클래스는 부모 클래스의 모든 멤버를 상속받기 때문에 자식 클래스는 부모 클래스의 멤버들을 포함한다고 할 수 있다. 단, 생성자와 초기화 블럭은 상속되지 않는다. 부모 클래스의 변경은 자식 클래스에 자동적으로 반영되지만, 자식 클래스가 변경되는 것은 부모 클래스에 아무런 영향을 주지 않는다.
자식 클래스의 인스턴스를 생성하면 조상 클래스의 멤버도 함께 따라 오기 때문에 따로 조상 클래스의 인스턴스를 생성하지 않고도 조상 클래스의 멤버들을 사용할 수 있다.
public class Parent {
String name;
public Parent(){
System.out.println("Parent");
}
}
///////////////////////////////////////////////////////////////////////////
public class Child extends Parent{
public Child() { System.out.println("Child"); }
public static void main(String[] args) {
Child c = new Child();
c.name = "김철수";
System.out.println(c.name);
}
}
다른 객체지향언어에서는 여러 부모 클래스로부터 상속 받을 수 있는 다중 상속(multiple inheritance)을 허용하는 경우가 있지만 자바에서는 다중 상속을 할 수 없기 때문에 하나의 클래스로부터만 상속을 받을 수 있다. 다중 상속은 여러 클래스로부터 상속을 받아 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다는 장점이 있지만, 클래스 간의 관계가 복잡해지고 상속 받은 클래스의 멤버간 이름이 같은 경우 구별하기 어려운 모호함(다이아몬드 문제)을 가지고 있다는 단점이 있다. 자바에서는 이런 문제를 없애기 위해 단일 상속만을 허용하고 있다.
https://www.geeksforgeeks.org/diamond-problem-solution/
this()
처럼 super()
도 생성자이다. this()
는 같은 클래스의 다른 생성자를 호출하는데 사용되지만 super()
는 부모의 생성자를 호출하는데 사용된다. 아래 코드를 보면 Child 클래스에서 name
을 호출할 때 자신을 가리키는 참조 변수인 this
를 사용하고 있지만 사실 여기서 this
는 Child 클래스의 부모인 Parent 클래스를 가리킨다. 변수 name
은 Parent 클래스의 멤버이기 때문이다.
public class Parent {
String name;
public Parent(){
System.out.println("Parent default constructor");
}
public Parent(String name) {
this.name = name;
System.out.println("Parent constructor with a parameter");
}
}
///////////////////////////////////////////////////////////////////////////
public class Child extends Parent{
int age;
public Child() {
System.out.println("Child default constructor");
}
public Child(String name, int age) {
this.name = name; // 부모 클래스의 멤버를 초기화
this.age = age;
System.out.println("Child constructor with parameters");
}
public static void main(String[] args) {
Child c = new Child("김철수", 10);
System.out.println("name: " + c.name + ", age: " + c.age);
}
}
틀린 코드는 아니지만 자신의 멤버 변수는 자신의 생성자가 초기화를 책임지도록 작성하는 것이 좋기 때문에 다음과 같이 작성하는 것이 바람직하다.
public Child(String name, int age) {
super(name); // 부모 클래스의 생성자 public Parent(String name){}를 호출
this.age = age; // 자신의 멤버를 초기화
System.out.println("Child constructor with parameters");
}
super
는 자식 클래스에서 부모 클래스로부터 상속 받은 멤버를 참조하는데 사용되는 참조 변수이다. 멤버 변수와 지역 변수의 이름이 같을 때 this
를 붙여서 구별했듯이 상속 받은 멤버와 자신의 멤버의 이름이 같을 때는 super
를 붙여서 구별할 수 있다. 모든 인스턴스 메서드에는 자신이 속한 인스턴스의 주소가 저장된 this
와 super
가 지역 변수로 존재한다.
public class Parent {
String name = "김영철";
public Parent(){
System.out.println("Parent default constructor");
}
}
///////////////////////////////////////////////////////////////////////////
public class Child extends Parent{
String name = "김철수";
public Child() {
System.out.println("Child default constructor");
System.out.println("name: " + super.name);
System.out.println("name: " + this.name);
}
public static void main(String[] args) {
Child c = new Child();
}
}
다음과 같이 부모 클래스에만 있는 멤버 변수를 참조하는 경우 name
, this.name
, super.name
모두 같은 변수인 Parent 클래스의 변수 name
을 가리킨다.
public class Parent {
String name = "김영철";
public Parent(){
System.out.println("Parent default constructor");
}
}
///////////////////////////////////////////////////////////////////////////
public class Child extends Parent{
public Child() {
System.out.println("Child default constructor");
System.out.println("name: " + name);
System.out.println("name: " + super.name);
System.out.println("name: " + this.name);
}
public static void main(String[] args) {
Child c = new Child();
}
}
상속 이외에 클래스를 재사용하는 또 다른 방법은 바로 클래스 간에 포함 관계를 맺어 주는 것이다. 클래스 간에 포함 관계를 맺어 주기 위해서는 한 클래스의 멤버 변수로 다른 클래스 타입의 참조 변수를 선언하면 된다. 하나의 거대한 클래스를 작성하는 것보다 단위별로 여러 개의 클래스를 작성한 다음 클래스들을 서로 조립하듯 포함 관계로 재사용하면 보다 간결하게 코드를 작성할 수 있다.
만약 카페(Cafe)라는 클래스를 작성한다고 하면 다음과 같이 작성할 수 있을 것이다.
public class Cafe {
String name;
String location;
String menuName1;
int menuPrice1;
String menuName2;
int menuPrice2;
.
.
.
}
하지만 카페 정보와 메뉴 정보가 뒤섞여 하나의 거대한 클래스가 될 수 있다. 아래와 같이 메뉴(Menu)라는 클래스를 정의해서 메뉴가 추가될 때마다 재사용한다면 클래스가 커지는 것을 방지하고 유지보수에 도움이 될 것이다.
public class Cafe {
String name;
String location;
Menu[] menu = new Menu[10]; // 카페에 10가지의 메뉴가 있는 경우
.
.
.
}
///////////////////////////////////////////////////////////////////////////
public class Menu {
String name;
int price;
.
.
.
}
클래스를 작성하는데 상속 관계를 맺어 줄 것인지 포함 관계를 맺어 줄 것인지 헷갈린다면 다음과 같은 문장을 만들어 확인해 볼 수 있다.
상속 관계: ‘~은/는 ~이다.’ → is-a
포함 관계: ‘~은/는 ~을/를 가지고 있다.’ → has-a
예를 들어 위의 Cafe 클래스의 경우 ‘카페는 메뉴다.’보다는 ‘카페는 메뉴를 가지고 있다.’가 맞는 문장일 것이다. 따라서 Menu 클래스를 상속 받는 것보다 Menu 클래스를 포함 관계로 맺어 주어야 한다.
상속 받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다. 부모 클래스의 메서드가 자식 클래스에 맞지 않는 경우 자식 클래스에서 부모 클래스의 메서드를 오버라이딩할 수 있다.
음료를 나타내는 Beverage 클래스가 있고 Beverage 클래스에는 음료의 가격을 알려주는 calculatePrice 메서드가 있다.
public class Beverage {
String name;
int price;
public Beverage(String name, int price) {
this.name = name;
this.price = price;
}
int calculatePrice() {
return price;
}
}
그리고 Beverage 클래스를 상속 받아 커피를 나타내는 Coffee 클래스를 만들었다. 커피는 에스프레소 샷을 추가할 수 있으며 샷을 한 번 추가할 때마다 500원씩 가격이 올라간다. 커피를 Beverage 클래스의 calculatePrice 메서드로 계산하기엔 맞지 않기 때문에 Coffee 클래스에 맞춰 calculatePrice 메서드를 오버라이딩해야 한다.
public class Coffee extends Beverage{
int extraShot;
public Coffee(String name, int price, int extraShot) {
super(name, price);
this.extraShot = extraShot;
}
int calculatePrice() {
return price + (extraShot * 500);
}
public static void main(String[] args) {
Coffee coffee = new Coffee("아메리카노", 3000, 2);
System.out.println( coffee.name + ": " + coffee.calculatePrice() + "원");
// 실행 결과) 아메리카노: 4000원
}
}
오버로딩: 기존에 없는 새로운 메서드를 정의.
오버라이딩: 상속받은 메서드의 내용을 변경.
public class Parent {
String name;
void parentMethod() { System.out.println("Parent Method"); }
}
///////////////////////////////////////////////////////////////////////////
public class Child extends Parent{
int age;
// 오버라이딩
void parentMethod() { System.out.println("Parent Method Overriding"); }
// 오버로딩
void parentMethod(String name) { }
void childMethod() { }
void childMethod(int age) { } // 오버로딩
// void childMethod(){ } -> 에러 'childMethod()' is already defined in 'Child'
}