상속(잘못된 정답)

류한선·2025년 6월 21일

실기연습-2

목록 보기
19/95

이게 답임, 밑의 답 잘못됨

GrandParent constructor
Parent constructor
Child constructor
Child print(): 3, super.value=2
GrandParent staticVal: 100
Parent staticVal: 200
Child staticVal: 300
Child print(): 3, super.value=2
Child print(): 3, super.value=2
1

[Execution complete with exit code 0]

🧠 소스 코드 (다시 보기)

import java.util.*;
import java.lang.*;
import java.io.*;

class GrandParent {
    final int value = 1;
    static int staticVal = 100;

    GrandParent() {
        System.out.println("GrandParent constructor");
    }

    void print() {
        System.out.println("GrandParent print(): " + value);
    }

    static void staticPrint() {
        System.out.println("GrandParent staticVal: " + staticVal);
    }
}

class Parent extends GrandParent {
    int value = 2;
    static int staticVal = 200;

    Parent() {
        System.out.println("Parent constructor");
    }

    @Override
    void print() {
        System.out.println("Parent print(): " + value + ", super.value=" + super.value);
    }

    static void staticPrint() {
        System.out.println("Parent staticVal: " + staticVal);
    }
}

class Child extends Parent {
    int value = 3;
    static int staticVal = 300;

    Child() {
        System.out.println("Child constructor");
    }

    @Override
    void print() {
        System.out.println("Child print(): " + value + ", super.value=" + super.value);
    }

    static void staticPrint() {
        System.out.println("Child staticVal: " + staticVal);
    }
}

public class Main {  // ✅ 컴파일러가 자동으로 실행 인식
    public static void main(String[] args) {
        GrandParent gp = new Child();  // (1)
        gp.print();                    // (2)
        GrandParent.staticPrint();     // (3)
        Parent.staticPrint();          // (4)
        Child.staticPrint();           // (5)
        Parent p = (Parent) gp;        // (6)
        p.print();                     // (7)
        Child c = (Child) gp;          // (8)
        c.print();                     // (9)
        System.out.println(gp.value);  // (10)
    }
}

🔍 디버깅 시작!


(1) GrandParent gp = new Child();

생성자 호출 순서

Java의 생성자 호출은 항상 부모부터 호출됩니다.

  1. GrandParent() 호출 → "GrandParent constructor"
  2. Parent() 호출 → "Parent constructor"
  3. Child() 호출 → "Child constructor"

출력:

GrandParent constructor
Parent constructor
Child constructor

상태:

  • gp의 참조 타입 = GrandParent
  • gp의 실제 객체 = Child

(2) gp.print();

여기서 print() 메서드는 동적 바인딩 → 실제 객체 Childprint() 호출

Child.print() 내부

System.out.println(
 "Child print(): " + value + ", super.value=" + super.value
);
  • Child.value = 3 (Child의 인스턴스 필드)
  • super.value = 1 (GrandParent의 final int value = 1)

출력:

Child print(): 3, super.value=1

(3) GrandParent.staticPrint();

정적 메서드 호출 → 정적 바인딩
GrandParent.staticPrint() 호출
staticVal = 100

출력:

GrandParent staticVal: 100

(4) Parent.staticPrint();

Parent.staticPrint() 호출
→ Parent 클래스의 staticVal = 200

출력:

Parent staticVal: 200

(5) Child.staticPrint();

Child.staticPrint() 호출
→ Child 클래스의 staticVal = 300

출력:

Child staticVal: 300

(6) Parent p = (Parent) gp;

업캐스팅된 gp를 Parent 타입 참조 p에 대입.
실제 객체가 Child이므로 안전한 캐스팅.


(7) p.print();

→ p 참조 타입 Parent, 실제 객체 Child → Child.print() 호출
→ 위에서 본 대로 출력:

Child print(): 3, super.value=1

(8) Child c = (Child) gp;

Child 참조에 대입. 실제 객체가 Child이므로 안전.


(9) c.print();

c 참조 타입 Child, 실제 객체 Child → Child.print() 호출
→ 출력 동일:

Child print(): 3, super.value=1

(10) System.out.println(gp.value);

여기가 트릭 포인트!

  • gp 참조 타입 = GrandParent
  • 필드 참조는 정적 바인딩GrandParent.value 참조 → 1

출력:

1

✅ 최종 출력 정리

GrandParent constructor
Parent constructor
Child constructor
Child print(): 3, super.value=1
GrandParent staticVal: 100
Parent staticVal: 200
Child staticVal: 300
Child print(): 3, super.value=1
Child print(): 3, super.value=1
1

🎯 한 줄 한 줄 요약

부분호출되는 것출력
(1) 생성자 호출GrandParent → Parent → Child위 3줄
(2) gp.print()Child.print()Child print(): 3, super.value=1
(3) 정적 호출GrandParent.staticPrint()GrandParent staticVal: 100
(4) 정적 호출Parent.staticPrint()Parent staticVal: 200
(5) 정적 호출Child.staticPrint()Child staticVal: 300
(7) p.print()Child.print()Child print(): 3, super.value=1
(9) c.print()Child.print()Child print(): 3, super.value=1
(10) 필드 참조gp.value → GrandParent의 value1

🔍 핵심 포인트 정리

생성자 호출: 부모부터 → 자식
메서드 오버라이딩: 동적 바인딩 → 실제 객체 기준 호출
정적 메서드 호출: 정적 바인딩 → 참조타입 기준
필드 참조: 정적 바인딩 → 참조타입 기준
final 필드: 부모의 필드를 super.value로 호출 가능
✅ 업캐스팅/다운캐스팅하면서 오버라이딩된 메서드는 실제 객체 메서드 호출


좋아요. 이 부분은 객체의 캐스팅(casting), 즉 업캐스팅(upcasting)과 다운캐스팅(downcasting) 개념을 확실히 정리해야 이해할 수 있습니다.
지금부터 예제 상황을 바탕으로 꼼꼼히 설명해 드릴게요. 🎯


🔍 현재 상황을 다시 정리

GrandParent gp = new Child();

여기서 일어나는 일:

new Child()Child 인스턴스를 생성
✅ 참조변수 gp타입은 GrandParent이므로 업캐스팅(upcasting)이 일어남
✅ 즉, gp실제 Child 객체를 가리키지만, gp라는 참조 타입 자체는 GrandParent


🧠 업캐스팅(Upcasting)

업캐스팅: 자식 → 부모
ChildGrandParent 참조로 받았으므로 업캐스팅이 일어났습니다.

이 상태의 특징:

  • 호출 가능한 멤버(필드, 메서드)는 GrandParent 타입의 것들만 보임.
  • 그러나 print() 메서드 호출은 동적 바인딩되어 Child의 오버라이딩 메서드가 호출됨.

🧠 다운캐스팅(Downcasting)

다운캐스팅: 부모 참조 → 자식 참조
→ 이미 업캐스팅된 상태에서 ChildParent 타입 참조로 바꾸기

이 부분이 헷갈리는 이유:

부모 참조(gp)로 자식의 고유 멤버(Child에만 있는 필드나 메서드)를 쓰려면 반드시 캐스팅해야 합니다.


✏️ 캐스팅 라인들 정리

Parent p = (Parent) gp;

  • 현재 gp 참조의 실제 객체 타입은 Child.
  • ChildParent의 하위 클래스이므로 캐스팅 가능.
  • 이제 p 참조의 타입은 Parent이므로 Parent에 정의된 멤버들(예: Parent의 value 필드나 Parent의 메서드들) 볼 수 있음.
  • 하지만 오버라이딩된 메서드(print())를 호출하면 여전히 Child의 메서드가 호출됨.

Child c = (Child) gp;

  • 현재 gp가 가리키고 있는 건 Child 객체.
  • gpChild로 캐스팅하는 것은 안전.
  • 이제 c 참조를 이용해 Child 고유의 멤버들에도 접근할 수 있음.

🧠 이해를 돕기 위한 도식화

