KB 13 init 과 부생성자 — 초기화는 선언 순서대로
고친 사람 github-actions[bot]
0. 첫 예제 — 무엇이 먼저 찍힐까
자바에서는 필드 초기화가 끝난 뒤에 생성자 몸통이 돈다고 배웁니다. 코틀린에서 클래스 이름 뒤 괄호로 적는 생성자에는 그 몸통 대신 init 블록이 있습니다. 그런데 이 블록은 클래스 안 여기저기에 몇 개든 흩어 둘 수 있습니다.
값을 정하는 줄 둘과 init 블록 둘을 번갈아 적은 클래스를 만들었습니다.
class Cup(size: Int) {
val first = note("property first", size)
init {
println("init 1")
}
val second = note("property second", first * 2)
init {
println("init 2")
}
}
fun note(label: String, value: Int): Int {
println(label)
return value
}
fun main() {
Cup(10)
}
Cup(10) 한 번이 네 줄을 찍습니다. 프로퍼티 둘이 먼저일까요, init 블록 둘이 먼저일까요.
$ kotlinc Cup.kt -d out
$ kotlin -cp out CupKt
property first
init 1
property second
init 2
위에서 아래로 적은 순서대로 찍혔습니다. 프로퍼티가 전부 먼저 초기화되는 게 아니라, 프로퍼티와 init 블록이 번갈아 나옵니다.
class Cup(size: Int)— 클래스 이름 뒤 괄호가 생성자의 인자 목록입니다. 클래스 머리에 붙은 이 생성자를 주생성자(primary constructor)라고 부릅니다. 부를 때는new없이Cup(10)으로 씁니다val first = ...—val은 다시 대입할 수 없는 변수입니다. 클래스 안에 두면 필드와 게터를 한 이름으로 묶은 프로퍼티(property)가 됩니다.=뒤의 식이 초기값입니다init { ... }— 객체를 만들 때 실행할 코드를 담는 블록입니다. 1절에서 자세히 봅니다fun note(...)— 받은 이름을 찍고 값을 돌려주는 함수입니다. 클래스 밖, 파일 바로 아래에 둔 이런 선언을 톱레벨(top-level) 선언이라고 부릅니다
Cup.kt 로 저장해 코틀린 컴파일러 kotlinc 로 컴파일해 돌렸습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션이고, kotlin 은 java 에 해당하는 실행 명령입니다. 뒤에 적은 CupKt 는 톱레벨 main 이 들어간 클래스입니다 — JVM(자바 가상 머신)에서는 모든 함수가 어떤 클래스에 속해야 해서, 코틀린 컴파일러가 톱레벨 선언을 파일 이름 + Kt 라는 클래스에 모아 넣습니다.
이 편은 이 순서를 자바의 인스턴스 초기화 블록과 견주어 확인합니다. 그다음 생성자를 여럿 둘 때의 규칙과, 순서 때문에 생기는 함정을 봅니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.
같은 모양을 자바로 옮겼습니다. 자바에도 비슷한 문법이 있습니다. 클래스 몸통에 이름 없이 중괄호만 둔 블록, 곧 인스턴스 초기화 블록입니다.
public class Cup {
int first = note("field first", 10);
{
System.out.println("block 1");
}
int second = note("field second", first * 2);
{
System.out.println("block 2");
}
public Cup() {
System.out.println("constructor body");
}
// note 와 main 은 Cup.kt 와 같은 일을 한다
}
$ javac -d jout Cup.java
$ java -cp jout Cup
field first
block 1
field second
block 2
constructor body
note 와 main 은 줄여 실었습니다. 자바의 필드 초기화 식은 생성자 인자를 읽을 수 없어 10 을 바로 적었습니다. 이 차이는 1절에서 봅니다. 자바 쪽 결과물은 섞이지 않게 jout 폴더에 둡니다.
자바도 필드와 블록을 적힌 순서대로 번갈아 실행했습니다. 눈에 띄는 다른 점은 맨 끝의 constructor body 한 줄입니다. 두 출력을 줄마다 맞대 보는 것은 다음 절에서 합니다.
1. init 블록 — 몸통 없는 주생성자에 코드를 넣는 곳
class Cup(size: Int) 의 주생성자는 괄호 안 인자 목록뿐입니다. 자바 생성자처럼 코드를 적을 중괄호가 없습니다.
init 블록은 주생성자의 몸통 역할을 합니다. 객체를 만들 때 한 번 돌고, 한 클래스에 몇 개든 둘 수 있습니다. 여러 개면 0절에서 본 대로 프로퍼티 초기화 식과 섞여 적힌 순서대로 돕니다.
생성자 인자는 어디서 읽히나
자바 생성자 인자는 생성자 몸통 안에서만 읽힙니다. 코틀린 주생성자 인자는 몸통이 따로 없으니, 읽히는 범위가 다릅니다.
인자 size 를 세 군데에서 읽는 클래스를 만들었습니다. 프로퍼티 초기화 식, init 블록, 그리고 멤버 함수 show 입니다.
class Glass(size: Int) {
val doubled = size * 2
init {
println(size)
}
fun show() {
println(size) // 컴파일 오류
}
}
$ kotlinc Glass.kt -d out
Glass.kt:9:17: error: unresolved reference 'size'.
println(size)
^^^^
에러는 show 안의 println(size) 하나에만 났습니다. unresolved reference 는 그 이름을 찾지 못했다는 뜻입니다. 주생성자 인자는 프로퍼티 초기화 식과 init 블록에서만 보이는 이름입니다. 같은 클래스 몸통에 있어도 멤버 함수 안에서는 보이지 않습니다.
자바에서는 반대 방향이 막힙니다. 생성자 Glass(int size) 를 두고, 필드 초기화 식에서 그 인자 size 를 읽은 Glass.java 를 컴파일했습니다.
$ javac -d jout Glass.java
Glass.java:2: error: cannot find symbol
int doubled = size * 2;
^
symbol: variable size
location: class Glass
1 error
자바의 필드 초기화 식은 생성자 인자를 모릅니다. 생성자 몸통만이 인자를 읽을 수 있습니다.
init 블록은 따로 부르는 메서드가 아니다
init 블록이 메서드로 따로 생기는지는 돌려 봐서는 알 수 없습니다. 실행 출력에 나오는 것은 찍힌 줄뿐이고, 어떤 멤버가 만들어졌는지는 컴파일 결과인 .class 파일, 곧 클래스 파일 안에 있습니다.
JDK 에 딸린 javap 가 그 멤버 목록을 자바 선언문처럼 보여 줍니다. -p 는 private 멤버까지 달라는 옵션입니다. 0절의 Cup 을 그대로 열었습니다.
$ javap -p out/Cup.class
Compiled from "Cup.kt"
public final class Cup {
private final int first;
private final int second;
public Cup(int);
public final int getFirst();
public final int getSecond();
}
init이라는 메서드가 없습니다. 멤버는 필드 둘, 생성자Cup(int)하나, 게터 둘이 전부입니다size라는 필드도 없습니다. 주생성자 인자는 생성자의 인자로만 남습니다. 초기화 코드 밖에서는 쓸 수 없는 이름이니 필드로 남길 까닭이 없습니다
init 블록은 따로 부르는 메서드가 아닙니다. 주생성자 인자와 함께 생성자 Cup(int) 안으로 들어갑니다. 그 안에서 어떤 순서로 도는지는 다음 절에서 봅니다.
2. 초기화 순서는 선언 순서다 — 자바도 같은 규칙이다
0절의 두 출력을 줄마다 맞대면 규칙이 드러납니다. 왼쪽이 코틀린 Cup(10) 이, 오른쪽이 자바 new Cup() 이 찍은 줄입니다.
| 소스에 적은 순서 | 코틀린 출력 | 자바 출력 |
|---|---|---|
| 첫 번째 초기화 식 | property first |
field first |
| 첫 번째 블록 | init 1 |
block 1 |
| 두 번째 초기화 식 | property second |
field second |
| 두 번째 블록 | init 2 |
block 2 |
| 생성자 몸통 | 적을 곳이 없다 | constructor body |
다섯 줄 가운데 넷이 나란히 섭니다. 두 언어 모두 초기화 코드를 위에서 아래로 적힌 순서대로 한 번씩 실행합니다. 프로퍼티가 전부 먼저도 아니고 블록이 전부 먼저도 아닙니다.
앞의 것이 먼저 끝난다는 말은 뒤의 것이 앞의 것을 읽을 수 있다는 뜻이기도 합니다. second 를 만드는 식 note("property second", first * 2) 가 first 를 읽는데, 출력에서 property first 가 이미 지나간 뒤라 읽을 값이 들어 있습니다. 위에서 아래로 한 줄씩 실행하는 평범한 생성자 몸통이라고 보면 맞습니다(공식 문서).
갈리는 것은 표의 마지막 줄 하나입니다. 자바는 필드와 블록을 다 돌린 뒤에 생성자 몸통이 옵니다. 코틀린 주생성자에는 그 몸통을 적을 중괄호가 아예 없어서 init 2 에서 끝납니다.
그러면 자바에서 생성자 몸통에 적던 코드는 코틀린에서 어디로 갈까요. 하나는 지금까지 본 init 블록이고, 다른 하나가 다음 절의 부생성자 몸통입니다.
3. 부생성자 — constructor(...) : this(...) 로 반드시 주생성자를 거친다
자바에서는 인자 개수가 다른 생성자를 여러 개 두곤 합니다. 코틀린 주생성자는 클래스 머리에 하나뿐이라, 두 번째 생성자는 다른 문법으로 적습니다.
좌석 번호만 받으면 1열로 정하는 생성자를 하나 더 둔 클래스입니다.
class Ticket(val row: Int, val seat: Int) {
init {
println("init")
}
constructor(seat: Int) : this(1, seat) {
println("secondary body")
}
}
fun main() {
Ticket(7)
}
Ticket(7) 은 부생성자를 부릅니다. 무엇이 먼저 찍힐지 돌려 봤습니다.
$ kotlinc Ticket.kt -d out
$ kotlin -cp out TicketKt
init
secondary body
부생성자를 불렀는데 init 블록이 먼저 찍혔습니다. 부생성자 몸통은 그 뒤입니다.
class Ticket(val row: Int, val seat: Int)— 주생성자 인자 앞에val을 붙이면 그 인자가 곧 프로퍼티가 됩니다. 1절의size와 달리 필드와 게터가 생깁니다constructor(seat: Int) { ... }— 클래스 몸통 안에constructor키워드로 적는 생성자를 부생성자(secondary constructor)라고 부릅니다. 중괄호 안이 몸통입니다: this(1, seat)— 몸통보다 먼저 같은 클래스의 다른 생성자를 부르는 구문입니다. 자바 생성자 첫 줄의this(1, seat);에 해당합니다. 이렇게 다른 생성자에 넘기는 것을 위임(delegation)이라고 부릅니다
부생성자 몸통도 객체를 만드는 동안 돕니다. 그래도 val 이 없는 주생성자 인자는 1절의 size 처럼 여기서도 보이지 않습니다. class S(size: Int) 의 부생성자 몸통에서 size 를 읽자 1절과 같은 unresolved reference 'size' 에러가 났습니다.
부생성자가 주생성자를 거치는 문법
init 이 먼저, secondary body 가 나중으로 선 까닭은 : this(1, seat) 한 줄에 있습니다. 부생성자의 중괄호가 열리기 전에 적는 이 구문이 주생성자를 먼저 부릅니다.
자바에도 같은 뜻의 문법이 있습니다. 적는 곳만 다릅니다.
| 자바 | 코틀린 | |
|---|---|---|
| 다른 생성자를 부르는 구문 | this(1, seat); |
: this(1, seat) |
| 적는 곳 | 생성자 몸통의 첫 문장 | 인자 목록과 몸통 사이 |
| 생략 | 할 수 있다 | 주생성자가 있으면 못 한다 (4절) |
그래서 Ticket(7) 은 제 몸통에 들어가기 전에 주생성자 Ticket(1, 7) 을 지납니다. 주생성자가 row·seat 를 채우고 init 블록을 돌린 뒤에야 부생성자 몸통으로 돌아옵니다. 흐름을 그리면 이렇습니다.
flowchart TD
A["Ticket(7) 호출"] --> B["부생성자 Ticket(int seat)"]
B --> C["첫 줄 this(1, 7) 로 주생성자 호출"]
C --> D["row = 1, seat = 7 대입"]
D --> E["init 블록: init 출력"]
E --> F["부생성자로 돌아와 몸통: secondary body 출력"]
자바는 찍힌 순서만 같다
자바 쪽 Ticket.java 는 final 필드 둘, 초기화 블록 하나, this(1, seat); 로 넘기는 생성자 하나로 썼습니다. 이것도 block, secondary body 순서로 찍었습니다. 찍힌 순서는 같습니다. 생성자 안의 순서는 다릅니다. 이것만은 소스를 봐도 드러나지 않아, 클래스 파일을 거꾸로 읽어 자바 코드로 다시 적는 도구인 디컴파일러 CFR 0.152 로 인자 둘을 받는 쪽을 열었습니다. 결과에서 머리 주석과 import 같은 곁가지는 ... 로 줄여 싣습니다.
$ java -jar cfr.jar jout/Ticket.class
...
public Ticket(int n, int n2) {
System.out.println("block");
this.row = n;
this.seat = n2;
}
...
javac 는 초기화 블록을 생성자 몸통의 대입보다 앞에 뒀습니다. 2절에서 자바 Cup 의 constructor body 가 맨 끝에 찍힌 것과 같은 규칙입니다 — 초기화 블록이 먼저이고 생성자 몸통이 나중입니다.
코틀린은 반대 순서입니다. val row 와 val seat 는 클래스 머리에 적은 선언이라 init 블록보다 위에 있고, 2절에서 본 대로 위에 적은 것이 먼저 돕니다. 찍히는 줄은 같아도 대입과 init 사이의 앞뒤는 두 언어에서 갈립니다.
4. 위임을 빼면 — 두 컴파일러가 막는 이유가 다르다
차이는 위임을 빼 볼 때 더 드러납니다. Ticket 에서 init 블록과 main 을 걷고 부생성자의 : this(1, seat) 를 지운 Pass.kt 를 컴파일했습니다.
$ kotlinc Pass.kt -d out
Pass.kt:2:5: error: primary constructor call expected.
constructor(seat: Int) {
^^^^^^^^^^^^^^^^^^^^^^
primary constructor call expected 는 주생성자 호출이 와야 한다는 뜻입니다. 주생성자가 있는 클래스의 부생성자는 반드시 주생성자로 위임해야 합니다. 다른 부생성자를 거쳐도 되지만, 끝은 주생성자여야 합니다. 이 위임 덕분에 주생성자의 초기화 코드는 부생성자 몸통보다 먼저 돕니다(공식 문서).
자바는 위임을 강제하지 않습니다. 같은 모양으로 옮긴 Pass.java 입니다.
public class Pass {
final int row;
final int seat;
public Pass(int row, int seat) {
this.row = row;
this.seat = seat;
}
public Pass(int seat) {
System.out.println("secondary body");
}
}
$ javac -d jout Pass.java
Pass.java:12: error: variable row might not have been initialized
}
^
1 error
에러는 두 번째 생성자를 닫는 중괄호에 났습니다. javac 는 위임이 없다고 탓하지 않습니다. final 필드 row 에 값을 넣지 않은 채 생성자가 끝났다고 탓합니다. 두 필드에서 final 만 뗀 같은 클래스는 에러 없이 컴파일됐습니다.
주생성자가 있는 코틀린 클래스에서 부생성자는 언제나 주생성자를 먼저 거칩니다. 그래서 프로퍼티 초기화 식과 init 블록은 어느 생성자로 만들어도 한 번씩 돕니다. 부생성자 몸통은 언제나 그 뒤입니다.
5. 주생성자가 없는 클래스의 부생성자
클래스 이름 뒤에 괄호를 안 쓰면 주생성자가 없습니다. 그러면 부생성자가 위임할 주생성자도 없습니다. 그때 init 블록은 언제 돌까요.
부생성자 셋을 둔 Door 입니다. 둘은 위임 없이, 하나는 다른 부생성자로 위임합니다.
class Door {
init {
println("init")
}
constructor(width: Int) {
println("constructor Int")
}
constructor(open: Boolean) {
println("constructor Boolean")
}
constructor() : this(90) {
println("constructor no-arg")
}
}
fun main() {
Door(true)
Door()
}
$ kotlinc Door.kt -d out
$ kotlin -cp out DoorKt
init
constructor Boolean
init
constructor Int
constructor no-arg
주생성자가 없는데도 init 블록이 두 번 돌았습니다. 객체 하나에 한 번씩입니다. Door() 는 Door(90) 을 거쳤는데도 init 이 두 번 찍히지 않았습니다.
class Door— 이름 뒤에 괄호가 없으니 주생성자가 없습니다constructor(width: Int)—: this(...)가 없습니다. 주생성자가 없는 클래스에서는 위임 없이 적어도 됩니다constructor() : this(90)— 부생성자끼리도 위임합니다. 인자 없이 만들면Door(90)을 거칩니다
어느 생성자로 만들어도 init 은 한 번씩 돈다
위임할 주생성자가 없는데 어떻게 객체마다 한 번씩만 돌았을까요. 세 생성자를 CFR 로 열었습니다.
$ java -jar cfr.jar out/Door.class
...
public final class Door {
public Door(int width) {
System.out.println((Object)"init");
System.out.println((Object)"constructor Int");
}
public Door(boolean open) {
System.out.println((Object)"init");
System.out.println((Object)"constructor Boolean");
}
public Door() {
this(90);
System.out.println((Object)"constructor no-arg");
}
}
init 블록의 코드가 위임하지 않는 생성자 둘에 하나씩 복사됐습니다. 위임하는 Door() 에는 없습니다. this(90) 으로 넘어간 Door(int) 에서 이미 돌기 때문입니다.
그래서 어느 생성자로 만들든 init 블록은 정확히 한 번 돕니다. 주생성자가 없어도 위임은 암묵적으로 일어나는 셈입니다(공식 문서).
자바의 모든 클래스가 이 모양이다
자바에는 주생성자라는 구분이 없습니다. 그래서 자바 클래스는 늘 이 절의 Door 와 같은 처지입니다.
init 블록을 인스턴스 초기화 블록으로, 부생성자 셋을 자바 생성자 셋으로 옮긴 Door.java 는 코틀린과 같은 순서로 찍었습니다. 객체 둘을 만드는 동안 초기화 블록이 한 번씩 돌았고, this(90) 으로 넘긴 생성자에서는 따로 돌지 않았습니다.
초기화 블록을 생성자에 넣는 방식은 자바와 같습니다. 주생성자가 있는 코틀린 클래스만 모든 생성자가 한 곳을 거치도록 한 단계 더 조입니다.
6. 함정 — 초기화 전 프로퍼티를 읽으면 null 이나 0 이 보인다
초기화가 선언 순서대로라면, 위의 init 블록이 도는 동안 아래에 선언한 프로퍼티는 아직 값이 없습니다. 함정 둘을 실행해 봅니다 — 바로 읽기, 그리고 함수를 거친 읽기입니다.
바로 읽으면 컴파일러가 막는다
init 블록에서, 그보다 아래에 선언한 프로퍼티를 이름만 적어 읽는 클래스입니다.
class Early {
init {
println(name)
}
val name = "kim"
}
$ kotlinc Early.kt -d out
Early.kt:3:17: error: variable 'name' must be initialized.
println(name)
^^^^
코틀린은 컴파일을 막았습니다. must be initialized 는 읽기 전에 초기화돼 있어야 한다는 뜻입니다.
자바로 같은 모양을 옮겼습니다. Early.java 는 초기화 블록과 그 아래 필드 String name = "kim"; 입니다.
$ javac -d jout Early.java
Early.java:3: error: illegal forward reference
System.out.println(name);
^
1 error
illegal forward reference 는 아래에 선언한 필드를 앞에서 참조했다는 뜻입니다. 이름만 적어 바로 읽으면 두 컴파일러가 모두 막습니다.
두 언어는 this.name 으로 적을 때 갈립니다. 자바는 초기화 블록의 System.out.println(this.name); 을 통과시키고, 돌리면 null 을 찍습니다. 코틀린은 println(this.name) 도 같은 variable 'name' must be initialized 에러로 막습니다.
함수를 거치면 막지 못한다
이번에는 init 블록에서 멤버 함수를 부릅니다. 그 함수 안에서 아직 초기화 전인 프로퍼티를 읽습니다.
class Profile {
init {
describe()
}
val name: String = "kim"
val age: Int = 30
fun describe() {
println(name) // null
println(age) // 0
println(name.length) // 예외
}
}
fun main() {
Profile()
}
kotlinc 는 에러도 경고도 없이 컴파일했습니다. 앞의 두 줄이 찍은 값은 코드 오른쪽 주석에 붙여 뒀습니다. 셋째 줄에서는 예외와 함께 스택 트레이스(예외가 난 곳까지 거쳐 온 호출 목록)가 찍힙니다. 앞의 두 값은 아래 출력에서 ... 로 줄였습니다.
$ kotlinc Profile.kt -d out
$ kotlin -cp out ProfileKt
...
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "this.name" is null
at Profile.describe(Profile.kt:12)
at Profile.<init>(Profile.kt:3)
at ProfileKt.main(Profile.kt:17)
at ProfileKt.main(Profile.kt)
null—null을 받지 않는 타입인String프로퍼티에서null이 나왔습니다0—age는 30 이 아니라 0 입니다NullPointerException—name.length에서 멈췄습니다.Profile.<init>은 JVM 이 생성자에 붙이는 이름이고,Profile.kt:3은init블록의describe()줄입니다
이유는 소스에 적은 순서에 있습니다. describe() 를 부르는 init 블록이 name 과 age 선언보다 위에 있습니다. 2절의 규칙대로 위에 적은 것부터 도니, 두 프로퍼티에 값이 들어가는 것은 describe() 가 끝난 다음입니다. 스택 트레이스에서 Profile.<init> 이 가리키는 Profile.kt:3 이 바로 그 describe() 호출입니다.
JVM 은 객체를 만들 때 필드를 먼저 기본값으로 채워 둡니다. 참조 타입은 null, int 는 0 입니다. 대입 전의 describe() 는 그 기본값을 읽어 null 과 0 을 찍었습니다.
val name: String = "kim"— 이름 뒤 콜론 다음이 타입입니다. 코틀린의String은null을 받지 않는 타입입니다.null도 받으려면String?처럼?를 붙입니다name.length— 자바의name.length()입니다describe()—init블록에서 멤버 함수를 부릅니다. 이 줄은name과age보다 위에 있습니다
같은 모양으로 옮긴 Profile.java 도 null, 0 을 찍고 같은 문구의 NullPointerException 으로 멈췄습니다. 자바의 함정이 코틀린에도 남아 있는 것입니다. 코틀린의 String 타입도 초기화가 끝나기 전에는 null 이 아니라고 보장하지 못합니다.
init 블록에서 멤버 함수를 부를 때는 그 함수가 읽는 프로퍼티를 init 블록보다 위에 선언합니다. 코틀린 컴파일러는 init 블록 안의 직접 읽기만 막고, 함수를 거친 읽기는 막지 못합니다.
7. 한 장 요약
코틀린은 프로퍼티 초기화 식과
init블록을 적힌 순서대로 주생성자 하나에 이어 붙인다. 주생성자가 있으면 부생성자는 반드시 주생성자로 위임하므로 그 초기화가 부생성자 몸통보다 먼저 돈다. 주생성자가 없으면 자바처럼 위임하지 않는 생성자마다init블록이 복사된다. 순서대로 돌기 때문에, 아래에 선언한 프로퍼티를 함수로 먼저 읽으면null이나 0 이 보인다.
| 하는 일 | 자바 | 코틀린 | 확인한 방법 |
|---|---|---|---|
| 생성할 때 도는 코드 | 인스턴스 초기화 블록 { } · 생성자 몸통 |
init { } · 부생성자 몸통 |
실행 |
| 초기화 코드의 순서 | 필드·블록을 적힌 순서대로, 생성자 몸통은 맨 끝 | 프로퍼티·init 을 적힌 순서대로, 부생성자 몸통은 맨 끝 |
실행 |
| 초기화 코드가 들어가는 곳 | 생성자 안 (init 같은 메서드 없음) |
생성자 안 (init 같은 메서드 없음) |
javap -p · 디컴파일 |
| 생성자 인자를 읽는 범위 | 생성자 몸통만 | 프로퍼티 초기화 식 · init 블록 |
kotlinc·javac 에러 |
| 다른 생성자로 위임 | 첫 줄 this(...); (선택) |
: this(...) (주생성자가 있으면 필수) |
primary constructor call expected |
| 위임 없는 생성자 | 초기화 블록이 복사된다 | 주생성자가 없을 때만 허용, init 이 복사된다 |
CFR |
| 아래 필드를 바로 읽기 | illegal forward reference (this.name 은 통과해 null) |
variable 'name' must be initialized (this.name 도) |
컴파일 에러 · 실행 |
| 함수를 거쳐 읽기 | 기본값 null·0 이 보인다 |
기본값 null·0 이 보인다 |
실행 |
관련 항목
코틀린 객체 초기화를 이루는 문법 요소
주 생성자 · 부 생성자 · init 블록 · 프로퍼티 · 프로퍼티 초기화 식 · 생성자 위임 · val
코틀린 초기화 문법에 대응하는 자바 요소
생성자 · 인스턴스 초기화 블록 · 필드 초기화 식 · 생성자 오버로딩 · this 생성자 호출 · final 필드
초기화 순서가 틀릴 때 나는 오류와 예외
전방 참조 · NullPointerException · 필드 기본값 · this 탈출 · 스택 트레이스
초기화 전 읽기를 피하는 다른 수단
lateinit · by lazy · 지연 초기화 · 널 안전성 · 널 허용 타입
이 편의 코드가 컴파일되어 생기는 클래스 파일 요소
컴파일 결과를 열어 보는 도구
javap · 디컴파일 · 디컴파일러 · CFR