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)
}
}
GrandParent gp = new Child();Java의 생성자 호출은 항상 부모부터 호출됩니다.
GrandParent() 호출 → "GrandParent constructor"Parent() 호출 → "Parent constructor"Child() 호출 → "Child constructor"GrandParent constructor
Parent constructor
Child constructor
gp의 참조 타입 = GrandParentgp의 실제 객체 = Childgp.print();여기서 print() 메서드는 동적 바인딩 → 실제 객체 Child의 print() 호출
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
GrandParent.staticPrint();정적 메서드 호출 → 정적 바인딩
→ GrandParent.staticPrint() 호출
→ staticVal = 100
GrandParent staticVal: 100
Parent.staticPrint();→ Parent.staticPrint() 호출
→ Parent 클래스의 staticVal = 200
Parent staticVal: 200
Child.staticPrint();→ Child.staticPrint() 호출
→ Child 클래스의 staticVal = 300
Child staticVal: 300
Parent p = (Parent) gp;업캐스팅된 gp를 Parent 타입 참조 p에 대입.
실제 객체가 Child이므로 안전한 캐스팅.
p.print();→ p 참조 타입 Parent, 실제 객체 Child → Child.print() 호출
→ 위에서 본 대로 출력:
Child print(): 3, super.value=1
Child c = (Child) gp;Child 참조에 대입. 실제 객체가 Child이므로 안전.
c.print();→ c 참조 타입 Child, 실제 객체 Child → Child.print() 호출
→ 출력 동일:
Child print(): 3, super.value=1
System.out.println(gp.value);여기가 트릭 포인트!
gp 참조 타입 = GrandParentGrandParent.value 참조 → 11
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의 value | 1 |
✅ 생성자 호출: 부모부터 → 자식
✅ 메서드 오버라이딩: 동적 바인딩 → 실제 객체 기준 호출
✅ 정적 메서드 호출: 정적 바인딩 → 참조타입 기준
✅ 필드 참조: 정적 바인딩 → 참조타입 기준
✅ final 필드: 부모의 필드를 super.value로 호출 가능
✅ 업캐스팅/다운캐스팅하면서 오버라이딩된 메서드는 실제 객체 메서드 호출
좋아요. 이 부분은 객체의 캐스팅(casting), 즉 업캐스팅(upcasting)과 다운캐스팅(downcasting) 개념을 확실히 정리해야 이해할 수 있습니다.
지금부터 예제 상황을 바탕으로 꼼꼼히 설명해 드릴게요. 🎯
GrandParent gp = new Child();
여기서 일어나는 일:
✅ new Child()로 Child 인스턴스를 생성
✅ 참조변수 gp의 타입은 GrandParent이므로 업캐스팅(upcasting)이 일어남
✅ 즉, gp는 실제 Child 객체를 가리키지만, gp라는 참조 타입 자체는 GrandParent임
업캐스팅: 자식 → 부모
→ Child를 GrandParent 참조로 받았으므로 업캐스팅이 일어났습니다.
이 상태의 특징:
GrandParent 타입의 것들만 보임.print() 메서드 호출은 동적 바인딩되어 Child의 오버라이딩 메서드가 호출됨.다운캐스팅: 부모 참조 → 자식 참조
→ 이미 업캐스팅된 상태에서 Child나 Parent 타입 참조로 바꾸기
이 부분이 헷갈리는 이유:
부모 참조(
gp)로 자식의 고유 멤버(Child에만 있는 필드나 메서드)를 쓰려면 반드시 캐스팅해야 합니다.
Parent p = (Parent) gp;gp 참조의 실제 객체 타입은 Child.Child는 Parent의 하위 클래스이므로 캐스팅 가능.p 참조의 타입은 Parent이므로 Parent에 정의된 멤버들(예: Parent의 value 필드나 Parent의 메서드들) 볼 수 있음.print())를 호출하면 여전히 Child의 메서드가 호출됨.Child c = (Child) gp;gp가 가리키고 있는 건 Child 객체.gp를 Child로 캐스팅하는 것은 안전.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)를 참조
캐스팅(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 참조대상에는 영향 없음