Kotlin の関数は、fun 関数名(引数名: 型): 戻り値の型 { 本体 } の形で書きます。 本体が式1つなら = でつないで波かっこと return を省け、引数には既定値(デフォルト引数)を付けられ、呼び出すときは引数名を指定(名前付き引数)できます。

この記事では、fun の書き方を7種類の早見表にまとめ、すべてのコードに実行結果を付けました。さらに、デフォルト引数と名前付き引数を混ぜるときの順番のルール、vararg に配列を渡すスプレッド演算子、ブロック本体で戻り値の型を省いたときの動きなど、最初に引っかかりやすい点を実際のコンパイルエラー文つきで再現しています。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行して得たものです。

fun の書き方の早見表

種類 書き方 呼び出し 結果
ブロック本体 fun add(a: Int, b: Int): Int { return a + b } add(2, 3) 5
単一式関数 fun double(x: Int) = x * 2 double(21) 42
Unit(返す値なし) fun greet(name: String) { println(...) } greet("田中") 表示だけ行い、戻り値は kotlin.Unit
Nothing(戻らない) fun stop(message: String): Nothing { throw ... } name ?: stop("...") 例外を投げて終わる
ローカル関数 関数の中に fun format(p: Int) = ... 外側の関数の中だけ 1,200円 / 35,000円
infix(中置) infix fun Int.times(text: String) = ... 3 times "ab" ababab
tailrec(末尾再帰) tailrec fun sumTo(n: Long, acc: Long = 0): Long = ... sumTo(1_000_000) 500000500000

引数まわりの書き方は次の4つです。

やりたいこと 宣言 呼び出し 覚えておく点
既定値を持たせる count: Int = 1 order("ノート") 省略した引数に既定値が入る
名前で渡す (宣言は通常どおり) order("ノート", gift = true) 途中の引数を飛ばしたら、それ以降は名前を付ける
個数を決めずに受け取る vararg nums: Int total(1, 2, 3) 関数の中では配列(IntArray や Array)として扱う
配列を vararg に渡す (同上) total(*arr) 配列の前に *(スプレッド演算子)を付ける

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin関数の書き方 fun・デフォルト引数・名前付き引数・vararg」

まず動かす:7種類の fun を1本のコードで

次のコードを Basic.kt という名前で保存します。早見表の7種類がすべて入っています。

// 1. ブロック本体:戻り値型を書き、return で返す
fun add(a: Int, b: Int): Int {
    return a + b
}

// 2. 単一式関数:= の右に式を1つ。戻り値型は推論される
fun double(x: Int) = x * 2

// 3. Unit:返す値がない関数。「: Unit」は省略できる
fun greet(name: String) {
    println("こんにちは、${name}さん")
}

// 4. Nothing:必ず例外を投げ、呼び出し元に戻らない関数
fun stop(message: String): Nothing {
    throw IllegalStateException(message)
}

// 5. ローカル関数:関数の中だけで使う関数。外側の変数を読める
fun priceLabel(prices: List<Int>): String {
    val unit = "円"
    fun format(p: Int) = "%,d%s".format(p, unit)
    return prices.joinToString(" / ") { format(it) }
}

// 6. infix:引数1つのメンバー関数・拡張関数を「a 関数名 b」で呼べる
infix fun Int.times(text: String) = text.repeat(this)

// 7. tailrec:末尾再帰をループに変換する
tailrec fun sumTo(n: Long, acc: Long = 0): Long =
    if (n == 0L) acc else sumTo(n - 1, acc + n)

fun main() {
    println(add(2, 3))
    println(double(21))
    val r = greet("田中")
    println(r)
    println(priceLabel(listOf(1200, 35000)))
    println(3 times "ab")
    println(sumTo(1_000_000))
    val name: String? = null
    val n = name ?: stop("name が null です")
    println(n)
}

コンパイルして実行します。-include-runtime は Kotlin の標準ライブラリを jar に含める指定、-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 にそろえる指定です。

kotlinc Basic.kt -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -jar basic.jar
5
42
こんにちは、田中さん
kotlin.Unit
1,200円 / 35,000円
ababab
500000500000
Exception in thread "main" java.lang.IllegalStateException: name が null です
    at BasicKt.stop(Basic.kt:16)
    at BasicKt.main(Basic.kt:42)
    at BasicKt.main(Basic.kt)

