본문 바로가기
IT/김영한의실전JAVA-기본_inFlearn

9. 상속 - 시작 / 상속 관계 / 상속과 메모리 구조 / 상속과 기능 추가 / 상속과 메서드 오버라이딩 / 상속과 접근 제어 / super(부모 참조) / super(생성자) / 문제와 풀이 / 클래스와 메서드에 사용되는 final

by for-learn 2025. 3. 3.

9-1. 상속 시작

ElectricCar.java

package extends1.ex1;

public class ElectricCar {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
    public void charge() {
        System.out.println("충전합니다.");
    }
}

GasCar.java

package extends1.ex1;

public class GasCar {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
    public void fillUp() {
        System.out.println("기름을 주유합니다.");
    }
}

CarMain.java

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();
    }
}

  • 전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념
  • 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념
  • 전기차와 가솔린차는 공통 기능 move()가 있어 상속 관계를 사용하는 것이 효과적  

 

9-2. 상속 관계

상속

  • 객체 지향 프로그래밍의 핵심 요소 중 하나
  • 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용 가능
    => 기존 클래스의 속성과 기능을 그대로 물려받는 것
  • extends 키워드로 상속을 사용하고, 대상은 하나만 선택 가능

용어

  • 부모 클래스 (슈퍼 클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
  • 자식 클래스 (서브 클래스): 부모 클래스로부터 필드와 메서드를 상속받는 클래스

Car.java

package extends1.ex2;

public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
}

ElectricCar.java

package extends1.ex2;

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

GasCar.java

package extends1.ex2;

public class GasCar extends Car {
    public void fillUp() {
        System.out.println("기름을 주유합니다.");
    }
}

CarMain.java

package extends1.ex2;

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();
    }
}
  • 전기차와 가솔린차가 Car를 상속받은 덕분에 electricCar.move(), gasCar.move()를 사용
  • 부모는 자식에 대한 정보가 없고, 자식은 extends Car를 통해 부모를 안다.

단일 상속

  • 자바는 다중 상속을 지원하지 않음 => extend 대상은 하나만 선택
  • 부모가 또 다른 부모를 가질 수 있다. 

다이아몬드 문제

  • 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정
  • AirplaneCar입장에서 move()를 호출할 때,
  • 어떤 부모의 move()를 사용해야 할지 애매해지는 문제
  • 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해 짐

 

 

 

인터페이스의 다중 구현을 허용해서 위 와 같은 문제를 피한다.

 

9-3. 상속과 메모리 구조 (중요❗)

ElectricCar electricCar = new ElectricCar(); // new ElectricCar() 호출

  • new ElectricCar()를 호출하면,
  • 상속관계에 있는 Car까지 포함해 인스턴스를 생성
    (부모 클래스도 함께 포함해서 생성)
  • 외부에서는 하나의 인스턴스처럼 보임
  • 내부에서는 부모와 자식 모두 생성, 공간 구분
electricCar.charge() // 호출

  • 참조값을 확인해서 x001.charge() 를 호출
  • 내부에 부모와 자식이 모두 존재
  • 호출하는 변수의 타입(클래스)을 기준으로 선택
  • 같은 타입인 ElectricCar 를 통해서 charge() 를 호출
electricCar.move() // 호출

  • 호출하는 변수 electricCar의 타입이 ElectricCar이므로 이 타입을 선택
  • 자식 타입에 해당 기능이 없으면,
    부모 타입으로 이동
  • 부모에서도 해당 기능 없으면 상위 부모로 이동

정리

  • 상속 관계의 객체를 생성하면, 내부에 부모와 자식 모두 생성
  • 상속 관계의 객체를 호출할 때, 호출자의 타입을 통해 대상 타입 물색
  • 현재 타입에서 기능을 못 찾으면, 상위 부모 타입으로 이동해 기능을 찾아서 실행
  • 기능을 찾지 못하면 컴파일 오류 발생

 

9-4. 상속과 기능 추가

상속 관계의 장점 알아보기

  • 모든 차량에 문열기 기능 추가 => openDoor()
  • 새로운 수소차 추가 => HydrogenCar
  • 수소차는 수소 충전 기능 보유 => fillHydrogen()

ex3을 만들어, ex2의 모든 클래스 복붙 

ex3.Car.java (openDoor( ) 추가)

package extends1.ex3;

