[TIL 21] JAVA - 상속

nini·2025년 4월 17일

KB IT's Your Life

목록 보기
22/40

상속

1. 상속 개념

상속?

  • 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있음

자식 클래스가 부모 클래스의 '필드(변수)와 메소드(기능)'를 물려받는 것

즉, 이미 만들어진 클래스를 재사용해서 새로운 클래스를 만드는 것

상속 구조 그림
Animal (부모)Dog (자식)
class 부모클래스 {
    // 부모의 변수와 메소드
}

class 자식클래스 extends 부모클래스 {
    // 부모의 멤버를 모두 사용 가능
    // 필요한 기능은 추가하거나 수정할 수 있음
}

상속의 이점

  • 이미 개발된 클래스를 재사용하므로 중복 코드를 줄임
  • 클래스 수정을 최소화

2. 클래스 상속

클래스 상속

  • 자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지를 결정하고, 부모 클래스를 extends 뒤에 기술
public class 자식클래스 extends 부모클래스 {
}
  • 다중 상속 허용하지 않음, extends 뒤에 하나의 부모 클래스만 상속
    • 자바는 단일 상속만 가능(extends는 한 번만!)
    • 허용? C++, Python 등

3. 부모 생성자 호출

부모 생성자 호출
자식 생성자가 호출됐을 때 제일 먼저 할 일은 부모 클래스의 생성자를 호출하는 것이다 -> 부모 클래스의 필드가 초기화 코드가 먼저 실행돼야 자식 클래스도 제대로 동작할 수 있기 때문에

  • 자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성
    자식 클래스 변수 = new 자식클래스();

  • 부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출

// 자식 생성자 선언
public 자식클래스(...) {
	super(); // super(매개값, ...);
	...
}

-> 부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 super(매개값, ...) 코드를 직접 넣어야 한다. 이 코드는 매개값의 타입과 개수가 일치하는 부모 생성자를 호출한다.


자식 클래스 생성자 안에서는 super() 키워드를 사용해서 부모 클래스의 생성자를 명시적으로 호출할 수 있음

class Parent {
    Parent() {
        System.out.println("부모 생성자 호출됨");
    }
}

class Child extends Parent {
    Child() {
        super(); // 생략 가능하지만, 맨 첫 줄에 있어야 함
        System.out.println("자식 생성자 호출됨");
    }
}

두 코드 모두 SmartPhone 클래스에서 Phone 클래스를 상속하고 있는데, 부모 생성자를 호출하는 방식이 다르다!

📌 두 코드의 차이점

항목첫 번째 코드두 번째 코드
super() 호출super() → 매개변수 없는 기본 생성자 호출super(model, color)매개변수 생성자 호출
전제 조건부모 클래스에 기본 생성자가 정의되어 있어야 함부모 클래스에 Phone(String model, String color) 생성자가 정의되어 있어야 함
this.model = model자식 클래스에서 직접 필드 초기화부모 클래스에서 생성자에서 필드를 초기화
역할 분담필드 초기화 책임이 자식 클래스에 있음필드 초기화 책임이 부모 클래스에 있음

1️⃣ 첫 번째 코드

public class Phone {
    String model;
    String color;

    public Phone() {
        System.out.println("Phone() 생성자 실행");
    }
}

public class SmartPhone extends Phone {
    public SmartPhone(String model, String color) {
        super(); // 부모의 기본 생성자 호출
        this.model = model; // 자식이 필드를 직접 초기화
        this.color = color;
        System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
    }
}

Phone 클래스에 기본 생성자가 있어야 하고,

modelcolor는 자식 클래스에서 직접 초기화함.


2️⃣ 두 번째 코드

public class Phone {
    String model;
    String color;

    public Phone(String model, String color) {
        this.model = model;
        this.color = color;
        System.out.println("Phone(String model, String color) 생성자 실행");
    }
}

public class SmartPhone extends Phone {
    public SmartPhone(String model, String color) {
        super(model, color); // 부모의 매개변수 생성자 호출
        System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
    }
}

Phone 클래스에 매개변수 생성자가 있어야 하고,

필드 초기화는 부모 생성자에서 처리함.


✅ 결론

상황추천 방식
부모 클래스가 기본 생성자만 가질 때super()
부모 클래스가 매개변수 생성자를 통해 필드 초기화하도록 설계되었을 때super(…) 사용하여 부모에게 초기화 책임 위임

항목1번 방식2번 방식
초기화 위치자식 클래스부모 클래스
권장도❌ (비추)✅ (추천)
객체지향 원칙위반 가능성 있음원칙에 부합
유지보수어렵다쉽다
가독성다소 복잡명확하고 간결

부모 생성자가 필드를 초기화하도록 설계되어 있다면,
자식 클래스는 super(...)를 통해 부모의 생성자를 호출
하는 2번 방식이 더 바람직하다!