読み取れることは3つあります。

  • greet の戻り値を変数に入れて表示すると kotlin.Unit と出ます。返す値がない関数も、Unit という値を1つ返しています。
  • sumTo(1_000_000) は100万回の再帰ですが、tailrec を付けているので最後まで計算できます。
  • 最後の4行は stop が例外を投げた結果です。name ?: stop(...) の右側が実行され、その後ろの println(n) には進みません。

ここから、それぞれの書き方を順に見ていきます。

ブロック本体と単一式関数:戻り値の型を書くとき・省けるとき

本体の形 戻り値の型 return
ブロック本体 { ... } で値を返す 必ず書く 必要
ブロック本体 { ... } で値を返さない 省略できる(Unit になる) 不要
単一式 = 式 省略できる(式から推論される) 書かない

Kotlin の公式ドキュメントも、ブロック本体の関数は戻り値の型を必ず明示し、例外は Unit を返すときだけ、と説明しています。

ブロック本体で戻り値の型を省くと Unit になる

Java や他の言語に慣れていると、戻り値の型を書き忘れることがあります。次のコードはコンパイルエラーになります。

// E5.kt
fun area(w: Int, h: Int) {
    return w * h
}

fun main() {
    val a: Int = area(3, 4)
}
E5.kt:3:12: error: return type mismatch: expected 'Unit', actual 'Int'.
    return w * h
           ^^^^^
E5.kt:7:16: error: initializer type mismatch: expected 'Int', actual 'Unit'.
    val a: Int = area(3, 4)
               ^

型を書いていないブロック本体は Unit を返す関数として扱われます。そのため「Unit のはずなのに Int を返している」と「Int の変数に Unit を入れようとしている」の2つが報告されます。fun area(w: Int, h: Int): Int { と型を足せば解決します。

return を書かなかった場合はコンパイルが通ってしまうので、さらに気づきにくくなります。次のコードで確かめます。

// = と { } を両方書くと「ラムダを返す関数」になる
fun hello() = { println("hello") }

// ブロック本体で戻り値型を省くと Unit。最後の式は返らない
fun area(w: Int, h: Int) {
    w * h
}

fun explicitUnit(): Unit {
    return Unit
}

fun main() {
    hello()                 // 何も表示されない(ラムダを作って捨てている)
    hello()()               // ラムダを呼ぶと表示される
    println(area(3, 4))
    println(explicitUnit())
    val f: () -> Unit = ::explicitUnit   // 関数型では Unit を省略できない
    println(f())
}
hello
kotlin.Unit
kotlin.Unit
kotlin.Unit
  • area(3, 4) は 12 ではなく kotlin.Unit です。ブロックの最後に w * h と書いても、ブロック本体では return を書かないかぎり値は返りません。
  • fun hello() = { ... } は「ラムダ(関数の値)を返す関数」です。hello() だけでは何も表示されず、hello()() と呼んではじめて中身が動きます。= と { } はどちらか一方だけを書きます。
書いたもの 意味
fun f() { println("hi") } 表示する関数(Unit を返す)
fun f() = println("hi") 同じく表示する関数(単一式)
fun f() = { println("hi") } 「表示するラムダ」を返す関数

単一式関数で型を書いたほうがよい場面

単一式関数は型を省けますが、自分自身を呼ぶ再帰関数では推論できません。

// E6.kt
fun fact(n: Int) = if (n <= 1) 1 else n * fact(n - 1)

fun main() {
    println(fact(5))
}
E6.kt:2: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)
                                          ^^^^^^^^^^^

メッセージのとおり、fun fact(n: Int): Int = ... と型を書けばコンパイルできます。ほかにも、公開する関数(ライブラリの API など)は、本体を書き換えたときに戻り値の型が意図せず変わらないよう、型を明示しておくと安心です。

Unit:返す値がない関数の戻り値

Unit は「値が1つだけある型」です。Java の void に近い役割ですが、Unit は実際の型なので、変数に入れたりジェネリクスの型引数に使ったりできます。