public class Car {
    public void move() { System.out.println("차를 이동합니다."); }

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

HydrogenCar.java 

package extends1.ex3;

// 추가
public class HydrogenCar {
    public void fillHydrogen() {
        System.out.println("수소를 충전합니다.");
    }
}

CarMain.java

package extends1.ex3;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.move(); 
        electricCar.charge();
        electricCar.openDoor();
        
        GasCar gasCar = new GasCar();
        gasCar.move(); 
        gasCar.fillUp();
        gasCar.openDoor();
        
        HydrogenCar hydrogenCar = new HydrogenCar();
        hydrogenCar.move();
        hydrogenCar.fillHydrogen();
        hydrogenCar.openDoor();
    }
}

 

기능 추가와 클래스 확장

  • 상속 관계 덕분에 중복은 줄어들고,
  • 새로운 수소차를 편리하게 확장(extend)

 

 

 

 

9-5. 상속과 메서드 오버라이딩

메서드 오버라이딩(Overriding)

부모에게서 상속 받은 기능을 자식이 재정의 하는 것

  • 자동차의 Car.move() 라는 기능을 사용하면, "차를 이동합니다."라고 출력
  • 전기차가 move() 를 호출한 경우에는 "전기차를 빠르게 이동합니다."라고 출력을 변경

overriding을 만들어, ex3의 모든 클래스 복붙 

overriding.ElectricCar.java (나머지 클래스는 기존과 동일)

package extends1.overriding;

public class ElectricCar extends Car {

    @Override // 메서드 오버라이딩
    public void move() {
        System.out.println("전기차를 빠르게 이동합니다.");
    }

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

CarMain.java

package extends1.overriding;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.move();

        GasCar gasCar = new GasCar();
        gasCar.move();
    }
}

@Override

  • @이 있는 부분을 애노테이션이라 하고,  주석과 비슷한데 프로그램이 읽을 수 있는 특별한 주석
  • @Override 애노테이션은 상위 클래스의 메서드를 오버라이드하는 것
  • 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인
  • 오버라이딩 조건을 만족시키지 않으면 컴파일 에러 발생
    => 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생
  • 이 기능은 필수는 아니지만(생략 가능) 코드의 명확성을 위해 붙여주는 것이 좋다.

오버로딩(Overloading)과 오버라이딩(Overriding)

  • 메서드 오버로딩
    • 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것
    • 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻
    • 같은 이름의 메서드를 여러개 정의
  • 메서드 오버라이딩
    • 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정 = 부모의 기능을 자식이 다시 정의하는 것
    • 따라서 상속 관계에서 사용 => 기존 기능을 다시 정의
    • 오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것
      => 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다
    • 우리말로 번역하면 무언가를 다시 정의한다고 해서 재정의
    • 실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용

메서드 오버라이딩 조건 (참고)

  • 메서드 이름: 메서드 이름이 같아야 한다.
  • 메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
  • 반환 타입: 반환 타입이 같아야 한다. 단, 반환 타입이 하위 클래스 타입일 수 있다.
  • 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다.
    예를 들어, 상위 클래스의 메서드가 protected 로 선언되어 있으면
    하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, (가능)
    private 또는 default 로 오버라이드 할 수 없다. (불가능)
  • 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 로 선언할 수 없다.
    하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외는 뒤에서 다룬다.
  • static , final , private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다
    • static클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다.
      그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다.
    • final메서드는 재정의를 금지한다. (final은 메서드를 변경 안되게 고정한다는 의미)
    • private메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다.
  • 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.

 

9-6. 상속과 접근 제어

 

 

  • 접근 제어 설명을 위해 부모, 자식 패키지를 따로 분리
  • 접근 제어자 표현을 위해 UML 표기법 일부 사용
+ : public => 모든 외부 호출을 허용
# : protected => (package-private) 같은 패키지 안에서 호출은 허용
~ : default => 같은 패키지 호출은 허용, 다른 패키지의 상속 관계 호출은 허용
- : private => 모든 외부 호출을 불가

 

 

 

parent.Parent.java

package extends1.access.parent;

public class Parent {
    public int publicValue;
    protected int protectedValue;
    int defaultValue;
    private int privateValue;

    public void publicMethod() {
        System.out.println("Parent.publicMethod");
    }
    protected void protectedMethod() {
        System.out.println("Parent.protectedMethod");
    }
    void defaultMethod() {
        System.out.println("Parent.defaultMethod");
    }
    private void privateMethod() {
        System.out.println("Parent.privateMethod");
    }

