상속 관계가 왜 필요한지 이해하기 위해 다음 예제 코드를 만들어서 실행해보자.
예제 코드
package extends1.ex1;
public class ElectricCar {
public void move() {
System.out.println("move");
}
public void charge() {
System.out.println("charge");
}
}
package extends1.ex1;
public class GasCar {
public void move() {
System.out.println("move");
}
public void fillUp() {
System.out.println("fill up");
}
}
package extends1.ex1;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
}
}
move
charge
move
fill up
전기차(ElectricCar) 와 가솔린차(GasCar)를 만들었다.
전기차는 이동(move()) 충전(charge()) 기능이 있고, 가솔린차는 이동(move()) 주유(fillUp()) 기능이 있다.
전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념이다. 반대로 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념이다. 그래서인지 잘 보면 둘의 공통 기능이 보인다. 바로 이동(move()) 이다.
전기차든 가솔린차든 주유하는 방식이 다른 것이지 이동하는 것은 똑같다. 이런 경우 상속 관계를 사용하는 것이 효과적이다.
상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다. 이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는 것이다.
상속을 사용하려면 extends 키워드를 사용하면 된다. 그리고 extends 대상은 하나만 선택할 수 있다.
[ 용어 정리 ]
상속 관계를 사용하도록 코드를 작성해보자.
package extends1.ex2;
public class Car {
public void move() {
System.out.println("move");
}
}
Car 는 부모 클래스가 된다. 여기에는 자동차의 공통 기능인 move() 가 포함되어 있다.
package extends1.ex2;
public class ElectricCar extends Car {
public void charge() {
System.out.println("charge");
}
}
package extends1.ex2;
public class GasCar extends Car {
public void fillUp() {
System.out.println("fill up");
}
}
package extends1.ex2;
public class CarMain {
public static void main(String[] args) {
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
}
}
move
fill up
move
charge
전기차와 가솔린차가 Car 를 상속 받은 덕분에 electricCar.move() gasCar.move() 를 사용할 수 있다.
참고로 당연한 이야기지만 상속은 부모의 기능을 자식이 물려 받는 것이다. 따라서 자식이 부모의 기능을 물려 받아서 사용할 수 있다. 반대로 부모 클래스는 자식 클래스에 접근할 수 없다. 자식 클래스는 부모 클래스의 기능을 물려 받기 때문에 접근할 수 있지만, 그 반대는 아니다. 부모 코드를 보자! 자식에 대한 정보가 하나도 없다. 반면에 자식 코드는 extends Parent 를 통해서 부모를 알고 있다.
참고로 자바는 다중 상속을 지원하지 않는다. 그래서 extend 대상은 하나만 선택할 수 있다. 부모를 하나만 선택할 수 있다는 뜻이다. 물론 부모가 또 다른 부모를 하나 가지는 것은 괜찮다.
다중 상속 그림
만약 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정해보자. 만약 그림과 같이 다중 상속을 하게 되면 AirplaineCar 입장에서 move() 를 호출할 때 어떤 부모의 move() 를 사용해야 할지 애매한 문제가 발생한다. 이걸을 다이아몬드 문제라 한다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있다. 이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않는다. 대신 이후에 설명할 인터페이스의 다중 구현을 허용해서 이러한 문제를 피한다.
상속 관계를 객체로 생성할 때 메모리 구조를 확인해보자.
ElectricCar electricCar = new ElectricCar();
new ElectricCar() 를 호출하면 ElectricCar 뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성한다.
참조값은 x001(랜덤) 하나이지만 실제로 그 안에는 Car, ElectricCar 라는 두가지 클래스 정보가 공존하는 것이다.
상속이라고 해서 단순하게 부모의 필드와 메서드만 물려 받는 게 아니다. 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성된다. 외부에서 볼때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다.
electricCar.charge() 를 호출하면 참조값을 확인해서 x001.charge() 를 호출한다. 따라서 x001 을 찾아서 charge() 를 호출하면 되는 것이다. 그런데 상속 관계의 경우에는 내부에 부모와 자식이 모두 존재한다. 이때 부모인 Car 를 통해서 charge() 를 찾을지 아니면 ElectricCar 를 통해서 charge() 를 찾을지 선택해야 한다.
이때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다. electricCar 변수의 타입이 ElectricCar 이므로 인스턴스 내부에 같은 타입인 ElectricCar 를 통해서 charge() 를 호출한다.
electricCar.move() 를 호출하면 먼저 x001 참조로 이동한다. 내부에는 Car, ElectricCar 두가지 타입이 있다. 이때 호출하는 변수인 electricCar 의 타입이 ElectricCar 이므로 이 타입을 선택한다. 그런데 ElectricCar 에는 move() 메서드가 없다. 상속 관계에서 자식 타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾는다. 이 경우 ElectricCar 의 부모인 Car 로 올라가서 move() 를 찾는다. 부모인 Car 에 move() 가 있으므로 부모에 있는 move() 메서드를 호출한다.
만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아본다. 부모에 부모로 계속 올라가면서 필드나 메서드를 찾는 것이다. 물론 계속 찾아도 없으면 컴파일 오류가 발생한다.
지금까지 설명한 상속과 메모리 구조는 반드시 이해해야 한다.