書き方 説明
fun log(msg: String) { ... } いちばん普通の書き方。: Unit は省略
fun log(msg: String): Unit { ... } 省略しない書き方。意味は同じ
return または return Unit どちらも書ける。書かなくてもよい
val f: () -> Unit = ::log のような関数型 関数型では Unit を省略できない

先ほどの UnitPitfall.kt の explicitUnit() と f() が、どちらも kotlin.Unit を表示していたのがこの動きです。

デフォルト引数:既定値を持たせる

引数の型の後ろに = 値 を書くと、呼び出すときにその引数を省略できます。

fun order(item: String, count: Int = 1, gift: Boolean = false, note: String = "なし") {
    println("item=$item count=$count gift=$gift note=$note")
}

fun main() {
    order("ノート")                              // 既定値をすべて使う
    order("ノート", 3)                           // 位置で2つ目まで
    order("ノート", gift = true)                 // 途中を飛ばすときは名前を付ける
    order(note = "急ぎ", item = "ペン")          // 名前付きなら順番は自由
    order("ペン", count = 2, true)               // 名前付きでも位置が合っていれば続けて位置で書ける
    order(item = "ペン", 5, gift = true, "包装") // 位置どおりなら混在できる
}
item=ノート count=1 gift=false note=なし
item=ノート count=3 gift=false note=なし
item=ノート count=1 gift=true note=なし
item=ペン count=1 gift=false note=急ぎ
item=ペン count=2 gift=true note=なし
item=ペン count=5 gift=true note=包装

既定値には式が書ける(呼ぶたびに評価される)

既定値は固定の値だけでなく、関数呼び出しや、前にある引数を使った式も書けます。式は関数を呼ぶたびに評価されます。

var counter = 0
fun nextId(): Int { counter++; return counter }

// 既定値には式が書ける。前の引数も使える。呼ぶたびに評価される
fun register(name: String, id: Int = nextId(), label: String = "$name#$id") {
    println(label)
}

fun main() {
    register("A")
    register("B")
    register("C", id = 100)
    register("D")
}
A#1
B#2
C#100
D#3

register("C", id = 100) のときは nextId() が呼ばれないので、次の D は 3 番になります。既定値の式は、その引数が省略されたときだけ動きます。

既定値のある引数を前に置いたとき

既定値のある引数を、既定値のない引数より前に置くこともできます。その場合、既定値を使うには後ろの引数を名前付きで渡します。

fun greeting(userId: Int = 0, message: String) {
    println("userId=$userId message=$message")
}

// 最後の引数がラムダなら、既定値を飛ばして波かっこの外に書ける
fun retry(times: Int = 3, action: (Int) -> Unit) {
    for (i in 1..times) action(i)
}

fun main() {
    greeting(message = "Hello!")
    greeting(7, "Hi!")
    retry { println("try $it") }
    retry(1) { println("once $it") }
}
userId=0 message=Hello!
userId=7 message=Hi!
try 1
try 2
try 3
once 1

greeting("Hello!") と位置だけで書くと、"Hello!" は1つ目の userId に渡そうとしたことになり、次のエラーになります。

// E2.kt
fun greeting(userId: Int = 0, message: String) {}

fun main() {
    greeting("Hello!")
}
E2.kt:5:5: error: no value passed for parameter 'message'.
    greeting("Hello!")
    ^^^^^^^^
E2.kt:5:14: error: argument type mismatch: actual type is 'String', but 'Int' was expected.
    greeting("Hello!")
             ^^^^^^^^

例外は最後の引数がラムダの場合です。retry { ... } のように波かっこを丸かっこの外に書くと、それは必ず最後の引数に対応するので、前にある times の既定値がそのまま使われます。

設計の目安としては、必ず渡す引数を前、既定値のある引数を後ろに並べると、位置だけでも名前付きでも呼びやすくなります。

名前付き引数:混ぜて書くときのルール

呼び出すときに 引数名 = 値 と書くのが名前付き引数です。true や数値が並ぶ呼び出しでも、何を渡しているのかが読み取れるようになります。

