이것이 자바다 책을 보면서 조금 애매한 설명이 있어서 추가로 공부하게 되었다.

 

 

[레퍼런스] Chapter 12. Execution | Oracle Java SE 21

 

흔히 우리가 알던 자바 버전에서는 객체를 생성하게 되면 다음과 같은 순서로 진행된다.

  1. 메모리 공간 할당
  2. 모든 인스턴스 변수(상위 클래스 포함) 기본값으로 초기화
  3. 새 객체를 초기화 하기 위해 생성자 처리 5단계 진행
  4. 예외 없이 정상적으로 생성자 처리가 끝나면 새로 생성된 객체에 대한 참조가 결과로 반환

 

생성자 체이닝 (Constructor Chaining)

생성자가 다른 생성자를 호출하는 메커니즘

두 가지 형태가 있다.

  • super(...) → 부모 클래스 생성자 호출
  • this(...) → 같은 클래스의 다른 생성자 호출

 

생성자 처리 5단계

  1. 생성자 인자를 생성자를 호출하기 위해 매개변수 변수들에 할당한다. (그냥 함수 호출하기 위한 과정이라고 보면 된다.)
  2. 생성자 첫 줄이 this(...)면, 해당 생성자에 대해 step 1~5를 재귀적으로 먼저 처리한다. 정상적으로 실행이 끝나면 step 5로 이동한다.
    • 왜 바로 step 5로 가냐? → 같은 클래스에서 step 4initializers가 중복 실행되지 않도록 (super 를 호출하는 this 체인의 맨 끝에서 step 4가 한 번 실행될 수 있게)
  3. this(...)가 아니면, Object가 아닌 한 super(...)를 명시/암묵적으로 호출하고, 그 슈퍼 생성자 호출을 재귀적으로 처리한다. 끝나면 step 4 진행.
    • 생성자 첫 줄에 super()가 없더라도, 컴파일러가 자동으로 넣어준다.
    • 따라서 부모 생성자가 항상 먼저 실행됨
    • Object 클래스는 부모가 없으므로 재귀 종료 시점이 된다.
  4. 현재 클래스에 적힌 필드 초기값 설정(instance variable initializer)과 인스턴스 초기화 블록(instance initializer)을 소스 코드 순서대로 실행한다.
  5. 생성자 본문의 나머지(super/this 호출 이후)를 실행한다.

한 줄로 표현하자면, 재귀 호출을 통해 최상위 클래스(Object)까지 올라갔다가 다시 원래 클래스로 내려오면서 필드를 초기화하고 생성자 본문을 실행한다고 보면 된다.

이 과정에서 예외가 한 번이라도 터지면 객체 생성은 실패한다.

 

Java 25: Flexible Constructor Bodies (JEP 513)

JEP 513: Flexible Constructor Bodies

그런데 Java 25부터 생성자 본문에서 명시적 생성자 호출(super(...) 또는 this(...)) 앞에 문장을 쓸 수 있게 변경되었다.

 

이제 생성자 본문이 두 구간으로 나뉜다.

  • 프롤로그(Prologue) : 생성자 호출 전에 오는 문장들
  • 에필로그(Epilogue) : 생성자 호출 후에 오는 문장들

 

목표

  • 상위 클래스 생성자를 호출하기 전에 인자를 쉽게 검증할 수 있다.
  • 새 객체의 상태를 다른 코드에서 사용되기 전에 초기화할 수 있다.
  • 생성자들끼리 서로 상호 작용해서 생성 과정을 재구상할 수 있다.

 

Early construction contexts

명시적 생성자 호출에서 인자들은 정적 컨텍스트(static context)에 있다고 한다. 즉, 인스턴스가 존재하지 않은 정적 메서드 안의 코드처럼 취급된다. 이 개념을 수정하지 않는 대신 early consturction context를 만들었다고 한다. 여기엔 인자 + 프롤로그가 포함된다.

 

여기선 명시적이든 암시적이든 this 를 사용해서 현재 인스턴스를 참조, 필드 접근, 메서드 호출을 할 수 없다. 단, 현재 클래스에 선언된 필드 중에서 초기화식이 없는 필드에 대해선 단순 대입문은 허용된다고 한다.

class X {

    int i;
    String s = "hello";

    X() {

        System.out.print(this);  // 오류 - 현재 인스턴스를 명시적으로 참조함

        var x = this.i;          // 오류 - 현재 인스턴스의 필드를 명시적으로 참조함
        this.hashCode();         // 오류 - 현재 인스턴스의 메서드를 명시적으로 참조함

        var y = i;               // 오류 - 현재 인스턴스의 필드를 암시적으로 참조함
        hashCode();              // 오류 - 현재 인스턴스의 메서드를 암시적으로 참조함

        i = 42;                  // OK - 초기화식이 없는 필드에 대한 대입

        s = "goodbye";           // 오류 - 초기화식이 있는 필드에 대한 대입

        super();

    }

}

 

추가로, 슈퍼 클래스에 대한 필드나 메서드 호출도 금지된다.

class Y {
    int i;
    void m() { ... }
}

class Z extends Y {

    Z() {
        var x = super.i;         // 오류
        super.m();               // 오류
        super();
    }

}

 

바뀐 생성자 처리 7단계

[레퍼런스] Chapter 12. Execution | Oracle Java SE 25

 

JEP 513으로 인해 JLS 12.5도 바뀌었다.

 

  1. 생성자 인자를 생성자를 호출하기 위해 매개변수 변수들에 할당한다.
  2. 이 생성자가 명시적 생성자 호출을 포함하지 않으면 step 5로 이동한다.
  3. 생성자 본문에 프롤로그에 해당하는 BlockStatements 가 있으면 실행한다.
  4. 명시적 생성자 호출(this/super)에 대해 step 1~7을 재귀적으로 처리한다. 정상적으로 실행이 끝나면 다음과 같이 행동한다.
    1. this 였으면 → step 7으로 이동
    2. super 였으면 → step 6으로 이동
  5. 현재 생성자가 Object가 아닌 클래스의 생성자면 이 생성자는 인자 없는 슈퍼클래스 생성자에 대한 암묵적 호출을 가지고 있고, 이 역시 step 1~7을 재귀적으로 처리한다.
  6. 현재 클래스에 적힌 필드 초기값(instance variable initializer)과 인스턴스 초기화 블록(instance initializer)을 소스 코드 순서대로 실행한다.
  7. 이 생성자의 에필로그에 해당하는 BlockStatements 가 있으면 실행한다.

 

정리

간단하게 다음과 같이 표현하면 될 것 같다.

  • 생성자 처리 5단계: (super/this 호출) → (클래스당 한 번 initializer) → 생성자 남은 부분 실행
  • 생성자 처리 7단계: prologue → (super/this 호출) → (클래스당 한 번 initializer) → epilogue

비유하면 전위 순회에서 중위 순회로 바뀐 느낌이다.