    public void printParent() { // 자기자신 호출
        System.out.println("==Parent 메서드 안==");
        System.out.println("publicValue = " + publicValue);
        System.out.println("protectedValue = " + protectedValue);
        System.out.println("defaultValue = " + defaultValue); // 부모 메서드 안에서 접근 가능
        System.out.println("privateValue = " + privateValue); // 부모 메서드 안에서 접근 가능

        // 부모 메서드 안에서 모두 접근 가능
        defaultMethod();
        privateMethod();
    }
}

child.Child.java

package extends1.access.child;

import extends1.access.parent.Parent;

public class Child extends Parent {
    public void call() {
        publicValue = 1;
        protectedValue = 1; // 상속 관계 or 같은 패키지
        //defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
        //privateValue = 1; // 접근 불가, 컴파일 오류

        publicMethod();
        protectedMethod(); // 상속 관계 or 같은 패키지
        //defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
        //privateMethod(); // 접근 불가, 컴파일 오류

        printParent(); // 이 메서드는 public 메서드
    }
}

둘의 패키지가 다르다는 부분의 유의

자식 클래스인 Child에서 부모 클래스인 Parent에 접근 가능 범위 확인

  • publicValue = 1 : 부모의 public 필드에 접근
  • protectedValue = 1 : 부모의 protected 필드에 접근. 다른 패키지이지만, 상속 관계이므로 접근
  • defaultValue = 1 : 자식과 부모가 다른 패키지이므로 접근할 수 없다.
  • privateValue = 1 : private은 모든 외부 접근을 막으므로 자식이라도 호출할 수 없다.

access.ExtendsAccessMain.java

package extends1.access;

import extends1.access.child.Child;

public class ExtendsAccessMain {
    public static void main(String[] args) {
        Child child = new Child();
        child.call();
    }
}

 

접근 제어와 메모리 구조

  • 본인 타입에 없으면 부모 타입에서 기능을 찾는데,
    이때 접근 제어자가 영향을 준다.
  • 객체 내부에서는 자식과 부모가 구분되어 있기 때문
  • 결국 자식 타입에서 부모 타입의 기능을 호출할 때,
    부모 입장에서 보면 외부에서 호출한 것과 같다.

 

 

9-7. super - 부모 참조

 

super

  • 부모와 자식의 필드명이 같거나, 메서드가 오버라이딩 되어 있을 때,
  • 자식에서 부모의 필드나 메서드를 호출 못함
  • 이때, super 키워드를 사용하면 부모 클래스 참조 가능

 

 

super1.Parent.java

package extends1.super1;

public class Parent {
    public String value = "parent";
    
    public void hello() {
        System.out.println("Parent.hello");
    }
}

super1.Child.java

package extends1.super1;

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); // this 생략 가능
        System.out.println("super value = " + super.value);

        this.hello(); // this 생략 가능 => 자식에서 hello()찾고 없으면 부모로 이동
        super.hello();
    }
}
  • this 는 자기 자신의 참조 => this 생략 가능
  • super 는 부모 클래스 참조

Super1Main.java

package extends1.super1;

public class Super1Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.call();
    }
}

 

9-8. super - 생성자

상속 관계의 인스턴스를 생성하면 => 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다.

Child 를 만들면 부모인 Parent 까지 함께 만들어지는 것 (위 그림 참고)

따라서 각각의 생성자도 모두 호출 되어야 한다.

 

상속 관계를 사용하면, 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다. (규칙)

상속 관계에서 부모의 생성자를 호출할 때는 super(...) 를 사용하면 된다.

ClassA.java (최상위 부모 클래스)

package extends1.super2;

public class ClassA {
    public ClassA() {
        System.out.println("ClassA '기본'생성자");
    }
}

ClassB.java

package extends1.super2;

public class ClassB extends ClassA {
    public ClassB(int a) {
        super(); // 기본 생성자는 생략 가능 => java에서 자동 생성해 줌
        System.out.println("ClassB 생성자 a="+a);
    }
    public ClassB(int a, int b) {
        super(); // 기본 생성자는 생략 가능
        System.out.println("ClassB 생성자 a="+a + " b=" + b);
    }
}
  • ClassB 는 ClassA 를 상속 받았다.
    상속을 받으면 생성자의 첫줄에 super(...)를 사용해서 부모 클래스의 생성자를 호출해야 한다.
  • 예외 생성자 첫줄에 this(...)를 사용할 수는 있다.
    하지만 super(...)는 자식의 생성자 안에서 언젠가는 반드시 호출해야 한다.
  • 모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super()를 생략할 수 있다.
  • 상속 관계에서 첫줄에 super(...) 를 생략하면 자바는 부모의 기본 생성자를 호출하는 super() 를 자동 생성
    참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공