書き方 結果 理由
order("ノート", 3) 通る すべて位置どおり
order("ノート", gift = true) 通る count を飛ばし、以降は名前付き
order(note = "急ぎ", item = "ペン") 通る すべて名前付きなら順番は自由
order("ペン", count = 2, true) 通る count が本来の位置にあるので、続きを位置で書ける
order(item = "ペン", 5, gift = true, "包装") 通る 名前付きの引数がすべて本来の位置にある
order("ペン", gift = true, 2) エラー gift が本来の位置(3番目)ではなく2番目にあり、その後ろに位置引数がある

最後の行を実際にコンパイルすると、次のメッセージが出ます。

// E1.kt
fun order(item: String, count: Int = 1, gift: Boolean = false, note: String = "なし") {}

fun main() {
    order("ペン", gift = true, 2)
}
E1.kt:5:30: error: mixing named and positional arguments is not allowed unless the order of the arguments matches the order of the parameters.
    order("ペン", gift = true, 2)
                             ^

メッセージは「名前付き引数と位置引数は、引数の並びが宣言の順番と一致しているときだけ混ぜられる」という意味です。迷ったときは、「引数を1つでも飛ばしたら、そこから後ろはすべて名前を付ける」と覚えておけば、このエラーにはなりません。

名前付き引数に関するほかのエラー文もまとめて確認しました。order は fun order(item: String, count: Int = 1) と宣言しています。

書いた呼び出し エラー文 直し方
order("ペン", count = 2, count = 3) argument already passed for this parameter. 同じ引数を2回渡さない
order(name = "ペン") no parameter with name 'name' found. 宣言にある引数名(item)を使う
order() no value passed for parameter 'item'. 既定値のない引数は必ず渡す
order("ペン", 1, 2) too many arguments for 'fun order(item: String, count: Int = ...): Unit'. 引数の数を宣言に合わせる

なお、関数をオーバーライドするときは、子クラス側に既定値を書きません。親の既定値がそのまま使われます。

open class Shape {
    open fun draw(width: Int = 10, height: Int = 5) = println("Shape ${width}x$height")
}

class Rect : Shape() {
    // override 側には既定値を書かない。親の既定値が使われる
    override fun draw(width: Int, height: Int) = println("Rect ${width}x$height")
}

fun main() {
    Rect().draw()
    Rect().draw(height = 2)
}
Rect 10x5
Rect 10x2

子クラス側に width: Int = 20 と書くと、an overriding function is not allowed to specify default values for its parameters. というエラーになります。

vararg:引数の個数を決めずに受け取る

引数に vararg を付けると、0個以上の値をカンマ区切りで渡せるようになります。関数の中では配列として扱います。

fun total(vararg nums: Int): Int {
    println("型=${nums::class.simpleName} 個数=${nums.size}")
    return nums.sum()
}

fun tags(vararg names: String) = names.joinToString(",", "[", "]")

// vararg を先頭に置いた場合、後ろの引数は名前付きで渡す
fun log(vararg parts: String, sep: String = " ") = parts.joinToString(sep)

fun <T> asList(vararg ts: T): List<T> = ts.toList()

fun main() {
    println(total())
    println(total(1, 2, 3))

    val arr = intArrayOf(10, 20, 30)
    println(total(*arr))                 // IntArray はそのまま * で展開
    println(total(1, *arr, 99))          // 前後に値を足せる

    val list = listOf("kotlin", "jvm")
    println(tags(*list.toTypedArray()))  // List は配列に変換してから *

    println(log("a", "b", "c"))
    println(log("a", "b", "c", sep = "-"))

    val a = intArrayOf(1, 2, 3)
    println(asList(-1, 0, *a.toTypedArray(), 4))   // ジェネリクスの vararg は Array<T>
    println(asList(a).size)                        // * なしだと IntArray 1個のリスト
}
型=IntArray 個数=0
0
型=IntArray 個数=3
6
型=IntArray 個数=3
60
型=IntArray 個数=5
160
[kotlin,jvm]
a b c
a-b-c
[-1, 0, 1, 2, 3, 4]
1

vararg nums: Int の実体は IntArray で、引数を1つも渡さなければ個数 0 の配列になります。vararg names: String なら Array<String> です。

配列を渡すときはスプレッド演算子を付ける

すでに配列を持っているときに、そのまま渡すとエラーになります。

// E4.kt
fun total(vararg nums: Int) = nums.sum()
fun tags(vararg names: String) = names.joinToString()

