Post

[JAVA] 상속

[JAVA] 상속

상속이란?

  • 상속(Inheritance)은 부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 사용하는 기능이다.
  • 중복 코드를 줄이고, 공통된 기능을 재사용할 수 있음
  • extends 키워드를 사용하여 상속
  • 자식 클래스는 부모의 기능을 사용할 수 있지만, 부모는 자식의 기능을 모름
  • 자바는 단일 상속만 허용함 (하나의 부모만 상속 가능)

상속을 왜 쓰는지?

전기차와 가솔린차 클래스가 있다고 해보자. 각각 move() 라는 공통 기능이 있음에도 따로 구현되어 있다면, 중복을 줄이고 구조를 더 깔끔하게 만들 방법이 필요함. 바로 이럴 때 상속이 등장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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("기름을 주유합니다.");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() ← 코드 중복이 확인된다.

이런경우 상속을 사용하는 것이 좋다. 부모 클래스 Car 을 만들어 자식 클래스에서 상속 받으면 된다.

상속 적용

1
2
3
4
5
6
//부모 클래스
public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class ElectricCar extends Car {
    public void charge() {
        System.out.println("충전합니다.");
    }
}

//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class GasCar extends Car {
    public void fillUp() {
        System.out.println("기름을 주유합니다.");
    }
}

전기차와 가솔린차가 Car 를 상속 받은 덕분에 electricCar.move() , gasCar.move() 를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
ElectricCar electricCar = new ElectricCar();
electricCar.move();   // 부모의 move() 호출
electricCar.charge();

GasCar gasCar = new GasCar();
gasCar.move();        // 부모의 move() 호출
gasCar.fillUp();

차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
  • 부모 클래스 (Super Class): 상속을 제공하는 클래스 (Car)
  • 자식 클래스 (Sub Class): 상속을 받는 클래스 (ElectricCar, GasCar)
  • 자식 —> 부모 호출은 가능
  • 부모 —> 자식 호출은 불가능

    (Car 클래스는 ElectricCarGasCar를 모른다!)

자바는 다중 상속을 허용하지 않음

다중 상속을 허용하면 move() 같은 메서드가 어떤 부모의 것을 써야 할지 모호해짐 → 다이아몬드 문제

그래서 자바는 하나의 클래스만 상속 가능. 물론 부모가 또 다른 부모를 하나 가지는 것은 괜찮다.

대신 인터페이스는 여러 개 구현 가능해서, 다중 상속의 일부 장점을 인터페이스로 보완한다.

상속과 메모리 구조

1
ElectricCar electricCar = new ElectricCar();
  • ElectricCar 인스턴스를 생성하면, 내부적으로는 부모 클래스인 Car까지 포함되어 생성된다.
  • 하나의 참조값(x001) 안에 부모(Car) + 자식(ElectricCar) 구조가 겹쳐져 존재한다.

image.png

  • electricCar의 타입은 ElectricCar
  • ElectricCar 내부에서 charge() 메서드를 찾음
  • 자식 클래스에 charge() 존재 → 바로 실행

image.png

  • ElectricCar 내부에 move() 없음
  • 상속 관계에 따라 부모 클래스(Car) 로 올라감
  • Carmove() 존재 → 부모의 메서드 실행

자식 → 부모 순서대로 메서드 탐색

상속 시 객체 구조 : 부모 + 자식 하나의 인스턴스에 같이 생성

메서드 탐색 순서 : 자식 → 부모 → 조부모..

참조 변수 타입 : 메서드 탐색 기준이 됨 ElectricCar electricCar

호출 불가능한 경우 : 컴파일 에러

✅ 상속 관계 객체는 하나의 인스턴스 안에 부모와 자식 클래스 구조를 함께 포함하며, 메서드 호출 시 참조 변수의 타입과 클래스 계층에 따라 기능을 탐색 한다.

상속과 기능 추가

1
2
3
4
5
6
7
8
9
10
public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }

    // 공통 기능 추가
    public void openDoor() {
        System.out.println("문을 엽니다.");
    }
}
✅ 부모 클래스에 기능을 추가하면, 모든 자식 클래스가 자동으로 상속받는다!

새로운 자식 클래스 추가

1
2
3
4
5
public class HydrogenCar extends Car {
    public void fillHydrogen() {
        System.out.println("수소를 충전합니다.");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ElectricCar electricCar = new ElectricCar();
electricCar.move();         // 부모 기능
electricCar.charge();       // 자식 고유 기능
electricCar.openDoor();     // 부모에 추가된 새 기능

HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();

차를 이동합니다.
충전합니다.
문을 엽니다.
차를 이동합니다.
수소를 충전합니다.
문을 엽니다.
✅ 상속을 사용하면 공통 기능을 한 번에 관리할 수 있고, 새로운 클래스를 간편하게 추가할 수 있어 유지보수와 확장(extend)매우 유리하다.

상속과 메서드 오버라이딩(Overriding)

부모에게 상속받은 메서드를 자식 클래스에서 새로운 방식으로 재정의하는 것

1
2
3
4
@Override
public void move() {
    System.out.println("전기차를 빠르게 이동합니다.");
}
  • 부모 Carmove()는 “차를 이동합니다.”
  • 자식 ElectricCar에서 move()를 오버라이딩 → “전기차를 빠르게 이동합니다.”

부모클래스

1
2
3
4
5
6
7
8
9
public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }

    public void openDoor() {
        System.out.println("문을 엽니다.");
    }
}

자식클래스

1
2
3
4
5
6
7
8
9
10
public class ElectricCar extends Car {
    @Override
    public void move() {
        System.out.println("전기차를 빠르게 이동합니다.");
    }

    public void charge() {
        System.out.println("충전합니다.");
    }
}

메인

1
2
3
4
ElectricCar electricCar = new ElectricCar();
electricCar.move(); // 오버라이딩된 메서드 호출

전기차를 빠르게 이동합니다.
  • electricCarElectricCar 타입
  • ElectricCar 내부에서 move() 찾음 → 있음 → 그 메서드 실행
  • 부모까지 올라가지 않음
✅ 자식이 같은 이름의 메서드를 가지고 있으면 그 메서드가 우선 실행

@Override 애노테이션

  • 상위 클래스의 메서드를 정확히 오버라이드하는지 컴파일 타임에 체크
  • 실수 방지 및 코드 명확성 향상
  • 안 붙여도 되지만 무조건 붙이는 습관 권장
1
2
3
4
@Override
public void move() {
    // ...
}

오버라이딩 조건

부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분하다.

  • 메서드 이름: 부모와 같아야 함
  • 매개변수: 타입, 개수, 순서 동일
  • 반환 타입: 동일 or 하위 타입
  • 접근 제어자: 더 좁으면 안 됨

    상위 클래스의 메서드가 protected 로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, private 또는 default 로 오버라이드 할 수 없다.

  • 예외: 더 많은 체크 예외 선언 ❌
  • static, final, private 메서드: 오버라이딩 ❌
  • 생성자: 오버라이딩 불가
✅ 오버라이딩은 자식 클래스가 부모 클래스의 기능을 자기 방식으로 재정의하는 것. 다형성 구현의 핵심이며, @Override 를 반드시 붙이자!

상속과 접근제어자

상속 관계에서 자식 클래스는 부모 클래스의 필드와 메서드에 접근 제어자에 따라 접근할 수 있다.

접근 제어자는 정보 은닉과 캡슐화의 핵심!

접근 제어자 의미 같은 클래스 같은 패키지 자식 클래스(다른 패키지) 외부
private 완전 차단
(default) 패키지 전용
protected 패키지 + 상속 허용
public 전부 허용

부모 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Parent {
    public int publicValue;
    protected int protectedValue;
    int defaultValue;
    private int privateValue;

    public void publicMethod() { ... }
    protected void protectedMethod() { ... }
    void defaultMethod() { ... }
    private void privateMethod() { ... }

    public void printParent() {
        // 모든 필드/메서드 접근 가능
    }
}

자식 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Child extends Parent {
    public void call() {
        publicValue = 1;       // 가능
        protectedValue = 1;    // 가능 (상속 관계이므로)
        // defaultValue = 1;   // ❌ 다른 패키지 접근 불가
        // privateValue = 1;   // ❌ 접근 불가

        publicMethod();        // 가능
        protectedMethod();     // 가능
        // defaultMethod();    // ❌
        // privateMethod();    // ❌

        printParent();         // public 메서드는 호출 가능
    }
}

Parent.publicMethod
Parent.protectedMethod
==Parent 메서드 ==
publicValue = 1
protectedValue = 1
defaultValue = 0
privateValue = 0
Parent.defaultMethod
Parent.privateMethod
필드/메서드 자식 클래스에서 접근 이유
public ✅ 가능 외부 포함 모두 접근 가능
protected ✅ 가능 상속 관계이므로 가능
default (패키지 전용) ❌ 불가 다른 패키지라서 불가
private ❌ 불가 클래스 내부에서만 사용 가능
  • 자식 클래스는 부모의 public, protected만 접근 가능
  • default, private는 자식이라도 다른 패키지면 접근 불가
  • 부모 클래스 내부에서는 자신의 모든 멤버에 자유롭게 접근 가능
✅ 자식 클래스는 부모 클래스의 public, protected 만 접근 가능하며, 부모 클래스 내부에서는 자신의 모든 필드와 메서드에 접근 할 수 있다.

접근제어자 UML 표기법

  • + : public
  • # : protected
  • ~ : default (package-private)
  • - : private

접근제어자와 메모리 구조

image.png

상속 관계에서 객체가 생성되면, 부모 클래스와 자식 클래스가 하나의 인스턴스 안에 함께 존재 하지만 메모리 내부에서는 구분 되어 있다.

자식에서 부모의 기능을 호출하는 건 부모 입장에서는 외부에서 접근하는 것과 같음