메모리 상의 실제 객체: Child
          ↑
 gp (GrandParent 참조)    // 업캐스팅 상태
 p  (Parent 참조)         // 다운캐스팅됨
 c  (Child 참조)          // 다운캐스팅됨

🧠 캐스팅을 꼭 해야만 하는 이유

Java에서 참조 타입이 무엇이냐에 따라, 컴파일러가 호출 가능한 멤버를 결정하기 때문입니다.

예를 들어:

GrandParent gp = new Child();
gp.print();     // 동적 바인딩 → Child의 print() 호출됨
// gp.value     → GrandParent의 value만 볼 수 있음
// gp.someChildMethod() → 호출 불가, 그런 메서드 없음

하지만:

Child c = (Child) gp;     // 캐스팅
c.someChildMethod();      // 이제 Child 전용 메서드 호출 가능

❓ 캐스팅 실패하는 경우

만약:

GrandParent gp = new Parent();
Child c = (Child) gp; // 런타임 오류! gp가 Child가 아니므로

ClassCastException 발생.
캐스팅 전에 instanceof 검사로 안전하게 캐스팅하는 것이 좋습니다.


📝 정리

✅ 업캐스팅(자식 → 부모): 암묵적, 안전
✅ 다운캐스팅(부모 → 자식): 명시적 캐스팅 필요, 실제 객체가 캐스팅한 타입의 인스턴스여야 함
✅ 캐스팅한 참조를 쓰면, 해당 타입의 멤버를 볼 수 있음
✅ 오버라이딩 메서드 호출은 참조 타입이 아닌 실제 객체 타입 기준


맞아요, 아주 좋은 질문이에요! 🎯
여기서 중요한 건 "super.value가 어떻게 정해지냐" 그리고 다운캐스팅(다운스케일링)되더라도 이 부분이 바뀌지 않는 이유를 이해하는 거예요.


🔍 super.value의 정체

💡 필드 접근의 규칙

  • 메서드 안에서 super.value라고 쓰면 무조건 **부모 클래스(여기서는 GrandParent)**의 value 필드를 참조합니다.
  • super 키워드를 쓰는 순간, 현재 클래스가 상속받은 부모 클래스의 멤버를 가리킵니다.
  • 이건 캐스팅이나 참조 타입과 전혀 무관해요!

즉:

// Child 클래스 안에서
System.out.println("..., super.value = " + super.value);
// -> 반드시 GrandParent의 value(=1)를 참조

🧠 캐스팅과 super의 관계

캐스팅(Parent p = (Parent) gp;, Child c = (Child) gp;)은 참조 타입을 바꾸는 것일 뿐:

  • 메서드 안의 super.value는 항상 Child 클래스 안의 소스 코드로 고정되어 있어요.
  • 캐스팅을 하더라도 이 소스 코드 자체가 바뀌지 않으니, 여전히 GrandParent의 value(1)를 참조합니다.

✏️ 정리

✅ 캐스팅(업캐스팅, 다운캐스팅)은 참조가 보이는 멤버나 메서드 호출에 영향을 줍니다.
✅ 그러나 Child 클래스 안에서 super.value라고 쓴 부분은 컴파일 타임에 이미 GrandParent의 value를 가리키도록 확정되어 있습니다.
✅ 따라서 캐스팅해도 Child의 print() 내부에서 super.value는 계속 GrandParent.value(1)을 참조하게 됩니다.


💬 쉬운 비유

코드를 이렇게 생각해보세요:

class Child extends Parent {
    void print() {
        int parentValue = super.value; // <- 이미 고정된 호출
        System.out.println(parentValue); 
    }
}

여기서 super.value 부분은 이미 "나는 반드시 GrandParent의 value를 읽을 거야!"라고 정해져 있어요.
캐스팅을 하든 말든 print()가 호출되면 내부 소스 코드가 변하지 않기 때문에 동일한 부분을 참조합니다.


🎯 결론

  • super.value → 캐스팅 상관없이 항상 GrandParent의 value 참조
  • 캐스팅은 참조 타입의 인터페이스를 바꿀 뿐, 소스 코드 안의 super.value 참조대상에는 영향 없음

0개의 댓글