fun main() {
    val arr = intArrayOf(10, 20, 30)
    println(total(arr))
    val list = listOf("kotlin", "jvm")
    println(tags(*list))
}
E4.kt:7:19: error: argument type mismatch: actual type is 'IntArray', but 'Int' was expected.
    println(total(arr))
                  ^^^
E4.kt:9:19: error: argument type mismatch: actual type is 'List<String>', but 'Array<out String>' was expected.
    println(tags(*list))
                  ^^^^

1つ目は「Int を1個ずつ受け取る場所に IntArray を渡した」、2つ目は「* で展開できるのは配列で、List は展開できない」という意味です。直し方は次のとおりです。

持っているもの 渡し方 説明
IntArray(vararg nums: Int へ) total(*arr) 配列の前に * を付けて展開する
List<String>(vararg names: String へ) tags(*list.toTypedArray()) List を配列に変換してから *
IntArray(ジェネリクスの vararg ts: T へ) asList(*a.toTypedArray()) Array<Int> に変換してから *
配列+追加の値 total(1, *arr, 99) 展開の前後に値を並べられる

3行目は見落としやすい点です。ジェネリクスの vararg は Array<T> を受け取るため、プリミティブ型の配列である IntArray をそのまま *a で渡すと、次のエラーになります。

E10.kt:8:35: error: argument type mismatch: actual type is 'IntArray', but 'Array<out Int>' was expected.
    val list: List<Int> = asList(*a)
                                  ^

また、asList(a) のように * を付けずに渡すとコンパイルは通りますが、「IntArray が1個入ったリスト」になります(上の実行結果の最後の行で、size が 1 になっています)。

vararg の後ろの引数は名前付きで渡す

vararg は最後の引数に置くのが基本です。途中や先頭に置いた場合、その後ろの引数は名前付きで渡します。位置だけで書くと、すべて vararg 側に取り込まれるためです。

// E3.kt
fun join(vararg parts: String, sep: String) = parts.joinToString(sep)

fun main() {
    println(join("a", "b", "-"))
}
E3.kt:5:13: error: no value passed for parameter 'sep'.
    println(join("a", "b", "-"))
            ^^^^

join("a", "b", sep = "-") と書けば通ります。Vararg.kt の log のように後ろの引数に既定値を付けておくと、log("a", "b", "c") と log("a", "b", "c", sep = "-") の両方で呼べます。

vararg を付けられる引数は、1つの関数につき1つだけです。2つ書くと multiple vararg parameters are prohibited. というエラーになります。

Nothing:呼び出し元に戻らない関数

戻り値の型 Nothing は、「この関数は必ず例外を投げるなどして、呼び出し元に戻らない」ことを表します。

fun fail(message: String): Nothing = throw IllegalArgumentException(message)

fun parsePort(text: String): Int {
    // toIntOrNull() が null のとき fail() に進む。fail は戻らないので port は Int になる
    val port: Int = text.toIntOrNull() ?: fail("数値ではありません: $text")
    return port
}

fun notYet(): String = TODO("あとで書く")

fun main() {
    println(parsePort("8080"))
    try {
        parsePort("abc")
    } catch (e: IllegalArgumentException) {
        println("捕まえた: ${e.message}")
    }
    try {
        notYet()
    } catch (e: NotImplementedError) {
        println(e.message)
    }
}
8080
捕まえた: 数値ではありません: abc
An operation is not implemented: あとで書く

text.toIntOrNull() ?: fail(...) の右側が Nothing なので、コンパイラは「ここを通過できたなら左側は null ではない」と判断し、port を Int として扱えます。標準ライブラリの TODO() も Nothing を返す関数で、まだ書いていない本体の仮置きに使えます。

型 意味 関数が終わったあと
Unit 返す値がない 呼び出し元の次の行へ進む
Nothing そもそも戻らない 次の行へは進まない

ローカル関数:関数の中だけで使う関数

関数の中にも fun を書けます。外側の関数の変数をそのまま読み書きできるので、その関数の中でだけ繰り返す処理をまとめるのに向いています。