4. 메소드 재정의

메소드 오버라이딩

  • 상속된 메소드를 자식 클래스에서 재정의 하는 것
  • 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용

<메소드를 오버라이딩할 때의 규칙>

  • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 함
  • 접근 제한을 더 강하게 오버라이딩 할 수 없음(public → private으로 변경 불가)
  • 새로운 예외를 throws 할 수 없음

✅ 왜 오버라이딩을 할까?

  • 자식 클래스만의 방식으로 동작을 바꾸고 싶을 때!
  • 부모 클래스에 이미 정의된 메소드를 그대로 쓰지 않고, 자식 클래스에 맞게 다시 작성하는 것

✅ 오버라이딩의 조건

조건설명
메소드 이름부모 메소드와 같아야 함
매개변수개수, 순서, 타입 모두 같아야 함
리턴 타입동일하거나, 더 좁은 타입(covariant)
접근 제어자부모보다 같거나 더 넓은 범위여야 함 (예: protected → public 가능)
예외부모보다 더 많은 예외를 던지면 안 됨

오버로딩(overloading)과 차이점

항목오버라이딩 (Overriding)오버로딩 (Overloading)
부모 클래스의 메소드를 재정의같은 클래스 내에서 메소드 이름은 같고, 매개변수가 다르게 여러 개 정의
관계상속 관계에서만 가능상속 관계 필요 없음
메소드 이름같음같음
매개변수완전히 같아야 함 (타입, 순서, 개수)다를 수 있음 (타입, 개수, 순서 중 하나 이상)
리턴 타입같거나, 부모보다 좁은 타입달라도 상관 없음
사용 목적부모의 기능을 자식이 바꾸고 싶을 때같은 이름으로 다양한 인자 처리를 하고 싶을 때
실행 시점런타임(실행 시간)에 결정됨 (동적 바인딩)컴파일 시간에 결정됨 (정적 바인딩)

✅ 오버라이딩은 "상속받은 걸 바꾸는 것",

✅ 오버로딩은 "같은 이름으로 여러 개 만들기"


@Override // 컴파일 시 정확히 오버라이딩이 되었는지 체크 해줌

@Override 어노테이션

  • 오버라이딩을 할 때는 반드시 @Override를 붙이는 것이 좋음!
  • 컴파일러가 오버라이딩이 제대로 되었는지 체크해줘서 실수를 방지할 수 있다

부모 메소드 호출

  • 자식 메소드 내에서 super 키워드와 도트(.) 연산자를 사용하면 숨겨진 부모 메소드를 호출
    • super().메소드() : 자식 메소드 안에서 부모의 메소드를 명시적으로 호출하는 방법
  • 부모 메소드를 재사용함으로써 자식 메소드의 중복 작업 내용을 없애는 효과

5. final 클래스, final 메소드

final 클래스

  • final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없음

public final class 클래스 { ... }

final 메소드

  • 메소드를 선언할 때 final 키워드를 붙이면 오버라이딩 할 수 없음
  • 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의 할 수 없음

public final 리턴타입 메소드(매개변수, ...) { ... }


6. protected 접근 제한자

  • protected는 상속과 관련이 있고, public과 default의 중간쯤에 해당하는 접근 제한
  • protected는 같은 패키지에서는 default처럼 접근이 가능 -> 다른 패키지에서는 자식 클래스만 접근을 허용

7. 타입 변환

자동 타입 변환(업캐스팅, Upcasting)

  • 자동적으로 타입 변환이 일어나는 것
  • 자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급

용어 | 설명
자동 타입 변환 (업캐스팅) | 자식 → 부모로 자동 변환
언제? | 자식 객체를 부모 변수에 담을 때
왜? | 자식은 부모의 모든 특징을 물려받기 때문에, 부모처럼 행동할 수 있음
결과 | 부모 타입으로는 부모가 알고 있는 기능만 접근 가능

정보 손실? ❌ 실제 객체는 그대로
접근 제한? ✅ 부모 타입으로는 자식 기능에 접근 불가


8. 다형성

  • 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질
  • 다형성을 구현하기 위해서는 자동 타입 변환(upcasting)과 메소드 재정의(overriding)가 필요

자동 타입 변환 + 메소드 오버라이딩 -> 다형성

필드 다형성

  • 필드 타입은 동일하지만, 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는 것
class Animal {
    void sound() {
        System.out.println("동물이 소리를 냅니다");
    }
}

class Dog extends Animal {
    void sound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    void sound() {
        System.out.println("야옹~");
    }
}

