
상속은 객체지향 프로그래밍(OOP)의 핵심 개념 중 하나다.
기존 클래스의 속성과 기능을 물려받아 새로운 클래스를 만드는 것을 의미하며
코드의 중복을 줄이고 확장성을 높이는 데 큰 역할을 한다.
public class ElectricCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
public class GasCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
위 두 클래스는 모두 move()라는 공통 기능을 가지고 있다.
전기차와 가솔린차 모두 자동차의 한 종류이기 때문에
공통된 기능을 부모 클래스 Car로 분리하여 상속 구조로 만들 수 있다.
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
extends 키워드는 부모 클래스를 상속받겠다는 의미다.
이제 ElectricCar와 GasCar는 Car의 move() 메서드를 그대로 사용할 수 있다.
즉 중복 제거와 재사용성 향상이라는 장점을 얻을 수 있다.
ElectricCar car = new ElectricCar();
car.move();
car.charge();
이렇게 객체를 생성하면 실제 메모리 내부에는
ElectricCar뿐 아니라 Car도 함께 포함되어 생성된다.
즉 하나의 인스턴스 내부에
부모(Car)와 자식(ElectricCar)의 구조가 함께 존재하게 된다.
메서드를 호출할 때는 다음 순서로 탐색이 이루어진다.
1. 호출한 변수 타입(ElectricCar)에서 메서드를 찾는다.
2. 없다면 부모(Car)로 올라가서 찾는다.
3. 그래도 없으면 컴파일 오류가 발생한다.
상속의 장점은 기능 확장이 쉽다는 것이다.
예를 들어 모든 차량에 문을 여는 기능을 추가해보자.
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
새로운 차량 HydrogenCar도 간단히 추가할 수 있다.
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
public class CarMain {
public static void main(String[] args) {
HydrogenCar car = new HydrogenCar();
car.move();
car.fillHydrogen();
car.openDoor();
}
}
출력 결과
차를 이동합니다.
수소를 충전합니다.
문을 엽니다.
부모 클래스 한 곳만 수정하면 모든 자식 클래스가 기능을 상속받게 된다.
자식 클래스가 부모의 메서드를 재정의하고 싶을 때 오버라이딩을 사용한다.
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
}
public class Main {
public static void main(String[] args) {
ElectricCar car = new ElectricCar();
car.move();
}
}
출력 결과
전기차를 빠르게 이동합니다.
@Override는 부모 클래스의 메서드를 정확히 재정의했는지 컴파일러가 검사해준다.
메서드 이름과 매개변수, 반환 타입이 동일해야 한다.
접근 제어자는 상속 관계에서도 영향을 미친다.
public : 어디서나 접근 가능하다. protected : 같은 패키지 또는 상속 관계에서 접근 가능하다. default : 같은 패키지 내부에서만 접근 가능하다. private : 클래스 내부에서만 접근 가능하다. 자식 클래스는 부모의 private 멤버에는 접근할 수 없다.
자식 클래스에서 부모의 멤버와 이름이 겹치면 super를 사용해 부모의 것을 호출할 수 있다.
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
public class Child extends Parent {
public String value = "child";
@Override
public void hello() {
System.out.println("Child.hello");
}
public void call() {
System.out.println("this value = " + this.value);
System.out.println("super value = " + super.value);
this.hello();
super.hello();
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
출력 결과
this value = child
super value = parent
Child.hello
Parent.hello
this는 자신의 인스턴스를 가리키고 super는 부모 인스턴스를 참조한다.
이렇게 자식 클래스 내부에서 부모의 필드나 메서드에 접근할 수 있다.