KB 12 클래스와 주생성자 — 한 줄 클래스가 만드는 것
고친 사람 github-actions[bot]
0. 한 줄짜리 클래스를 자바로 쓰면 몇 줄인가
자바로 이름 하나를 담는 클래스를 쓰면 필드, 생성자, 게터를 손으로 적습니다. 코틀린은 이 한 줄로 끝납니다.
class User(val name: String)
class User— 클래스 선언입니다. 본문이 없으면 중괄호를 생략합니다(val name: String)— 클래스 이름 바로 뒤의 괄호가 생성자의 인자 목록입니다. 코틀린은 이 생성자를 주생성자(primary constructor)라고 부릅니다name: String— 코틀린은 이름 뒤에 콜론, 그 뒤에 타입을 씁니다val— 다시 대입할 수 없다는 표시입니다. 자바의final에 해당합니다
같은 모양을 자바로 손수 쓰면 이렇습니다.
public final class UserJ {
private final String name;
public UserJ(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
두 쪽을 맞대면 자바가 손으로 적는 네 가지가 코틀린 한 줄 안에 접혀 있습니다.
| 자바가 손으로 적는 것 | 코틀린 한 줄에서 |
|---|---|
private final String name; |
괄호 안의 val 이 대신합니다 |
public UserJ(String name) |
클래스 이름 뒤의 괄호가 생성자입니다 |
this.name = name; |
적지 않습니다 |
public String getName() |
괄호 안에 적은 이름 name 이 대신합니다 |
이렇게 필드와 게터를 한 이름으로 묶은 코틀린 선언을 프로퍼티(property)라고 부릅니다. val name 은 프로퍼티 name 을 선언한 것입니다. 코드에서는 user.name 처럼 이 이름으로 읽습니다.
표가 짝지은 대로 정말 멤버가 생기는지는 결과물을 열어 봐야 압니다. 이 한 줄을 s0/User.kt 로 저장해 코틀린 컴파일러 kotlinc 로 컴파일했습니다. 예제 파일은 절마다 폴더를 갈라 뒀습니다. s0/ 는 0절, s3err/ 는 3절의 에러 예제, s5jnew/ 는 5절에서 자바가 부르는 예제입니다.
JVM(Java Virtual Machine, 자바 가상 머신)은 클래스 파일이 자바에서 왔는지 코틀린에서 왔는지 따지지 않아서, 두 언어의 결과물은 같은 도구로 열어 맞댈 수 있습니다. 여기서 쓴 도구는 javap 입니다. JDK(Java Development Kit, 자바 개발 도구 묶음)에 딸린 도구로, 클래스 파일에 든 선언을 자바 선언문처럼 보여 줍니다.
$ kotlinc s0/User.kt -d out0
$ javap -p out0/User.class
Compiled from "User.kt"
public final class User {
private final java.lang.String name;
public User(java.lang.String);
public final java.lang.String getName();
}
-d out0— 결과물을out0폴더에 쓰라는 옵션입니다javap -p—-p는private멤버까지 보여 달라는 옵션입니다
소스에 없던 멤버가 셋 생겼습니다. private final 필드 name, 문자열 하나를 받는 생성자, 그리고 getName() 게터입니다. 위 표의 왼쪽 칸, 곧 자바가 손으로 적던 선언들입니다. 하나 다른 것은 게터에 붙은 final 인데, 코틀린은 수식어가 없으면 클래스와 메서드를 상속·재정의할 수 없는 final 로 둡니다. 상속은 KB 14 에서 다룹니다.
자바 열한 줄과 코틀린 한 줄이 같은 멤버 목록을 만듭니다. 이 편은 그 한 줄을 풀어 봅니다. 괄호 안의 val 을 빼면 무엇이 사라지는지, 본문에 프로퍼티를 더 두면 어떤 순서로 채워지는지 살핍니다. 게터·세터를 직접 쓰는 법과, 생성자를 여러 개 쓰던 자바 습관을 코틀린이 무엇으로 대신하는지도 따라갑니다.
출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.
1. 선언과 생성 — new 가 없다
클래스를 만들었으니 객체를 만들 차례입니다.
class User(val name: String)
fun main() {
val user = User("kim")
println(user.name) // kim
}
fun main()— 프로그램이 시작하는 함수입니다. 클래스 밖, 파일 바로 아래에 둡니다. 이런 선언을 톱레벨(top-level) 선언이라고 부릅니다. 안의println은 한 줄 출력입니다User("kim")— 생성자 호출입니다. 함수를 부르듯 클래스 이름 뒤에 괄호를 붙이고, 앞에new를 쓰지 않습니다user.name— 0절에서 본 프로퍼티name을 읽습니다
s1/Main.kt 로 저장해 컴파일하고 실행한 뒤, 클래스 파일을 디컴파일러 CFR 0.152 로 되돌렸습니다. 디컴파일(decompile)은 클래스 파일을 거꾸로 읽어 소스 코드 모양으로 다시 적는 일이라, 결과는 원래 소스를 되살린 것이 아니라 클래스 파일에 적힌 내용을 자바 문법으로 다시 적은 것입니다. 실행에 쓴 kotlin 은 코틀린 설치에 딸린 실행 명령으로, -cp 뒤에 클래스 파일이 든 폴더를 적습니다. 결과 맨 위의 머리 주석, import, 한 줄이 아주 긴 @Metadata(...)(코틀린 컴파일러가 남기는 메타데이터)는 ... 로 줄여 싣습니다.
$ kotlinc s1/Main.kt -d out1
$ kotlin -cp out1 MainKt
$ java -jar cfr.jar out1/MainKt.class
...
public final class MainKt {
public static final void main() {
User user = new User("kim");
System.out.println((Object)user.getName());
}
...
}
MainKt— 톱레벨 함수가 들어가는 클래스입니다. 이름은 파일 이름Main에Kt를 붙인 것입니다. 톱레벨 함수는 이 클래스의static메서드가 됩니다(→ KB 01 첫 파일 — fun main 한 줄이 JVM 에서 도는 법)(Object)— CFR 이 적은 캐스트입니다. 클래스 파일이println(Object)를 부른다는 사실을 이렇게 드러냅니다. 소스에 이런 캐스트는 없습니다- 끝의
...— 컴파일러가 덧붙인main(String[])을 줄였습니다
여기서 볼 것은 둘입니다.
첫째, CFR 이 되돌린 main() 에는 new User("kim") 이 있습니다. new 는 소스에서만 빠졌습니다. 클래스 파일은 자바와 똑같이 객체를 만들고 생성자를 부릅니다. 둘째, user.name 은 user.getName() 게터 호출로 바뀌었습니다.
자바 버릇대로 val user = new User("kim") 이라고 쓰면 kotlinc 가 unresolved reference 'new'. 로 거절합니다. 코틀린은 new 를 모르는 이름으로 읽습니다. 공식 문서도 객체를 만들 때 new 가 필요 없다고 밝힙니다(Classes).
반대로 자바에서 new 를 빼 봤습니다. s1j/Main.java 의 main 에 User user = User("kim"); 을 쓰고, 0절의 UserJ 와 같은 모양으로 쓴 s1j/User.java 와 함께 컴파일했습니다.
$ javac -d jout1 s1j/User.java s1j/Main.java
s1j/Main.java:3: error: cannot find symbol
User user = User("kim");
^
symbol: method User(String)
location: class Main
1 error
javac 는 User("kim") 을 User 라는 이름의 메서드 호출로 읽었습니다(symbol: method User(String)). 그런 메서드가 없으니 에러입니다. 자바에서 객체를 만드는 식은 new 뿐이라, 이름 뒤 괄호는 메서드 호출로 읽힙니다.
코틀린의 생성자 호출은 new 를 뺀 문법입니다. 클래스 파일에 남는 것은 자바의 new User("kim") 과 같습니다.
2. 주생성자 — val 을 붙이면 프로퍼티, 안 붙이면 인자
0절에서 필드와 게터를 만든 것은 괄호 안의 val 이었습니다. 이 절은 val, var, 아무것도 안 붙인 인자를 한 주생성자에 나란히 두고 javap -p 로 차이를 봅니다.
class Member(val name: String, nickname: String, var age: Int)
var age: Int—var는 다시 대입할 수 있다는 표시입니다.Int는 자바의int에 해당합니다nickname: String— 앞에val도var도 없는 인자입니다
s2/Member.kt 를 컴파일해 열었습니다.
$ kotlinc s2/Member.kt -d out2
$ javap -p out2/Member.class
Compiled from "Member.kt"
public final class Member {
private final java.lang.String name;
private int age;
public Member(java.lang.String, java.lang.String, int);
public final java.lang.String getName();
public final int getAge();
public final void setAge(int);
}
인자 셋이 서로 다르게 번역됐습니다. 줄을 맞추면 이렇습니다.
| 주생성자 인자 | 필드 | 게터 | 세터 |
|---|---|---|---|
val name: String |
private final String name |
getName() |
✗ |
nickname: String |
✗ | ✗ | ✗ |
var age: Int |
private int age (final 없음) |
getAge() |
setAge(int) |
nickname 은 생성자의 인자 목록 (String, String, int) 에만 남았습니다. val·var 가 없는 인자는 필드가 되지 않습니다. 생성자를 부를 때 받아서 초기화하는 동안에만 쓰는 보통 인자입니다.
그래서 객체 밖에서 읽을 수 없습니다. main 에서 println(m.nickname) 을 부르는 s2err/Member.kt 는 unresolved reference 'nickname'. 에러로 컴파일되지 않습니다.
자바에는 이 구분이 없습니다. 생성자 인자는 언제나 보통 인자입니다. 필드로 남기려면 필드 선언과 대입 줄을 따로 씁니다. 그 두 줄을 빠뜨린 s2j/MemberJ.java 입니다.
public class MemberJ {
public MemberJ(String name) {
}
public String getName() {
return name;
}
}
생성자는 인자를 받기만 합니다. 게터는 그 name 을 돌려주려 합니다.
$ javac -d jout2 s2j/MemberJ.java
s2j/MemberJ.java:6: error: cannot find symbol
return name;
^
symbol: variable name
location: class MemberJ
1 error
자바의 생성자 인자 name 은 생성자 괄호를 벗어나면 없는 이름입니다. 코틀린의 nickname 이 이 상태입니다. 괄호 안에 val 을 붙이는 것은 필드 선언, this.name = name 대입, 게터를 한꺼번에 적는 일입니다.
주생성자 괄호 안에서 val 은 필드와 게터를, var 는 필드와 게터·세터를 만듭니다. 아무것도 안 붙인 인자는 초기화하는 동안에만 보이는 인자로 남습니다.
자바에도 비슷한 한 줄이 있습니다. 자바 record 는 public record UserR(String name) {} 한 줄로 private final 필드와 생성자를 만듭니다. 다만 javap -p 로 열면 게터가 name() 이고 toString()·hashCode()·equals(java.lang.Object) 가 더 생깁니다. 코틀린에서 이 셋은 data class 가 만듭니다.
3. 본문의 프로퍼티 선언과 초기화
주생성자에 담지 않는 값은 클래스 본문에 프로퍼티로 둡니다.
초기값이 없으면
본문에 초기값 없는 프로퍼티 둘을 둔 s3err/Account.kt 입니다. balance 는 var, id 는 val 이고, 둘 다 = 뒤의 초기값이 없습니다.
class Account {
var balance: Int
val id: String
}
$ kotlinc s3err/Account.kt -d out3e
s3err/Account.kt:2:5: error: property must be initialized or be abstract.
var balance: Int
^^^^^^^^^^^^^^^^
s3err/Account.kt:3:5: error: property must be initialized or be abstract.
val id: String
^^^^^^^^^^^^^^
에러 문구가 요구하는 것은 초기값이 아니라 초기화(initialized)입니다. init { ... } 는 객체를 만들 때 실행할 문장을 담는 블록이고, 본문 프로퍼티를 여기서 채울 수 있습니다. 초기값도 없고 init 블록에서 채우지도 않으면 val 이든 var 든 컴파일되지 않습니다.
같은 두 줄 아래 init 블록을 두고 id = "x", balance = 5 로 채운 s3init/Account.kt 는 에러 없이 컴파일됩니다. init 블록의 자세한 모양은 아래 소절에서 봅니다.
문구의 abstract(추상)는 몸통 없이 선언만 두는 경우로, KB 14 에서 다룹니다.
s3err 의 두 줄을 자바 필드로 옮긴 s3j/AccountJ.java 입니다. id 에는 val 에 해당하는 final 을 붙였고, 생성자는 쓰지 않았습니다.
public class AccountJ {
int balance;
final String id;
}
$ javac -d jout3 s3j/AccountJ.java
s3j/AccountJ.java:3: error: variable id not initialized in the default constructor
final String id;
^
1 error
자바의 int balance; 는 에러 없이 지나갔습니다. 초기값 없는 필드는 0 이나 null 로 시작하기 때문입니다. 에러는 final 필드 id 에만 났습니다.
코틀린은 자바가 final 필드에만 거는 검사를 var 까지 모든 프로퍼티에 겁니다. 선언만 하고 끝내 채우지 않은 프로퍼티는 컴파일 단계에서 막힙니다. 다만 채우기 전에 함수를 거쳐 읽는 경우는 막지 못합니다. 그 함정은 KB 13 에서 다룹니다.
init 블록은 초기값과 적힌 순서대로
init { ... } 는 초기화 블록입니다. 주생성자에는 몸통이 없어서, 객체를 만들 때 실행할 문장을 여기에 씁니다. 초기값과 init 블록을 섞은 클래스를 s3/Account.kt 에 두고, main 에서 Account("kim") 을 만들어 label 을 찍게 했습니다.
class Account(owner: String) {
val id = "acc-" + owner
var balance: Int = 0
init {
println("init: " + id)
}
val label = id + ":" + balance
}
$ kotlinc s3/Account.kt -d out3
$ kotlin -cp out3 AccountKt
init: acc-kim
acc-kim:0
owner: String—val이 없으니 2절의 보통 인자입니다. 본문의 초기값 식과init블록에서는 쓸 수 있습니다val id = ...— 타입을 안 적으면 컴파일러가 값에서 타입을 알아냅니다. 여기서는String입니다
init: 줄이 먼저, label 값이 나중입니다. 그 init: 줄에는 한 줄 위에 적은 id 가 이미 acc-kim 으로 들어가 있고, init 블록보다 아래에 적은 label 은 그 뒤에 만들어졌습니다. 본문에 적은 초기값과 init 블록이 위에서 아래로, 적힌 순서대로 돌았습니다.
자바의 필드 초기값과 인스턴스 초기화 블록({ ... })도 적힌 순서대로 돕니다(자바 언어 명세 JLS 12.5).
본문 프로퍼티는 객체 생성이 끝나기 전에 초기값이나 init 블록으로 채워져야 하고, 둘은 적힌 순서대로 실행됩니다. 두 언어를 나란히 돌려 보는 일은 KB 13 에서 합니다.
4. 커스텀 getter·setter 와 field
자바에서 세터에 가공을 넣듯, 코틀린도 프로퍼티의 게터·세터 몸통을 직접 씁니다. 이 절은 몸통을 쓴 프로퍼티가 필드를 갖는지 보고, 자바 버릇이 만드는 무한 재귀를 확인합니다.
class Member(name: String) {
var name: String = name
set(value) {
field = value.trim()
}
val size: Int
get() = name.length
}
set(value) { ... }—name에 대입할 때 불리는 세터 몸통입니다. 프로퍼티 선언 바로 아래에 들여 씁니다.value는 대입하려는 값입니다field— 게터·세터 몸통 안에서만 쓰는 이름입니다. 이 프로퍼티의 값을 실제로 담는 필드, 곧 backing field 를 가리킵니다(Properties)get() = name.length— 읽을 때마다name.length를 계산해 돌려주는 게터입니다.=뒤의 식이 반환값입니다var name: String = name— 프로퍼티name의 초기값으로 같은 이름의 생성자 인자name을 넣습니다
s4/Member.kt 의 main 은 m.name 에 " lee " 를 대입한 뒤 이름과 size 를 찍습니다.
fun main() {
val m = Member("kim")
m.name = " lee "
// ↓ [lee] 3
println("[" + m.name + "] " + m.size)
}
$ kotlinc s4/Member.kt -d out4
$ kotlin -cp out4 MemberKt
$ javap -p out4/Member.class
Compiled from "Member.kt"
public final class Member {
private java.lang.String name;
public Member(java.lang.String);
public final java.lang.String getName();
public final void setName(java.lang.String);
public final int getSize();
}
공백이 잘린 [lee] 가 찍혔습니다. 세터 몸통의 field = value.trim() 이 다듬은 값을 필드에 넣었다는 뜻입니다. 비교할 자바 s4j/MemberJ.java 는 필드 private String name; 하나와 생성자·게터를 0절처럼 쓰고, 세터와 getSize() 를 이렇게 적었습니다.
public void setName(String value) {
this.name = value.trim();
}
public int getSize() {
return name.length();
}
javac 로 컴파일해 javap -p 로 열면 멤버 목록이 코틀린과 한 줄씩 맞습니다. 다른 것은 클래스와 메서드의 final 뿐입니다.
두 세터를 나란히 놓으면 다른 것은 대입의 왼쪽뿐입니다. 자바가 this.name 이라고 적는 자리를 코틀린은 field 라고 적습니다. 오른쪽의 value.trim() 은 같고, 두 쪽 다 공백을 다듬은 값이 필드로 들어갑니다.
컴파일러는 값을 담아 둘 곳이 필요한 프로퍼티에만 필드를 만듭니다. 필요하다고 보는 경우는 둘입니다.
- 게터·세터 중 하나라도 기본 모양이다
- 직접 쓴 몸통이
field를 쓴다
| 프로퍼티 | 게터 | 세터 | 필드 |
|---|---|---|---|
name |
기본 모양 | 몸통을 직접 썼고 field 를 쓴다 |
생긴다 |
size |
몸통을 직접 썼고 field 를 안 쓴다 |
val 이라 없다 |
안 생긴다 |
size 에는 필드가 없습니다. 읽을 때마다 name.length 를 계산해 돌려줄 뿐입니다.
세터가 field 를 안 써도 게터가 기본 모양이면 필드는 생깁니다. 세터 몸통이 println(value) 뿐인 s4set/Member.kt 도 javap -p 에 private java.lang.String name; 이 나옵니다.
세터 안에서 자기 이름에 대입하면
자바 세터는 위의 this.name = value.trim(); 처럼 this.name 에 대입해 필드에 씁니다. 그 버릇대로 field 대신 프로퍼티 이름 name 에 대입한 s4loop/Member.kt 입니다. size 는 뺐습니다. main 은 m.name = "lee" 한 줄로 세터를 부릅니다.
var name: String = name
set(value) {
name = value.trim()
}
kotlinc 는 경고 없이 컴파일했습니다. 실행 결과입니다.
$ kotlinc s4loop/Member.kt -d out4l
$ kotlin -cp out4l MemberKt 2>&1 | grep -m 3 -e Exception -e setName
Exception in thread "main" java.lang.StackOverflowError
at Member.setName(Member.kt:4)
at Member.setName(Member.kt:4)
2>&1 | grep -m 3 -e Exception -e setName— 에러 출력까지 받아,Exception이나setName이 든 줄을 앞에서 세 줄만 고릅니다. 그 아래로도setName줄이 계속 이어집니다
스택에 쌓인 것이 전부 Member.setName 입니다. 세터 안의 name = ... 이 필드 대입이 아니라 세터 호출로 읽혔다는 뜻입니다. 세터가 자기 자신을 계속 부르다가 스택이 넘쳤습니다.
자바의 this.name = value 는 필드에 쓰지만, 코틀린의 name = value 는 세터 몸통 안에서도 세터를 부릅니다. 몸통 안에서 필드에 쓰려면 프로퍼티 이름이 아니라 field 에 대입합니다.
5. 기본 인자로 생성자 오버로딩을 대신하기
자바에서 인자 일부를 생략해 객체를 만들게 하려면 생성자를 여러 개 쓰고 this(...) 로 잇습니다. 이 절은 코틀린이 이 일을 기본 인자(인자에 미리 정해 둔 값)로 하는 모습과, 그 결과를 자바에서 부를 때 생기는 일을 봅니다.
자바 쪽은 이런 모양입니다. 컴파일해 javap -p 로 열면 생성자 ServerJ()·ServerJ(String)·ServerJ(String, int) 세 개가 보입니다.
public class ServerJ {
private final String host;
private final int port;
public ServerJ() {
this("localhost");
}
public ServerJ(String host) {
this(host, 8080);
}
public ServerJ(String host, int port) {
this.host = host;
this.port = port;
}
}
코틀린은 생성자 하나로 씁니다.
class Server(
val host: String = "localhost",
val port: Int = 8080
)
fun main() {
val a = Server()
val b = Server("example.com")
val c = Server(port = 9090)
// ↓ localhost:8080
println(a.host + ":" + a.port)
// ↓ example.com:8080
println(b.host + ":" + b.port)
// ↓ localhost:9090
println(c.host + ":" + c.port)
}
$ kotlinc s5/Server.kt -d out5
$ kotlin -cp out5 ServerKt
$ javap -p out5/Server.class
Compiled from "Server.kt"
public final class Server {
private final java.lang.String host;
private final int port;
public Server(java.lang.String, int);
public Server(java.lang.String, int, int, kotlin.jvm.internal.DefaultConstructorMarker);
public final java.lang.String getHost();
public final int getPort();
public Server();
}
= "localhost"— 인자 뒤의= 값이 기본 인자입니다. 호출에서 그 인자를 빼면 이 값이 들어갑니다Server(port = 9090)— 이름 붙인 인자입니다. 인자 이름을 적으면 앞 인자를 건너뛰고 원하는 인자만 넘길 수 있습니다- 주생성자 괄호는 인자마다 줄을 나눠 써도 됩니다
DefaultConstructorMarker— 소스에 없고 컴파일러가 만든 합성 생성자임을 나타내는 표시용 인자입니다
세 호출이 모두 기본값을 채워 돌았습니다. 인자를 하나도 안 넘긴 Server() 는 localhost:8080, 이름 붙인 인자로 port 만 넘긴 Server(port = 9090) 은 localhost:9090 입니다. 빠진 자리마다 괄호에 적어 둔 기본값이 들어갔습니다.
그런데 javap -p 에는 생성자가 셋입니다. 소스에 적은 것은 주생성자 하나뿐인데, 나머지 둘은 쓴 적도 적은 적도 없습니다.
| 생성자 | 만든 쪽 | 하는 일 |
|---|---|---|
Server(String, int) |
주생성자 | 필드 둘을 채운다 |
Server(String, int, int, DefaultConstructorMarker) |
컴파일러 | 빠진 인자에 기본값을 넣는다 |
Server() |
컴파일러 | 기본값만으로 객체를 만든다 |
기본 인자는 소스에서 보면 호출 쪽이 인자를 빼는 문법이지만, 클래스 파일에서는 빠진 자리를 메우는 생성자가 따로 생기는 일입니다. 셋째 줄의 인자 없는 생성자는 누구를 위한 것인지, 다음 소절에서 봅니다.
자바에서 부르면
자바는 기본 인자를 모릅니다. 이 Server 를 자바에서 new Server("example.com") 으로 부르는 s5j/UseServer.java 를 컴파일했습니다.
$ javac -cp out5 -d jout5 s5j/UseServer.java
s5j/UseServer.java:3: error: no suitable constructor found for Server(String)
Server s = new Server("example.com");
^
constructor Server.Server(String,int) is not applicable
(actual and formal argument lists differ in length)
constructor Server.Server() is not applicable
(actual and formal argument lists differ in length)
1 error
-cp out5— 코틀린이 만든Server.class를 찾을 경로를 알려 줍니다
후보는 Server(String,int) 와 Server() 둘뿐입니다. 합성 생성자는 후보에도 안 나왔습니다. 호출을 new Server() 로 바꾼 s5jnew/UseServer0.java 는 컴파일되고 localhost:8080 을 찍습니다.
자바에서는 인자를 전부 채우거나(Server(String,int)), 기본값이 전부 있을 때 전부 빼는(Server()) 두 가지만 됩니다. 일부만 빼는 호출은 안 됩니다.
그러면 앞 표의 인자 없는 생성자 Server() 는 누가 부를까요. 방금 컴파일된 자바 쪽이 부릅니다. 인자 없는 생성자로 객체를 만드는 라이브러리도 이것을 부릅니다. 공식 문서도 주생성자 인자에 기본값이 전부 있으면 JVM 에서 인자 없는 생성자를 더 만든다고 밝힙니다(Classes).
자바에서 일부만 빼고 부르게 하려면 @JvmOverloads 애노테이션을 씁니다. 주생성자에 애노테이션을 붙일 때는 평소 생략하던 constructor 키워드를 적어야 합니다. 그래서 class Server @JvmOverloads constructor(...) 모양이 됩니다.
그러면 javap -p 에 public Server(java.lang.String); 이 더해집니다. 자바의 ServerJ 와 같은 생성자 셋이 갖춰집니다(Overloads generation).
코틀린은 생성자 여러 개 대신 기본 인자 하나로 생략을 허용합니다. 클래스 파일에는 기본값을 채우는 합성 생성자가 생깁니다. 자바에서 일부만 빼고 부르려면 @JvmOverloads 가 필요합니다.
6. 한 장 요약
class User(val name: String)한 줄은 자바가 손으로 쓰던 필드·생성자·게터를 컴파일러가 대신 쓰게 하는 선언이다. 주생성자 괄호 안에서 필드를 만드는 것은val·var다. 본문 초기값과init은 적힌 순서대로 돈다. 기본 인자는 생성자 오버로딩을 합성 생성자 하나로 바꾼다.
| 하려는 일 | 자바 | 코틀린 | 확인한 방법 |
|---|---|---|---|
| 이름 하나 담는 클래스 | 필드·생성자·게터를 손으로 씀 | class User(val name: String) |
자바 소스 대조 · javap -p |
| 객체 생성 | new User("kim") |
User("kim"). 클래스 파일에는 new User("kim") |
CFR · javac 에러 |
| 생성자 인자를 필드로 | 필드 선언 + this.name = name |
괄호 안에 val 또는 var |
javap -p |
| 필드로 안 남기는 인자 | 보통 생성자 인자 | val·var 없는 인자 |
kotlinc 에러 |
| 초기값 없는 필드 | 0·null 로 시작. final 만 에러 |
초기값·init 둘 다 없으면 val·var 모두 에러 |
kotlinc·javac 에러 |
| 생성할 때 도는 코드 | 인스턴스 초기화 블록 | init 블록. 초기값과 적힌 순서대로 |
실행 |
| 세터에서 필드에 쓰기 | this.name = value |
field = value. name = value 는 재귀 |
실행 · StackOverflowError |
| 계산만 하는 게터 | 필드 없이 getSize() |
get() = .... 필드가 안 생긴다 |
javap -p |
| 인자 생략 | 생성자 오버로딩 + this(...) |
기본 인자 · 이름 붙인 인자 | 실행 · javap -p |
| 자바에서 생략해 부르기 | 해당 없음 | @JvmOverloads constructor |
javac 에러 · javap -p |
관련 항목
주생성자 한 줄이 번역되는 클래스 파일 멤버
필드 · 생성자 · 게터 · 세터 · backing field · 합성 메서드
클래스 선언을 이루는 코틀린 문법 요소
주 생성자 · 부 생성자 · init 블록 · 프로퍼티 · 커스텀 접근자 · val · var · 타입 추론
생성자 오버로딩을 대신하는 코틀린의 수단
기본 인자 · 이름 붙인 인자 · JvmOverloads · DefaultConstructorMarker
주생성자와 맞세워지는 자바의 선언
생성자 오버로딩 · 인스턴스 초기화 블록 · 레코드 클래스 · final 필드
주생성자 클래스에 붙어 멤버를 바꾸는 코틀린 수식어
data class · open · abstract · lateinit
세터에서 field 를 빠뜨리면 나는 오류
StackOverflowError · 무한 재귀 · 호출 스택
클래스 파일을 열어 보는 도구와 표기
javap · CFR · 디컴파일 · 클래스 파일 · 메타데이터