KB 18 함수 — 기본인자와 명명인자가 오버로딩을 대신한다
고친 사람 github-actions[bot]
0. 자바는 메서드 셋, 코틀린은 함수 하나
금액을 15,000 KRW 처럼 찍는 메서드를 생각해 봅니다. 대개는 금액만 넘기고, 가끔 통화 단위를 바꾸고, 더 가끔 쉼표를 뺍니다. 자바에서 이렇게 부르는 방법을 여럿 주려면 같은 이름의 메서드를 인자 개수만 달리해 여러 벌 씁니다. 이것이 오버로딩(overloading)입니다.
public class PriceJ {
static String price(long amount) {
return price(amount, "KRW");
}
static String price(long amount, String unit) {
return price(amount, unit, true);
}
static String price(long amount, String unit, boolean comma) {
String num = comma ? String.format("%,d", amount) : "" + amount;
return num + " " + unit;
}
public static void main(String[] args) {
System.out.println(price(15000));
System.out.println(price(15000, "USD"));
System.out.println(price(15000, "KRW", false));
}
}
일을 하는 것은 마지막 메서드 하나입니다. 앞의 둘은 빠진 인자를 채워 다음 메서드로 넘기기만 합니다.
코틀린은 같은 일을 함수 하나로 씁니다.
fun price(
amount: Long,
unit: String = "KRW",
comma: Boolean = true
): String {
val num = if (comma) "%,d".format(amount) else "$amount"
return "$num $unit"
}
fun main() {
// ↓ 15,000 KRW
println(price(15000))
// ↓ 15,000 USD
println(price(15000, "USD"))
// ↓ 15000 KRW
println(price(15000, comma = false))
}
fun price(...): String— 함수 선언입니다. 인자는이름: 타입순서로 적고, 괄호 뒤 콜론 다음이 반환 타입입니다unit: String = "KRW"— 기본인자(default argument)입니다. 부를 때 이 인자를 빼면=뒤의 값이 들어갑니다comma = false— 명명인자(named argument)입니다. 인자 이름을 적어서, 가운데unit을 건너뛰고comma만 넘겼습니다"%,d".format(amount)— 자바의String.format("%,d", amount)와 같습니다."$amount"는 값을 문자열에 끼우는 문자열 템플릿입니다
Price.kt 로 저장해 코틀린 컴파일러 kotlinc 로 컴파일하고 kotlin 명령으로 돌렸습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션입니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.
$ kotlinc Price.kt -d out
$ kotlin -cp out PriceKt
$ javac -d jout PriceJ.java
$ java -cp jout PriceJ
자바도 같은 세 줄을 찍었습니다. 메서드 셋이 하던 일을 함수 하나가 했습니다. 셋째 호출을 눈여겨볼 만합니다. 자바 쪽은 쉼표만 빼고 싶어도 "KRW" 를 손으로 다시 적었습니다. (long, boolean) 오버로딩을 하나 더 만들면 피할 수 있지만, 조합이 늘 때마다 메서드도 늘어납니다. 코틀린은 comma = false 한 마디로 끝납니다.
이 편은 이 함수 하나를 뜯어 봅니다. 1절이 함수 선언, 2절이 기본인자, 3절이 명명인자, 4절이 자바에서 이 함수를 부를 때입니다. 5절과 6절은 인자 개수를 정하지 않는 vararg 와, 거기에 배열을 펼쳐 넘길 때 생기는 복사를 봅니다.
1. 함수 선언 — 블록 본문과 = 한 줄 본문
코틀린 함수는 본문을 두 가지로 씁니다. 중괄호 안에 문장을 쓰는 블록 본문과, = 뒤에 식 하나를 쓰는 식 본문입니다. 반환 타입을 생략할 수 있느냐가 둘에서 갈립니다.
fun twice(x: Int): Int {
return x * 2
}
fun thrice(x: Int) = x * 3
fun hello(name: String) {
println("hi $name")
}
fun main() {
println(twice(4)) // 8
println(thrice(4)) // 12
hello("kotlin") // hi kotlin
println(hello("k")) // 두 줄 — 아래
}
fun twice(x: Int): Int { ... }— 블록 본문입니다. 자바의static int twice(int x)에 해당합니다. 자바처럼return으로 값을 돌려줍니다fun thrice(x: Int) = x * 3— 식 본문입니다. 중괄호와return없이=뒤의 식이 곧 반환값입니다. 반환 타입을 안 적었는데, 컴파일러가x * 3에서Int를 알아냅니다fun hello(name: String)— 반환 타입이 없는 블록 본문입니다. 자바의void메서드에 해당합니다- 이 세 함수는 클래스 밖, 파일 바로 아래에 있습니다. 이런 함수를 톱레벨(top-level) 함수라고 부릅니다
$ kotlinc Twice.kt -d out
$ kotlin -cp out TwiceKt
...
hi k
kotlin.Unit
앞의 세 줄은 코드 옆에 적은 값이라 ... 로 줄였습니다. 마지막 println(hello("k")) 가 두 줄을 찍었습니다. hello 안에서 hi k 를 찍고, 돌아온 값을 바깥 println 이 kotlin.Unit 으로 찍었습니다.
반환 타입을 안 적은 블록 본문 함수는 Unit 을 돌려줍니다. Unit 은 「돌려줄 값이 없다」를 나타내는 타입인데, 자바의 void 와 달리 값이 하나 있는 진짜 타입입니다. 그래서 println 에 넘길 수 있고 kotlin.Unit 이라고 찍힙니다.
실행 명령에 쓴 TwiceKt 는 소스에 없는 이름입니다. JVM 에서는 모든 메서드가 어느 클래스엔가 속해야 해서, 컴파일러가 파일 이름 + Kt 클래스를 만들고 톱레벨 함수를 그 안의 static 메서드로 넣습니다. 4절에서 자바가 이 이름으로 함수를 부릅니다.
반환 타입을 꼭 적어야 하는 경우
식 본문에서는 반환 타입을 알아서 정해 주는데, 블록 본문에서도 그럴까요. 두 함수를 Bad.kt 에 적었습니다. 하나는 블록 본문에서 반환 타입을 뺐고, 하나는 자기 자신을 부르는 식 본문입니다.
fun twice(x: Int) {
return x * 2
}
fun fact(n: Int) = if (n <= 1) 1 else n * fact(n - 1)
if (...) 1 else ... 는 if 가 값을 돌려주는 식으로 쓰인 것입니다. 자바의 삼항 연산자 ? : 에 해당합니다.
$ kotlinc Bad.kt -d out
Bad.kt:2:12: error: return type mismatch: expected 'Unit', actual 'Int'.
return x * 2
^^^^^
Bad.kt:5:43: error: type checking has run into a recursive problem. Easiest workaround: specify the types of your declarations explicitly.
fun fact(n: Int) = if (n <= 1) 1 else n * fact(n - 1)
^^^^^^^^^^^
두 에러가 규칙 둘을 보여 줍니다. 첫 에러는 블록 본문이 반환 타입을 추론하지 않는다는 뜻입니다. 타입을 안 적으면 Unit 으로 정해지고, 그런데 Int 를 돌려주려 하니 막혔습니다. 두 번째 에러는 재귀 함수에서 납니다. fact 의 타입을 알려면 fact(n - 1) 의 타입을 알아야 해서, 추론이 제자리를 돕니다. 에러 문구대로 타입을 적으면(fun fact(n: Int): Int = ...) 풀립니다.
정리하면, 반환 타입을 생략할 수 있는 것은 식 본문뿐이고 재귀면 그것도 안 됩니다. 식 본문이라도 다른 파일에서 부르는 함수는 반환 타입을 적어 두는 편이 안전합니다. 본문을 고치다 추론된 반환 타입이 슬쩍 바뀌는 일을 막아 주기 때문입니다.
2. 기본인자 — 기본값은 부를 때마다 계산된다
기본값에는 "KRW" 같은 상수만 오는 것이 아니라 함수 호출도 올 수 있습니다. 그러면 궁금한 것이 하나 생깁니다. 그 호출은 언제 한 번 돌아서 값을 정해 두는 걸까요, 아니면 부를 때마다 도는 걸까요.
로그 한 줄에 시각을 붙이는 함수로 봅니다. 시각을 안 넘기면 now() 가 기본값을 대고, now() 는 불릴 때마다 한 줄을 찍습니다.
fun now(): Long {
println("now() 호출")
return System.currentTimeMillis()
}
fun log(msg: String, at: Long = now()) {
println("[$at] $msg")
}
fun main() {
log("a")
log("b")
log("c", 0L)
}
System.currentTimeMillis()— 자바 표준 라이브러리 메서드를 코틀린에서 그대로 부를 수 있습니다. 1970년 1월 1일부터 지난 밀리초입니다0L—Long타입 숫자라는 표시입니다
now() 호출 이 몇 번 찍힐까요. 출력은 아래와 같습니다.
$ kotlinc Now.kt -d out
$ kotlin -cp out NowKt
now() 호출
[1789771454414] a
now() 호출
[1789771454415] b
[0] c
now() 는 두 번 불렸습니다. 시각을 안 넘긴 log("a") 와 log("b") 가 부를 때마다 한 번씩입니다. 시각을 넘긴 log("c", 0L) 에서는 불리지 않았습니다.
기본값은 선언할 때 한 번 계산해 두는 값이 아니라, 인자가 빠진 호출마다 새로 계산하는 식입니다. 그래서 두 시각이 1 밀리초 다릅니다. 빠진 인자가 있을 때만 계산하니, 값을 넘기면 기본값 식은 아예 돌지 않습니다.
그래서 기본값으로 새 객체를 줘도 안전하다
부를 때마다 계산된다는 성질은 기본값으로 변경 가능한 객체를 줄 때 효과가 드러납니다. 목록을 안 넘기면 새 목록을 만들어 거기에 더하는 함수를 두 번 불렀습니다. 기본값 식이 앞의 파라미터를 쓰는 함수도 하나 같이 넣었습니다.
fun add(
x: String,
to: MutableList<String> = mutableListOf()
): MutableList<String> {
to.add(x)
return to
}
fun window(from: Int, to: Int = from + 10) = "$from..$to"
fun main() {
val a = add("x")
val b = add("y")
println(a) // [x]
println(b) // [y]
println(a === b) // false
println(window(5)) // 5..15
}
MutableList<String>— 원소를 더하고 뺄 수 있는 목록 타입입니다.mutableListOf()가 빈 목록을 만듭니다. 자바의new ArrayList<>()에 해당합니다===— 두 변수가 같은 객체를 가리키는지 보는 참조 비교입니다. 자바의==에 해당합니다to: Int = from + 10— 기본값 식에서 앞에 선언한 파라미터from을 썼습니다
$ kotlinc Fresh.kt -d out
$ kotlin -cp out FreshKt
두 번째 호출의 결과가 [x, y] 가 아니라 [y] 입니다. a === b 도 false 라서, 두 호출이 서로 다른 목록을 받았습니다. 부를 때마다 mutableListOf() 가 새로 돌았기 때문입니다. 한 호출에서 넣은 원소가 다음 호출로 새어 나가는 일은 없습니다.
window(5) 는 5..15 를 찍었습니다. 기본값 식은 앞에 선언된 파라미터를 읽을 수 있습니다. 자바라면 window(int from) 오버로딩 안에서 window(from, from + 10) 으로 넘기던 일입니다.
반대쪽도 짚어 둡니다. 부를 때마다 도니, 기본값 식이 무거우면(파일을 읽거나 DB 에 묻는 식) 인자를 빼고 부를 때마다 그 비용을 냅니다. 기본값에는 가벼운 식을 두고, 무거운 값은 부르는 쪽이 한 번 구해서 넘기는 편이 낫습니다.
정리하면, 기본값은 인자가 빠진 호출마다 새로 계산됩니다. 그래서 새 객체를 기본값으로 줘도 호출끼리 섞이지 않고, 앞의 파라미터를 기본값 식에 쓸 수도 있습니다.
3. 명명인자 — 파라미터 이름이 호출부에 박힌다
0절의 자바 호출 price(15000, "KRW", false) 에서 false 는 무엇을 끄는 걸까요. 선언을 열어 보기 전에는 모릅니다. 명명인자는 이 false 옆에 이름을 붙이는 문법입니다. 이 절에서는 명명인자를 섞어 쓰는 규칙과, 이름이 호출부에 박혀서 생기는 일을 봅니다.
fun main() {
// ↓ 15000 KRW
println(price(15000, "KRW", false))
// ↓ 15000 KRW
println(price(15000, comma = false))
// ↓ 15000 KRW
println(price(comma = false, amount = 15000))
// ↓ 15,000 USD
println(price(amount = 15000, "USD"))
}
price 는 0절과 같은 함수라 선언은 생략했습니다. 파일 이름은 Named.kt 입니다.
$ kotlinc Named.kt -d out
$ kotlin -cp out NamedKt
위의 세 줄은 같은 값을 찍었습니다. 둘째 줄은 comma = false 가 무엇을 끄는지 호출만 보고도 읽힙니다. 셋째 줄처럼 이름을 다 붙이면 순서를 바꿔 적어도 됩니다.
넷째 줄은 명명인자 뒤에 이름 없는 "USD" 가 왔는데도 통과했습니다. 이름 붙인 amount 가 선언 순서대로 첫째 칸에 있어서, 뒤따르는 "USD" 를 둘째 파라미터 unit 으로 읽을 수 있기 때문입니다.
섞어 쓰는 규칙과, 이름이 바뀌면 깨지는 호출
그러면 순서가 안 맞게 섞으면 어떻게 될까요. 틀린 호출 둘을 Mix.kt 에 적었습니다. 하나는 unit 을 이름으로 먼저 주고 금액을 이름 없이 뒤에 준 것이고, 하나는 없는 이름 currency 를 쓴 것입니다.
fun main() {
println(price(unit = "USD", 15000))
println(price(15000, currency = "USD"))
}
price 선언은 파일 위쪽에 있고, 에러의 줄 번호는 그것까지 센 번호입니다.
$ kotlinc Mix.kt -d out
Mix.kt:8:33: error: mixing named and positional arguments is not allowed unless the order of the arguments matches the order of the parameters.
println(price(unit = "USD", 15000))
^^^^^
Mix.kt:8:33: error: no value passed for parameter 'amount'.
println(price(unit = "USD", 15000))
^^^^^^
Mix.kt:9:26: error: no parameter with name 'currency' found.
println(price(15000, currency = "USD"))
^^^^^^^^
첫 에러가 섞어 쓰는 규칙을 그대로 말합니다. 이름 있는 인자와 없는 인자를 섞는 것은 인자 순서가 파라미터 순서와 맞을 때만 됩니다. unit 을 첫 칸에 두는 바람에 뒤의 15000 이 어느 파라미터인지 정할 수 없어졌습니다. 안전하게 쓰려면 이름 없는 인자를 앞에, 이름 붙인 인자를 뒤에 둡니다.
둘째 에러가 이 절 제목의 뜻입니다. 선언에서 unit 을 currency 로 바꿨다고 해 봅시다. 그러면 unit = "USD" 로 부르던 곳마다 이 에러가 납니다. 명명인자를 쓰는 순간 파라미터 이름이 호출부에 적히고, 그 이름도 함수 모양의 일부가 됩니다.
한 프로젝트 안이라면 IDE 의 이름 바꾸기 기능이 부르는 쪽까지 같이 고칩니다. 조심할 것은 남이 쓰는 라이브러리입니다. 자바에서는 파라미터 이름을 바꿔도 부르는 쪽이 멀쩡했지만, 코틀린에서는 이름을 바꾸는 것만으로 사용자의 코드가 깨질 수 있습니다.
자바 메서드에는 명명인자를 못 쓴다
거꾸로 코틀린에서 자바 메서드를 부를 때는 명명인자를 못 씁니다. greet(String name) 하나만 든 자바 클래스 Greeter 를 javac 로 컴파일하고, 코틀린에서 Greeter.greet(name = "kotlin") 으로 불렀습니다. kotlinc 는 named arguments are prohibited for non-Kotlin functions. 에러를 냈습니다. 같은 파일의 Greeter.greet("kotlin") 줄에는 에러가 없었습니다. 자바 메서드는 위치로만 부릅니다.
정리하면, 명명인자는 순서가 맞을 때만 이름 없는 인자와 섞이고, 이름을 다 붙이면 순서가 자유롭습니다. 대신 파라미터 이름이 호출부에 적히니, 공개한 함수의 파라미터 이름은 함부로 바꾸지 않습니다.
4. 자바에서 부를 때 — 기본인자가 안 보인다
코틀린 코드를 자바가 부르는 일은 흔합니다. 한 프로젝트에 두 언어가 섞여 있거나, 코틀린으로 만든 라이브러리를 자바 프로젝트가 가져다 쓸 때입니다. 이 절에서는 0절의 price 를 자바에서 불러 봅니다. 기본인자가 자바에서도 통하는지는 자바 쪽 호출이 컴파일되느냐로 가립니다.
생성자의 기본인자는 KB 12 클래스와 주생성자 — 한 줄 클래스가 만드는 것 5절에서 봤습니다. 거기서 자바는 인자를 전부 채우거나 전부 빼는 두 가지로만 부를 수 있었습니다. 함수는 어떤지 봅니다.
price 만 든 Price.kt 를 컴파일하고, 자바 UsePrice.java 에서 두 번 불렀습니다. 톱레벨 함수라서 자바에서는 1절에서 본 PriceKt 클래스의 static 메서드로 부릅니다.
public class UsePrice {
public static void main(String[] args) {
System.out.println(PriceKt.price(15000L, "USD", false));
System.out.println(PriceKt.price(15000L));
}
}
$ kotlinc Price.kt -d out
$ javac -cp out -d jout j/UsePrice.java
j/UsePrice.java:4: error: method price in class PriceKt cannot be applied to given types;
System.out.println(PriceKt.price(15000L));
^
required: long,String,boolean
found: long
reason: actual and formal argument lists differ in length
1 error
-cp out— 코틀린이 만든PriceKt.class를 찾을 경로(classpath)를 javac 에 알려 줍니다
인자를 셋 다 채운 3번째 줄은 통과하고, 금액만 넘긴 4번째 줄이 막혔습니다. required: long,String,boolean 이 자바가 본 price 의 전부입니다. 자바에서는 기본인자가 없는 것과 같습니다. 코틀린에서는 price(15000) 으로 부르던 함수를, 자바에서는 인자를 셋 다 적어야 부를 수 있습니다.
@JvmOverloads — 자바용 오버로딩을 만들어 달라는 표시
자바에서도 빼고 부르게 하려면 함수에 @JvmOverloads 애노테이션을 붙입니다. 애노테이션은 선언에 붙이는 표시이고, 이것은 「기본인자를 뺀 오버로딩을 자바용으로 만들어 달라」는 표시입니다.
@JvmOverloads
fun price(
amount: Long,
unit: String = "KRW",
comma: Boolean = true
): String {
val num = if (comma) "%,d".format(amount) else "$amount"
return "$num $unit"
}
본문은 0절과 같고 달라진 것은 첫 줄뿐입니다. 이번 자바 코드는 인자를 하나, 둘, 셋 넘기는 세 줄입니다.
public class UsePrice {
public static void main(String[] args) {
// ↓ 15,000 KRW
System.out.println(PriceKt.price(15000L));
// ↓ 15,000 USD
System.out.println(PriceKt.price(15000L, "USD"));
// ↓ 15000 USD
System.out.println(PriceKt.price(15000L, "USD", false));
}
}
$ kotlinc Price.kt -d out
$ javac -cp out -d jout j/UsePrice.java
$ kotlin -cp out:jout UsePrice
세 줄 다 컴파일되고 돌았습니다. -cp out:jout 은 코틀린 결과물과 자바 결과물 두 폴더를 다 경로에 넣은 것입니다. 그러면 0절의 셋째 호출처럼 가운데를 건너뛰는 것도 될까요. 금액과 false 만 넘기는 Skip.java 를 같은 방법으로 컴파일했습니다.
public class Skip {
public static void main(String[] args) {
System.out.println(PriceKt.price(15000L, false));
}
}
$ javac -cp out -d jout2 skip/Skip.java
skip/Skip.java:3: error: incompatible types: boolean cannot be converted to String
System.out.println(PriceKt.price(15000L, false));
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
막혔습니다. 에러가 인자 둘짜리 price 의 둘째 칸이 String 이라고 말합니다. @JvmOverloads 는 기본값이 있는 인자를 뒤에서부터 하나씩 뺀 오버로딩만 만들기 때문입니다(Overloads generation). 이 함수라면 (long), (long, String), (long, String, boolean) 셋입니다.
그래서 기본인자의 순서가 자바 쪽 쓰임새를 정합니다. 자바에서 자주 바꿀 인자를 앞에, 거의 안 바꿀 인자를 뒤에 두면 만들어지는 오버로딩이 쓸모 있어집니다.
정리하면, 자바는 기본인자를 모르고 코틀린 함수의 인자 전부를 요구합니다. 자바가 부를 함수에는 @JvmOverloads 를 붙이고, 이때 자바는 뒤쪽 인자를 빼는 호출만 할 수 있습니다. 코틀린끼리만 부르는 함수에는 붙일 까닭이 없습니다.
5. vararg 와 스프레드 *
listOf(1, 2, 3) 처럼 인자 개수를 정하지 않는 함수는 어떻게 만들까요. 자바의 int... xs 에 해당하는 것이 코틀린의 vararg 입니다. 이 절에서는 정수를 몇 개든 받아 더하는 함수로, 낱개로 넘기는 호출과 배열을 펼쳐 넘기는 호출을 봅니다.
fun sum(vararg xs: Int): Int {
var total = 0
for (x in xs) total += x
return total
}
fun main() {
val arr = intArrayOf(1, 2, 3)
val list = listOf(4, 5)
println(sum()) // 0
println(sum(1, 2, 3)) // 6
println(sum(*arr)) // 6
println(sum(10, *arr, 20)) // 36
println(sum(*list.toIntArray())) // 9
}
vararg xs: Int— 인자를 몇 개든 받습니다. 함수 안에서xs는 정수 배열 타입IntArray입니다. 자바의int[]에 해당합니다intArrayOf(1, 2, 3)—IntArray를 만듭니다.listOf(4, 5)는 바꿀 수 없는 목록을 만듭니다*arr— 스프레드(spread) 연산자입니다. 배열 앞에*를 붙이면 원소를 하나씩 풀어서 넘깁니다list.toIntArray()— 목록을IntArray로 바꿉니다. 목록은 바로 펼칠 수 없어서 배열로 바꾼 뒤*를 붙였습니다
$ kotlinc Sum.kt -d out
$ kotlin -cp out SumKt
인자가 없으면 빈 배열이라 0 이고, 낱개 셋은 6 입니다. *arr 도 6 이라 배열의 원소가 낱개로 넘어갔습니다. sum(10, *arr, 20) 이 36 인 것을 보면, 펼친 배열 앞뒤에 낱개를 섞을 수도 있습니다.
* 를 빼면, 그리고 vararg 뒤의 파라미터
자바는 int... 메서드에 배열을 그냥 넘겨도 됩니다. 코틀린에서 * 를 빼면 어떻게 될까요. vararg 뒤에 파라미터가 하나 더 있는 함수도 같이 봅니다.
fun sum(vararg xs: Int): Int = xs.sum()
fun tag(vararg names: String, sep: String) =
names.joinToString(sep)
fun main() {
val arr = intArrayOf(1, 2, 3)
println(sum(arr))
println(tag("a", "b", sep = "/"))
println(tag("a", "b", "/"))
}
xs.sum()— 배열의 원소를 다 더하는 표준 라이브러리 함수입니다names.joinToString(sep)— 원소를sep로 이어 붙인 문자열을 만듭니다
$ kotlinc NoSpread.kt -d out
NoSpread.kt:8:17: error: argument type mismatch: actual type is 'IntArray', but 'Int' was expected.
println(sum(arr))
^^^
NoSpread.kt:10:27: error: no value passed for parameter 'sep'.
println(tag("a", "b", "/"))
^^^^
에러가 두 개입니다. 첫째는 * 없이 넘긴 배열을 원소 하나로 봤다는 뜻입니다. Int 가 와야 할 인자로 IntArray 가 왔으니 타입이 안 맞습니다. 코틀린에서는 배열을 펼치려면 반드시 * 를 붙입니다.
둘째는 vararg 뒤의 파라미터 이야기입니다. "a", "b", "/" 셋이 전부 names 로 들어가 sep 에 줄 값이 남지 않았습니다. vararg 뒤에 오는 파라미터는 명명인자로만 넘길 수 있고, 그래서 가운데 줄 sep = "/" 은 통과했습니다. 한 함수에 vararg 는 하나만 둘 수 있습니다(Functions).
정리하면, vararg 파라미터는 함수 안에서 배열이고, 이미 있는 배열을 넘길 때는 * 로 펼칩니다. vararg 는 되도록 마지막 파라미터로 두고, 뒤에 무언가 둔다면 명명인자로 받습니다.
6. 성능 — * 는 배열을 복사한다
5절에서 *arr 은 「원소를 풀어서 넘긴다」고 했습니다. 그런데 함수 안에서 xs 는 배열이었습니다. 그러면 함수가 받은 배열은 arr 자체일까요, 새 배열일까요. 받은 배열을 그대로 돌려주는 함수로 가려 봤습니다.
fun keep(vararg xs: Int): IntArray = xs
fun keepArr(xs: IntArray): IntArray = xs
fun main() {
val arr = intArrayOf(1, 2, 3)
val got = keep(*arr)
arr[0] = 100
println(got[0]) // 1
println(got === arr) // false
println(keepArr(arr) === arr) // true
}
keep은vararg로 받고,keepArr는IntArray파라미터로 받습니다. 둘 다 받은 배열을 돌려주기만 합니다arr[0] = 100—keep을 부른 뒤에 원본 배열의 첫 원소를 바꿨습니다
$ kotlinc Copy.kt -d out
$ kotlin -cp out CopyKt
원본을 100 으로 바꿨는데 got[0] 은 1 입니다. got === arr 도 false 라서 둘은 다른 배열입니다. *arr 로 넘기면 arr 을 복사한 새 배열이 함수로 들어갑니다. 반면 IntArray 파라미터로 받은 keepArr 는 true, 곧 원본 배열 그 자체를 받았습니다.
같은 일을 자바의 int... 로 해 봤습니다.
public class CopyJ {
static int[] keep(int... xs) {
return xs;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3};
int[] got = keep(arr);
arr[0] = 100;
// ↓ 100
System.out.println(got[0]);
// ↓ true
System.out.println(got == arr);
}
}
$ javac -d jout CopyJ.java
$ java -cp jout CopyJ
자바는 복사하지 않았습니다. int... 에 넘긴 배열 자체가 함수에 들어가서, 원본을 바꾸자 got[0] 도 100 이 됐습니다.
복사는 함수가 아니라 부르는 쪽의 * 가 합니다. 자바 코드에서 코틀린 keep 에 배열을 넘기면(CopyKt.keep(arr) == arr) true 가 찍혔습니다. 자바 호출에는 * 가 없으니 복사도 없습니다.
언제 신경 쓰나
복사는 배열 길이만큼 원소를 옮기는 일이고, * 를 붙인 호출마다 한 번씩 일어납니다. sum(*arr) 처럼 원소 몇 개짜리 배열을 넘기는 대부분의 코드에서는 신경 쓸 필요가 없습니다.
문제가 되는 것은 큰 배열을 반복해서 넘길 때입니다. 원소가 수십만 개인 배열을 반복문 안에서 * 로 넘기면 부를 때마다 그만큼 새 배열을 만들고 채웁니다. 이런 함수는 vararg 대신 keepArr 처럼 IntArray 파라미터로 받으면 복사 없이 원본을 넘깁니다. 이 편에서는 복사 비용을 시간으로 재지 않았습니다.
복사가 이로운 쪽도 있습니다. 함수가 받은 배열을 필드에 저장해 두는 경우, * 로 받은 코틀린 쪽은 부르는 쪽이 나중에 원본을 고쳐도 영향이 없습니다. 다만 자바에서 부르면 복사가 없으니 이 보호는 사라집니다. 받은 배열을 오래 쥐고 있을 함수라면 함수 안에서 xs.copyOf() 로 직접 복사해 두는 편이 두 언어 모두에서 안전합니다.
정리하면, * 로 펼친 배열은 복사돼 들어갑니다. 작은 배열이면 무시해도 되고, 큰 배열을 자주 넘기는 함수라면 vararg 대신 배열 파라미터로 받습니다.
7. 한 장 요약
코틀린은 기본인자와 명명인자로, 자바가 오버로딩 여러 벌로 하던 일을 함수 하나로 한다. 기본값은 인자가 빠진 호출마다 새로 계산된다. 명명인자를 쓰면 파라미터 이름이 호출부에 적힌다. 자바는 기본인자를 모르니
@JvmOverloads로 오버로딩을 만들어 주고, 그때도 뒤쪽 인자만 뺄 수 있다.vararg에 배열을*로 펼쳐 넘기면 복사가 일어난다.
| 하는 일 | 자바 | 코틀린 | 확인한 방법 |
|---|---|---|---|
| 인자 일부를 빼고 부르기 | 오버로딩 여러 벌 | 기본인자 unit: String = "KRW" |
양쪽 실행 |
| 가운데 인자만 바꾸기 | 앞 인자를 손으로 다시 적거나 오버로딩 추가 | 명명인자 comma = false |
양쪽 실행 |
| 식 하나짜리 함수 | 해당 없음 | fun thrice(x: Int) = x * 3. 반환 타입 추론, 재귀면 적어야 함 |
실행 · kotlinc 에러 |
| 반환값 없음 | void |
Unit. 값이 있는 타입이라 kotlin.Unit 으로 찍힘 |
실행 |
| 기본값 계산 시점 | 해당 없음 | 인자가 빠진 호출마다. 새 객체를 줘도 호출끼리 안 섞임 | 실행 · === |
| 이름과 위치 섞기 | 해당 없음 | 순서가 맞을 때만. 없는 이름은 에러 | kotlinc 에러 |
| 자바 메서드에 명명인자 | 해당 없음 | 에러 | kotlinc 에러 |
| 자바에서 기본인자 함수 부르기 | 인자 전부 요구 | @JvmOverloads 로 뒤에서부터 뺀 오버로딩 생성 |
javac 에러 · 실행 |
| 가변 인자 | int... xs. 배열을 그냥 넘김 |
vararg xs: Int. 배열은 *arr 로 펼침 |
실행 · kotlinc 에러 |
| 배열을 펼쳐 넘길 때 | 복사 없음 | 복사됨. 큰 배열을 반복해 넘기면 배열 파라미터로 | 실행 · === |
관련 항목
기본인자와 명명인자가 대신하는 자바의 수단
오버로딩 · 메서드 시그니처 · 생성자 오버로딩 · 빌더 패턴 · 점층적 생성자 패턴
코틀린 함수 선언을 이루는 문법 요소
기본 인자 · 이름 붙인 인자 · 식 본문 · 블록 본문 · 반환 타입 추론 · Unit · 톱레벨 함수 · 타입 추론
코틀린 함수를 자바에서 부르게 하는 상호 운용 수단
@JvmOverloads · 애노테이션 · 자바 상호 운용성 · static 메서드 · 클래스패스 · 하위 호환
가변 인자 호출을 이루는 문법과 배열 연산
vararg · 가변 인자 · 스프레드 연산자 · 배열 · IntArray · 배열 복사 · 방어적 복사
이 편의 코드를 이루는 그 밖의 코틀린 문법 요소
문자열 템플릿 · if 식 · 참조 비교 · MutableList · 재귀 함수