class A {
int cost = 1;
A() {
System.out.println("A");
outputCost();
}
void outputCost() {
System.out.println(cost);
}
}
class B extends A {
int cost = 2;
B() {
System.out.println("B");
outputCost();
}
@Override
void outputCost() {
System.out.println(cost);
}
}
public class Main {
public static void main(String[] args) {
A b = new B();
b.outputCost();
}
}
이때 출력이 어떻게 될까?
B
2
2
라고 생각한 사람은, 이 글(부모 클래스 생성자는 자동으로 실행된다?)을 읽고 오기를 추천한다.
A
1 ←
B
2
2
라고 생각했거나,
A
2 ←
B
2
2
라고 생각했다면
그래도 이 글(같은 이름의 메서드지만 다르게 실행되는 이유)까지는 알고 있는 사람일 것이다.
근데 정답은
A
0
B
2
2
이다..⁉
위에서 내가 말한 두 글을 모두 이해한 사람이라면
어떻게 생각했을지에 맞춰서 코드 흐름을 한 번 따라가보자.
public class Main {
public static void main(String[] args) {
A b = new B(); ←
b.outputCost();
}
}
new B Class에 의해서 B Class 생성자가 호출된다.
B() {
System.out.println("B");
outputCost();
}
B Class는 A Class를 상속 받은 자식 Class이기 때문에, 부모 Class의 생성자가 먼저 호출된다.
즉, 코드로 엄밀히 따져 보자면
B() {
super() ← 부모 클래스 생성자 호출
System.out.println("B");
outputCost();
}
이렇게 부모 클래스 생성자를 먼저 호출한다.
A() {
System.out.println("A"); ←
outputCost();
}
해당 프린트문이 실행되면서 출력으로는 A가 찍힌다.
그런다음 우리가 헷갈린 부분이다.
outputCost()은 어느것이 실행될까?
이 부분은 동적 바인딩의 개념이 필요하다.
동적 바인딩이랑 실행되는 시점에 JVM이 객체의 실체 타입을 보고 어떤 메서드를 실행할지 결정한다는 것이다.
현재 객체는 자식 객체인 B 클래스로 생성되었고,
현재 B 클래스 내부에는 outputCost() 메서드가 오버라이딩 되어 있다.
그렇기에 A 클래스의 메서드가 아닌,
B 클래스의 메서드가 실행되는 것이다.
@Override
void outputCost() {
System.out.println(cost);
}
이 코드가 실행된다.
근데 이때 cost 값은 몇일까?
0이다!
왜냐면 부모 생성자부터 호출했기 때문에
자식 클래스인 B는 아직 생성이 되지 않았기 때문이다.
생성자가 실행이 되어야 비로소 인스턴스 영역이 힙 메모리에 탑재가 된다.
B 클래스의 '클래스 정보'(메서드, 변수 선언 등)는 이미 메소드 영역에 올라가 있지만,
실제로 인스턴스 변수(cost = 2)가 설정되는 'B 생성자'는 아직 실행되지 않았기 때문에
기본값 0으로 남아있다.
그렇기에 0이 출력된다.
B() {
super();
System.out.println("B"); ←
outputCost(); ←
}
B가 출력으로 나온다.
outputCost(); 부분은 오버라이딩된 B 클래스의 메서드가 실행되므로 초기화된 인스턴스 변수 2가 출력으로 나온다.
B의 인스턴스 변수가 2로 초기화 돼 있기에 2가 출력으로 나온다.
이 문제를 통해 자바에서 객체 생성 순서와 동적 바인딩,
그리고 초기화 타이밍의 미묘한 차이를 명확히 이해할 수 있었다.
앞으로 생성자 내부에서 오버라이딩된 메서드를 호출할 때는 이런 타이밍을 꼭 고려해야겠다...🤮