즉, 아무리 내부에 있어도

  • private, default부모 외부 접근 차단이므로 → 자식 클래스에서 접근 불가
  • protected, public만 접근 가능
✅ 객체 내부에 부모와 자식이 함께 있어도, 접근 제어자 규칙은 여전히 적용된다. 자식이 부모 기능을 호출하는 것은 결국 외부에서 접근하는 것과 같기 때문!

super

부모의 필드/메서드 참조

자식 클래스에서 부모의 필드나 메서드와 이름이 같을 때, 부모 쪽 기능을 호출하고 싶을 때 사용

부모 클래스

1
2
3
4
5
6
public class Parent {
    public String value = "parent";
    public void hello() {
        System.out.println("Parent.hello");
    }
}

자식 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);   // child
        System.out.println("super value = " + super.value); // parent
        this.hello();   // Child.hello
        super.hello();  // Parent.hello
    }
}

메인 클래스

1
2
3
4
5
6
7
8
9
10
11
12
public class Super1Main {
	 public static void main(String[] args) {
			 Child child = new Child();
			 child.call();
	 }
}

-- 실행결과 --
this value = child
super value = parent
Child.hello
Parent.hello

image.png

super 는 부모 클래스의 필드나 메서드를 참고할 때 사용! 이름이 겹칠 경우, 자식이 우선이므로 super 로 명시적으로 부모 접근 필요!

부모 생성자 호출

자식 객체를 생성하면, 부모 객체도 함께 생성됨 따라서 자식 생성자에서 반드시 부모 생성자도 호출해야 함

생성자 규칙

  • 자식 생성자의 첫 줄에서 super(...)를 호출해야 함
  • 생략 시 → 컴파일러가 자동으로 super() 추가
  • 부모에 기본 생성자 없으면, 반드시 명시적으로 super(...) 호출해야 함
1
2
3
4
5
public class ClassA {
    public ClassA() {
        System.out.println("ClassA 생성자"); //5
    }
}
1
2
3
4
5
public class ClassB extends ClassA {
    public ClassB(int a, int b) { //3
        super(); // 부모의 기본 생성자 호출 4
        System.out.println("ClassB 생성자 a=" + a + " b=" + b); //6
    }
1
2
3
4
5
6
public class ClassC extends ClassB {
    public ClassC() {
        super(10, 20); // 부모 생성자 직접 선택 2
        System.out.println("ClassC 생성자"); //7
    }
}
1
2
3
4
5
6
7
8
9
10
public class Super2Main {
	 public static void main(String[] args) {
			 ClassC classC = new ClassC(); //1
	 }
}

-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자
  • ClassC()가 호출됨
  • → 가장 먼저 super(10, 20) 호출 → ClassB(int a, int b)
  • → 그 안에서 다시 super() 호출 → ClassA()
✅ 상속 관계에서 객체가 생성될 때, 생성자는 부모부터 실행되고 자식으로 내려온다. 이 순서를 통해 부모의 데이터가 먼저 초기화된 후 , 자식의 데이터가 초기화된다.

this(…) 와 super(…)의 관계

  • 생성자에서 this(...)같은 클래스의 다른 생성자 호출 가능
  • 하지만 결국에는 한 번은 반드시 super(...) 호출해야 함
1
2
3
4
5
6
7
8
9
10
11
12
public class ClassB extends ClassA {
    public ClassB(int a) {
        this(a, 0); // 자기 생성자 호출
        System.out.println("ClassB 생성자 a=" + a);
    }

    public ClassB(int a, int b) {
        super(); // 부모 생성자 호출
        System.out.println("ClassB 생성자 a=" + a + " b=" + b);
    }
}

1
2
3
4
5
6
7
8
9
10
public class Super2Main {
	 public static void main(String[] args) {
			ClassB classB = new ClassB(100);
	 }
}

-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=100 b=0
ClassB 생성자 a=100
  • this(...)같은 클래스의 다른 생성자 호출
  • super(...)부모 클래스 생성자 호출
  • 생성자 호출은 반드시 최상위 부모 → 자식 순서로 끝까지 호출됨

정리

  • 상속은 부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 사용하는 기능
  • 중복 코드를 줄이고, 공통된 기능을 효율적으로 재사용할 수 있음
  • 상속 구조에서는 객체 내부에 부모와 자식이 함께 생성되며, 메서드 탐색은 자식 → 부모 순서로 이루어짐
  • 상속을 통해 기능을 편리하게 확장할 수 있고, 오버라이딩을 통해 자식만의 동작을 정의할 수 있음
  • 자식 클래스는 부모의 public, protected 멤버만 접근 가능
  • super 키워드를 사용하면 부모의 필드, 메서드, 생성자에 접근 가능함

상속은 자바 객체지향의 핵심 기초 개념 중 하나이며, 앞으로 배울 다형성, 추상 클래스, 인터페이스 개념의 기반이 된다.


출처: 김영한의 실전 자바 - 기본편

This post is licensed under CC BY 4.0 by the author.

Trending Tags