-> 하나의 타입(Animal)인데, 다른 행동(Dog, Cat의 sound)을 하는 것!
public class Main {
    public static void main(String[] args) {
    -> 상속 기반 참조 다형성 또는 업캐스팅 다형성
        Animal myAnimal1 = new Dog();  // Animal 타입이지만 Dog 객체
        Animal myAnimal2 = new Cat();  // Animal 타입이지만 Cat 객체

        myAnimal1.sound();  // 멍멍!
        myAnimal2.sound();  // 야옹~
    }
}

매개변수 다형성

  • 메소드가 클래스 타입의 매개변수를 가지고 있을 경우,
    • 호출할 때 동일한 타입의 자식 객체를 제공할 수 있음
  • 어떤 자식 객체가 제공되느냐에 따라서 메소드의 실행 결과가 달라짐(전략 strategy 패턴)
종류설명예시
① 매개변수 다형성(메소드 오버로딩)같은 이름의 메소드가 매개변수 개수나 타입에 따라 다르게 동작print(String s) vs print(int i)
② 상속 기반 다형성(메소드 오버라이딩 + 업캐스팅)부모 타입 참조변수로 자식 객체를 다루고, 자식의 메소드를 호출Animal a = new Dog();
③ 필드 다형성부모 타입의 필드로 자식 타입 객체를 할당하여 다양한 객체 저장Animal[] animals = {new Dog(), new Cat()};

9. 객체 타입 확인

instanceof 연산자

  • 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인할 때 instanceof 연산자를 사용
  • instanceof 연산자에서 좌향의 객체가 우향의 타입이면 true를 산출하고 그렇지 않으면 false를 산출

boolean result = 객체 instanceof 타입;

⇒ 객체가 특정 클래스의 인스턴스인지, 또는 그 클래스의 자식 클래스인지도 포함해서 확인해주는 논리 연산자


10. 추상 클래스

추상 클래스

  • 객체를 생성할 수 있는 실체 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스
  • 추상 클래스는 실체 클래스의 부모 역할, 공통적인 필드나 메소드를 물려받을 수 있음

추상 클래스 선언

  • 클래스 선언에 abstract 키워드를 붙임
  • new 연산자를 이용해서 객체를 직접 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있다

추상 메소드와 재정의

  • 자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우 추상 메소드를 선언할 수 있음
  • 일반 메소드 선언과의 차이점은 abstract 키워드가 붙고, 메소드 실행 내용은 중괄호 { } 가 없다

abstract 리턴타입 메소드명(매개변수, ...);

sealed 클래스

  • Java 15부터 무분별한 자식 클래스 생성을 방지하기 위해 봉인된 클래스가 도입
  • sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정
  • final은 더 이상 상속할 수 없다는 뜻이고, non-sealed는 봉인을 해제한다는 뜻

❗️더보기

1. 추상 클래스 vs 인터페이스 차이 간단 비교

항목추상 클래스인터페이스
키워드abstractinterface
다중 상속❌ 안됨 (단일 상속만 가능)✅ 다중 구현 가능
생성자✅ 가질 수 있음❌ 없음
필드✅ 일반 변수, 상태 보관 가능✅ (Java 8+) 상수, static, default 메서드만
메서드✅ 일반 메서드 + 추상 메서드 혼용 가능✅ 추상 메서드(기본), default/static 사용 가능
사용 목적공통 기능 제공 + 확장행위 정의 (기능 명세)

2. 추상 클래스 사용 시점 (언제 쓰면 좋을까?)

  • 자식 클래스들이 공통적으로 가져야 하는 필드/메서드가 있는 경우
  • 공통적인 메서드는 추상 클래스에서 구현(일부)하고,
    자식 클래스에 따라 다른 메서드는 추상 메서드로 강제 구현
  • 실체 클래스의 틀(template)을 만들고 싶을 때

3. 템플릿 메서드 패턴과의 관계

  • 추상 클래스는 자주 템플릿 메서드 패턴에서 활용됨
  • 어떤 전체 로직은 미리 구현, 세부 단계는 자식 클래스에 맡기는 방식
abstract class Game {
    abstract void start();
    abstract void play();
    abstract void end();

    public final void run() {
        start();
        play();
        end();
    }
}
  • run()변하지 않는 공통 로직, 나머지는 자식이 구현

4. 추상 클래스도 생성자는 가질 수 있다!

  • 직접 객체 생성은 불가능하지만, 자식 클래스에서 super()로 호출 가능
abstract class Animal {
    String name;
    Animal(String name) {
        this.name = name;
    }
}
class Dog extends Animal {
    Dog(String name) {
        super(name);  // 부모 생성자 호출
    }
}

5. 추상 클래스도 필드와 메서드 접근 제한자 사용 가능

  • private, protected, public접근 제어자 모두 사용 가능
  • 필요에 따라 자식 클래스에서만 상속 가능하도록 protected 사용 가능
profile
사용자를 고려한 디자인과 UX에 관심있는 개발자

0개의 댓글