init block은 필드에 값이 할당된 후 실행되는가?
Kotlin IN ACTION 이라는 책에서 다음과 같은 부분을 확인할 수 있다.
초기화 블록에는 클래스의 객체가 만들어질 때(인스턴스화될 때) 실행될 초기화 코드가 들어간다.
초기화 블록은 주 생성자와 함께 사용된다.
그런데 함께 사용된다 라는 말이 조금 거슬린다.
가령, init block 내부에서 인스턴스에 정의된 메서드를 사용해도 문제가 없는가?
class MyClass(
val foo: Int;
) {
init {
this.do()
}
fun do() {
foo = 1;
}
}
만약 이게 제대로 동작한다면, '함께 사용' 이란 말이 조금 어색하지 않나...? 하는 생각이다.
(메서드는 분명 인스턴스 생성 이후에 사용할 수 있을것이기 때문)
하지만 걱정과는 다르게 위 코드는 잘 동작한다.
다음과 같은 테스트를 만들어서 시행해보니 통과한다. (Junit5
)
internal class MyClassTest {
@Test
fun doTest() {
val myClass = MyClass(123)
myClass.foo eq 1
}
}
왜 그런지 잘 모르겠어서 조금 더 파보았다.
다음과 같이 NoInit.kt
와 YesInit.kt
클래스를 만들어서 bytecode를 비교해봤다.
// NoInit.kt
class NoInit( var foo: Int )
// YesInit.kt
class YesInit( var foo: Int ) {
init {
this.dod()
}
fun dod() {
this.foo = 1
}
}
bytecode로 변환 후 init 메서드를 확인해보자
// NoInit.class
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ILOAD 1
PUTFIELD NoInit.foo : I
RETURN
L1
LOCALVARIABLE this LNoInit; L0 L1 0
LOCALVARIABLE foo I L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
// YesInit.class
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ILOAD 1
PUTFIELD YesInit.foo : I
L1
LINENUMBER 11 L1
NOP
L2
LINENUMBER 12 L2
ALOAD 0
INVOKEVIRTUAL YesInit.dod ()V
L3
LINENUMBER 13 L3
RETURN
L4
LOCALVARIABLE this LYesInit; L0 L4 0
LOCALVARIABLE foo I L0 L4 1
MAXSTACK = 2
MAXLOCALS = 2
음... 난 Bytecode를 처음보니, 직접 비교해보자! (궁금하면 jvm instruction set 문서를 살펴봐도 좋다)
NoInit과 YesInit 의 L0 는 거의 같다
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ILOAD 1
PUTFIELD (NoInit.foo/YesInit.foo) : I
(RETURN)
NoInit.class
에선 이게 끝이니 RETURN
이 포함된 모습이다.YesInit의 이후 부분을 살펴보자
L1
LINENUMBER 11 L1
NOP
L2
LINENUMBER 12 L2
ALOAD 0
INVOKEVIRTUAL YesInit.dod ()V
L3
LINENUMBER 13 L3
RETURN
L1
의 LINENUMBER 11 L1
이 부분은 실제 line-11에 위치한 init
구문을 의미하는 듯 하다하지만, 인스턴스화 되어서야 실행할 수 있는 dod() 메서드는 어떻게 실행 하는걸까?
자, 이번엔 YesInit.kt
의 인스턴스를 생성하는 Do.kt
의 Dodo()
메서드를 만들어서 Bytecode화 해봤다
// Do.kt
class Do {
fun Dodo() {
YesInit(123)
}
}
// bytecode
public final Dodo()V
L0
LINENUMBER 22 L0
NEW YesInit
DUP
BIPUSH 123
INVOKESPECIAL YesInit.<init> (I)V
POP
L1
LINENUMBER 23 L1
RETURN
L0
의 이 부분을 자세히 보자
NEW YesInit
DUP
BIPUSH 123
INVOKESPECIAL YesInit.<init> (I)V
→ 객체를 미리 만들고 (dod() 메서드가 사용가능한 상태가 되고) 나서 <init>을 따로 호출하는 것을 볼 수 있다.
그렇다. 인스턴스를 만드는 것과, 이를 초기화 하는것은 별개의 프로세스인 것이다. 이렇게 프로세스가 분리되어 있으므로, init block에서 메서드를 호출할 수 있었던 것이다.
: 필드에 값 주입 이후 init block이 실행된다. 이 과정은 '인스턴스 초기화' 과정에 속한다.
bytecode를 확인해 봤을때, '인스턴스 생성'과 '인스턴스 초기화'는 분리되어있는 과정이라 추론 가능하다.
따라서, 인스턴스 메서드는 '생성'과정 이후 available 하고, 초기화 과정해서 이 메서드를 활용하는것은 어색하지 않다.