ClassC.java

package extends1.super2;

public class ClassC extends ClassB {
    public ClassC() {
        super(10, 20); // ClassB에는 기본생성자가 없어서 생략 불가능
        System.out.println("ClassC 생성자");
    }
}
  • ClassC 의 부모인 ClassB 에는 기본 생성자가 없다. super()를 사용하거나 생략할 수 없다.
    • 사용자 정의 생성자를 만들면, 자바에서는 기본생성자를 만들지 않는다.
  • ClassC 는 ClassB 를 상속 받았다. ClassB 다음 두 생성자가 있다.
    • ClassB(int a)
    • ClassB(int a, int b)
  • 생성자는 하나만 호출할 수 있다. 두 생성자 중에 하나를 선택하면 된다.
  • super(10, 20) 를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택

Super2Main.java

package extends1.super2;

public class Super2Main {
    public static void main(String[] args) {
        ClassC classC = new ClassC();
    }
}

  • ClassA ▶ ClassB  ClassC 순서로 실행
  • 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것
  • 초기화는 최상위 부모부터 => 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문

  • new ClassC()를 통해 ClassC 인스턴스 생성
  • ClassC()의 생성자 먼저 호출
  • ClassC() 생성자의 super(..)를 통해 ClassB(..)
    생성자 호출
  • ClassB(..) 생성자의 super(..)를 통해 ClassA()
    생성자 호출
  • ClassA()가 생성자 코드 실행 후 호출 종료
  • ClassA()를 호출한 ClassB(..) 생성자로 제어권이
    돌아감
  • ClassB(..)가 생성자 코드 실행 후 호출 종료
  • ClassB(..)를 호출한 ClassC() 생성자로 제어권 돌아감
  • ClassC()가 마지막으로 생성자 코드 실행

정리

  • 상속 관계의 생성자 호출은 부모에서 자식 순으로 실행
  • 부모의 데이터를 먼저 초기화하고, 그 다음에 자식의 데이터를 초기화
  • 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(..) 호출
  • 단, 기본 생성자 super()인 경우 생략 가능

this(...)와 함께 사용

코드의 첫줄에 this(...)를 사용하더라도 반드시 한번은 super(...)를 호출해야 한다.

package extends1.super2;

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);
    }
}
package extends1.super2;

public class Super2Main {
    public static void main(String[] args) {
        //ClassC classC = new ClassC();
        ClassB classB = new ClassB(100); // 코드 변경
    }
}

 

9-9. 문제와 풀이

Item.java

package extends1.ex;

public class Item {
    private String name;
    private int price;

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public void print() {
        System.out.println("이름:" + name + ", 가격:" + price);
    }

    public int getPrice() {
        return price;
    }
}

Book.java

package extends1.ex;

public class Book extends Item {
    private String author;
    private String isbn;

    public Book(String name, int price, String author, String isbn) {
        super(name, price);
        this.author = author;
        this.isbn = isbn;
    }

    @Override
    public void print() {
        super.print();
        System.out.println("- 저자:" + author + ", isbn:" + isbn);
    }
}

Album.java

package extends1.ex;

public class Album extends Item {
    private String artist;

    public Album(String name, int price, String artist) {
        super(name, price);
        this.artist = artist;
    }

    @Override
    public void print() {
        super.print();
        System.out.println("- 아티스트:" + artist);
    }
}

Movie.java

package extends1.ex;

public class Movie extends Item{
    private String director;
    private String actor;

    public Movie(String name, int price, String director, String actor) {
        super(name, price);
        this.director = director;
        this.actor = actor;
    }

    @Override
    public void print() {
        super.print();
        System.out.println("- 감독:" + director + ", 배우:" + actor);
    }
}

 

9-10. 클래스와 메서드에 사용되는 final

클래스에 final

  • 상속 끝!
  • final 로 선언된 클래스는 확장될 수 없다. 다른 클래스가 final 로 선언된 클래스를 상속받을 수 없다.
  • 예: public final class MyFinalClass {...}

메서드에 final

  • 오버라이딩 끝!
  • final 로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없다.
  • 예: public final void myFinalMethod() {...}