fun validate(name: String, email: String): List<String> {
    val errors = mutableListOf<String>()

    // ローカル関数:外側の errors に直接追加できる
    fun requireNotBlank(label: String, value: String) {
        if (value.isBlank()) errors += "$label は必須です"
    }

    requireNotBlank("名前", name)
    requireNotBlank("メール", email)
    return errors
}

fun main() {
    println(validate("田中", ""))
    println(validate("", " "))
    println(validate("田中", "t@example.com"))
}
[メール は必須です]
[名前 は必須です, メール は必須です]
[]

requireNotBlank は validate の外からは見えません。ファイル全体やクラス全体から呼ぶ必要のない小さな処理は、ローカル関数にしておくと名前が外に出ず、読む範囲が狭くなります。

infix:a 関数名 b の形で呼ぶ

infix を付けた関数は、ドットとかっこを省いて a 関数名 b の形で呼べます。条件は次の3つです。

  • クラスのメンバー関数か、拡張関数である
  • 引数がちょうど1つである
  • その引数は vararg ではなく、既定値も持たない
data class Version(val major: Int, val minor: Int) {
    infix fun isNewerThan(other: Version) =
        major > other.major || (major == other.major && minor > other.minor)
}

infix fun String.joinWith(other: String) = "$this-$other"

fun main() {
    val a = Version(2, 4)
    val b = Version(2, 1)
    println(a isNewerThan b)        // 中置で呼ぶ
    println(a.isNewerThan(b))       // 通常の呼び方も同じ
    println("a" joinWith "b" joinWith "c")
    println(1 to "one")             // 標準ライブラリの to も infix 関数
    println(1 + 2 shl 2)            // 算術演算子が先:(1 + 2) shl 2
    println(1 shl 2 + 1)            // 1 shl (2 + 1)
}
true
true
a-b-c
(1, one)
12
8

1 to "one" で Pair を作る to も、標準ライブラリの infix 関数です。最後の2行は優先順位の確認で、中置呼び出しは算術演算子(+ など)より後に評価されます。1 + 2 shl 2 は (1 + 2) shl 2 で 12、1 shl 2 + 1 は 1 shl (2 + 1) で 8 です。

条件を満たさない関数に infix を付けると、'infix' modifier is inapplicable to this function. というエラーになります(引数が2つの拡張関数と、拡張ではないトップレベル関数の両方で確認しました)。

tailrec:末尾再帰をループに変える

再帰関数は、呼び出しが深くなるとスタック(呼び出しの記録を積む領域)を使い切ります。tailrec を付けると、コンパイラが再帰をループに変換するため、深さを気にせず書けます。

// tailrec なし:呼び出しのたびにスタックを積む
fun sumPlain(n: Long, acc: Long = 0): Long =
    if (n == 0L) acc else sumPlain(n - 1, acc + n)

// tailrec あり:コンパイラがループに変換する
tailrec fun sumLoop(n: Long, acc: Long = 0): Long =
    if (n == 0L) acc else sumLoop(n - 1, acc + n)

// 再帰呼び出しのあとに掛け算が残るので、末尾呼び出しにならない
tailrec fun fact(n: Long): Long =
    if (n <= 1) 1 else n * fact(n - 1)

fun main() {
    println(sumLoop(1_000_000))
    try {
        println(sumPlain(1_000_000))
    } catch (e: StackOverflowError) {
        println("sumPlain: ${e::class.simpleName}")
    }
    println(fact(10))
}

コンパイルすると、fact に対して警告が2つ出ます。

Tailrec.kt:10:1: warning: a function is marked as tail-recursive but no tail calls are found.
tailrec fun fact(n: Long): Long =
^^^^^^^
Tailrec.kt:11:28: warning: recursive call is not a tail call.
    if (n <= 1) 1 else n * fact(n - 1)
                           ^^^^

実行結果は次のとおりです。

500000500000
sumPlain: StackOverflowError
3628800
  • sumLoop は100万回の再帰でも計算できます。
  • 同じ内容で tailrec のない sumPlain は StackOverflowError で止まります。
  • fact は、再帰呼び出しの結果に n * を掛ける処理が残っているので、末尾呼び出しになりません。警告が出て、通常の再帰としてコンパイルされます。

tailrec を使えるのは、自分自身の呼び出しが関数の最後の処理であるときです。公式ドキュメントでは、再帰呼び出しのあとに処理が残る場合、try / catch / finally の中、open な関数では使えないと説明されています。sumLoop のように途中経過を引数(acc)に持たせる形に書き換えると、末尾呼び出しにできます。

Java のオーバーロードとの対比と @JvmOverloads

Java にはデフォルト引数がないため、引数の数ごとにメソッドを並べます。

public class Mailer {
    // Java は引数の数ごとにメソッドを並べる(オーバーロード)
    static String send(String to) {
        return send(to, "(件名なし)");
    }
    static String send(String to, String subject) {
        return send(to, subject, false);
    }
    static String send(String to, String subject, boolean urgent) {
        return "to=" + to + " subject=" + subject + " urgent=" + urgent;
    }
    public static void main(String[] args) {
        System.out.println(send("a@example.com"));
        System.out.println(send("a@example.com", "見積もりの件", true));
    }
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=見積もりの件 urgent=true

Kotlin では、同じことが関数1つで書けます。次の Overloads.kt には、同じ内容の関数を2つ用意しました。違いは @JvmOverloads の有無だけです。

package demo

fun plain(to: String, subject: String = "(件名なし)", urgent: Boolean = false) =
    "to=$to subject=$subject urgent=$urgent"

@JvmOverloads
fun send(to: String, subject: String = "(件名なし)", urgent: Boolean = false) =
    "to=$to subject=$subject urgent=$urgent"

fun main() {
    println(send("a@example.com"))
    println(send("a@example.com", urgent = true))
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=(件名なし) urgent=true

Kotlin から呼ぶぶんには、どちらも同じように引数を省略できます。違いが出るのは Java から呼ぶときです。コンパイルしたクラスを javap で見ると、Java から見えるメソッドがわかります。

public final class demo.OverloadsKt {
  public static final java.lang.String plain(java.lang.String, java.lang.String, boolean);
  public static java.lang.String plain$default(java.lang.String, java.lang.String, boolean, int, java.lang.Object);
  public static final java.lang.String send(java.lang.String, java.lang.String, boolean);
  public static java.lang.String send$default(java.lang.String, java.lang.String, boolean, int, java.lang.Object);
  public static final void main();
  public static final java.lang.String send(java.lang.String, java.lang.String);
  public static final java.lang.String send(java.lang.String);
  public static void main(java.lang.String[]);
}

plain は引数3つの形だけ、send は引数3つ・2つ・1つの形がそろっています。Java から呼んで確かめます。

import demo.OverloadsKt;

public class Caller {
    public static void main(String[] args) {
        System.out.println(OverloadsKt.send("a@example.com"));
        System.out.println(OverloadsKt.send("a@example.com", "見積もりの件"));
        System.out.println(OverloadsKt.send("a@example.com", "見積もりの件", true));
    }
}
to=a@example.com subject=(件名なし) urgent=false
to=a@example.com subject=見積もりの件 urgent=false
to=a@example.com subject=見積もりの件 urgent=true

@JvmOverloads のない plain を Java から引数1つで呼ぶと、javac が次のエラーを出します。

CallerPlain.java:5: error: method plain in class OverloadsKt cannot be applied to given types;
        System.out.println(OverloadsKt.plain("a@example.com"));
                                      ^
  required: String,String,boolean
  found:    String
  reason: actual and formal argument lists differ in length
1 error
比べる点 Java のオーバーロード Kotlin のデフォルト引数 Kotlin + @JvmOverloads
書く関数の数 引数の組み合わせの数だけ 1つ 1つ
Kotlin から引数を省略 ― できる できる
Java から引数を省略 できる できない(全引数が必要) できる(後ろから順に省いた形)
途中の引数だけ省略 その形を用意すればできる 名前付き引数でできる Java からはできない

@JvmOverloads が作るのは「既定値のある引数を後ろから順に省いた形」です。上の例では (to, subject, urgent)・(to, subject)・(to) の3つで、(to, urgent) の形は作られません。Java からも呼ばれる Kotlin のコードで、引数を省略して呼ばせたいときに付けます。

エラー文から引く早見表

この記事で再現したコンパイルエラーを、メッセージから逆引きできるようにまとめます。

エラー文(Kotlin 2.4.20) 原因 直し方
return type mismatch: expected 'Unit', actual 'Int'. ブロック本体で戻り値の型を書いていない ): Int { のように型を書く
type checking has run into a recursive problem. 単一式の再帰関数で型を省いた 戻り値の型を書く
no value passed for parameter 'message'. 既定値のない引数を渡していない 引数を渡す、または名前付きで渡す
mixing named and positional arguments is not allowed unless the order of the arguments matches the order of the parameters. 名前付き引数の後ろに、位置のずれた引数がある 飛ばした後ろはすべて名前を付ける
argument type mismatch: actual type is 'IntArray', but 'Int' was expected. vararg に配列をそのまま渡した *arr と書く
argument type mismatch: actual type is 'List<String>', but 'Array<out String>' was expected. List に * を付けた *list.toTypedArray() と書く
no value passed for parameter 'sep'. vararg の後ろの引数を位置で渡した sep = "-" と名前を付ける
'val' cannot be reassigned. 関数の引数に代入した 別の変数を val や var で作る
an overriding function is not allowed to specify default values for its parameters. override 側に既定値を書いた 子クラス側の既定値を消す
'infix' modifier is inapplicable to this function. 引数が1つでない、またはメンバー・拡張関数でない 条件を満たす形にするか infix を外す

8行目の 'val' cannot be reassigned. は、次のコードで確認しました。Kotlin の引数は読み取り専用(val と同じ扱い)なので、計算結果は新しい変数に入れます。

// E7.kt
fun addTax(price: Int) {
    price = price * 110 / 100
    println(price)
}
E7.kt:3:5: error: 'val' cannot be reassigned.
    price = price * 110 / 100
    ^^^^^

よくある質問

Q. 戻り値の型は、書くのと省くのとどちらがよいですか? A. ブロック本体で値を返す関数は必ず書きます。単一式関数は省けますが、再帰関数と、ほかのモジュールに公開する関数は書いておくのがおすすめです。本体を見ればすぐわかる短い非公開の関数は、省いても読みやすさは変わりません。

Q. デフォルト引数とオーバーロードは、どう使い分けますか? A. 「同じ処理で、一部の値に既定値があるだけ」ならデフォルト引数が向いています。引数の型や処理の内容そのものが違うとき(文字列で受ける形とファイルで受ける形など)は、Kotlin でもオーバーロードを使います。

Q. vararg と List を受け取る引数は、どちらにすればよいですか? A. 呼び出す側が値をその場で並べて書くことが多いなら vararg、すでにコレクションを持っていることが多いなら List が呼びやすくなります。vararg に List を渡すときは *list.toTypedArray() と変換が入る点が判断材料になります。

Q. 関数の引数を関数の中で書き換えられますか? A. 引数そのものには代入できません('val' cannot be reassigned.)。val taxed = price * 110 / 100 のように新しい変数を作ります。

まとめ

  • 基本形は fun 関数名(引数: 型): 戻り値の型 { ... }。 式1つなら = でつないで、波かっこ・return・戻り値の型を省ける
  • ブロック本体で戻り値の型を省くと Unit。 値を返すなら型と return を書く。= と { } を両方書くとラムダを返す関数になる
  • デフォルト引数は省略されたときだけ式が評価される。 必ず渡す引数を前、既定値のある引数を後ろに並べると呼びやすい
  • 名前付き引数は、引数を飛ばしたらそこから後ろすべてに名前を付ける。 位置が宣言どおりなら混在できる
  • vararg に配列を渡すときは *。 List は toTypedArray() で配列に変換する。vararg の後ろの引数は名前付きで渡す
  • Nothing は戻らない関数、ローカル関数は外に見せない処理、infix は引数1つのメンバー・拡張関数、tailrec は末尾再帰のループ化
  • Java から引数を省略して呼ばせたいときは @JvmOverloads。 後ろから順に省いた形のメソッドが作られる

参考・出典

  • Kotlin 公式ドキュメント「Functions」 https://kotlinlang.org/docs/functions.html
  • Kotlin 公式ドキュメント「Calling Kotlin from Java」(@JvmOverloads) https://kotlinlang.org/docs/java-to-kotlin-interop.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin関数の書き方 fun・デフォルト引数・名前付き引数・vararg」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?