はじめに
本記事は
『Kotlinイン・アクション』
Dmitry Jemerov、Svetlana Isakova 著、長澤太郎、藤原聖、山本純平、yy_yank 訳
を読んで個人的に「へぇ」ってなったイディオム・設計思想・Tipsをまとめたものです。
私の前提知識
- Oracle Certified Java Programmer, Gold SE 8 保有
- Java 実務開発経験 3 カ月
- Kotlin 実務開発経験 5 カ月
第 1 章 Kotlin とは何か? なぜ使うのか?
Kotlin は、静的型付け言語で、型推論をサポートしています。それによって、ソースコードを簡潔に保ちつつ、正確性とパフォーマンスを維持できます。
「うん。」
第 2 章 Kotlin の基本
カスタムアクセサ
class Rectangle(
val height: Int,
val width: Int
) {
val isSquare: Boolean
get() = height == width
}
isSquare プロパティは、値をフィールドに格納する必要はありません。ここにはカスタム getter の実装があるだけです。必要な値は、プロパティから値を取得するたびに毎回計算されます。
「うん。でもこの例の場合、getter の意味ある?イミュータブルだから初期代入でよいのでは。」
第 3 章 関数の定義と呼び出し
ペアを扱う
マップを生成するためには、mapOf 関数を使用します。
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
このコードの to という言葉は、組み込みの機能ではなく、特別な種類のメソッド呼び出しであり、中置呼び出し(infix call)と呼ばれています。次の 2 つの呼び出しは等価です。
1.to("one")
1 to "one"
中置呼び出しは、1 つの必須な引数を持った通常のメソッドや拡張関数で使用可能です。中置記法を使って関数を呼び出すためには、その関数に infix 修飾子を付ける必要があります。
infix fun Any.to(other: Any) = Pair(this, other)
to 関数は、Pair のインスタンスを返します。
val (number, name) = 1 to "one"
この機能を分解宣言(destructuring declaration)と呼びます。
「へぇ。」
第 4 章 クラス、オブジェクト、インタフェース
open、final、abstract 修飾子
ご存じの通り、Java では、明示的に final キーワードを付けない限り、どのようなクラスのサブクラスでも作成可能で、すべてのメソッドをオーバーライドできます。これは便利ですが、いわゆる壊れやすい基底クラス(fragile base class)と呼ばれる問題を引き起こすこともあります。基底クラスのコードが変更されたときに、その変更がサブクラスが期待するものではなくなってしまったために、サブクラスでの不正な挙動を引き起こすということです。
Java プログラミングスタイルの名著である『Effective Java』Joshua Bloach 著、柴田 芳樹 訳 では、「継承のために設計および文書化する。でなければ継承を禁止する」と勤めています。つまり、サブクラスによってオーバーライドされることを特別に意図していないすべてのクラスやメソッドは、明示的に final と指定すべきであるということを意味しています。
Kotlin は、この思想を受け継いでいます。したがって、Kotlin の場合はデフォルトで final です。
修飾子 | 対応するメンバ | コメント |
---|---|---|
final | オーバーライドできない | クラスメンバのデフォルト値として指定されている |
open | オーバーライドできる | 明示的に指定する必要がある |
abstract | オーバーライドしなければいけない | 抽象クラスのみで使用可能。抽象メンバは実装を持つことができない |
override | スーパークラスかインタフェースのメンバをオーバーライドする | final と指定されていない場合、オーバーライドされるメンバはデフォルトで open |
「うんうん。」
可視性修飾子
Java のデフォルトの可視性であるパッケージジプライベートは Kotlin には存在しません。Kotlin におけるパッケージは、名前空間にコードをまとめるためだけに使用され、可視性の制御には使用されません。
それとは別に、Kotlin では internal という新しい修飾子を提供しています。これは「モジュール内で参照可能」という意味です。
修飾子 | クラスメンバ | トップレベルの宣言 |
---|---|---|
public(デフォルト) | どこからも参照可能 | どこからも参照可能 |
internal | モジュール内からのみ参照可能 | モジュール内からのみ参照可能 |
protected | サブクラスから参照可能 | |
private | クラス内からのみ参照可能 | ファイル内からのみ参照可能 |
internal 修飾子はバイトコード上では public になります。
「うんうん。」
シールドクラス
スーパークラスに sealed 修飾子を設定すると、生成可能なサブクラスを制限できます。すべての直接のサブクラスは、スーパークラスにネストされている必要があります。
sealed class Expr { // 取りうるサブクラスをすべてネストされたクラスとして列挙
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr): Expr()
}
fun eval(e: Expr): Int =
when (e) { // sealed により when 式はとりうるすべてのケースをカバーするので else 分岐は必要ない
is Expr.Num -> e.value
is Expr.Sum -> eval(e.left) + eval(e.right)
}
when 式内のすべてのシールドクラスのサブクラスを処理するのであれば、デフォルトの分岐 else を追加する必要はありません。
when を sealed クラスと一緒に使っており、新しいサブクラスを追加する際、値を返す when 式はコンパイルに失敗します。コードを変更しなければいけないと指摘しているのです。
「へぇ。安全だし便利。」
クラスを初期化する
class User(val nickname: String)
このように丸括弧で囲まれたコードブロックは、プライマリコンストラクタと呼ばれます。これは、コンストラクタの引数を指定し、それらの引数で初期化されるプロパティを定義するという 2 つの役割を担っています。
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
constructor キーワードは、プライマリもしくはセカンダリコンストラクタの宣言のはじめに付けられます。
init キーワードは、初期化ブロック(initializer block)を実装するためのキーワードです。このブロックは、クラスが生成されたときに実行される初期化コードを含み、プライマリコンストラクタと一緒に使用されることが意図されています。なぜなら、プライマリコンストラクタの構文には制約があり、初期化コードを含むことができないからです。
「へぇ。自由度高い。」
getter と setter からバッキングフィールドにアクセスする
値を格納するプロパティと、その値を取得または変更したときに実行される追加のロジックを実装する方法を見てみましょう。これをサポートするために、アクセサからプロパティのバッキングフィールドにアクセスできる必要があります。
class User(val name: String) {
var address: String = "unspecfied"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".
""".trimIndent())
field = value // address に代入すると再度セッターが呼ばれオーバーフローする
}
}
setter の本体では、field という特別な識別子を使って、バッキングフィールドの値にアクセスします。
バッキングフィールドを持つプロパティと、持たないプロパティの間にはどのような違いがあるのか、不思議に思うかもしれません。バッキングフィールドへの明示的な参照がある場合とデフォルトのアクセサの実装を使用する場合に、コンパイラはプロパティのバッキングフィールドを生成します。カスタムアクセサの実装が field を使わない場合、バッキングフィールドは存在しません。
「へぇ。試しにバッキングフィールド使わなかったら StackOverflow したよね。(そりゃそう)」
アクセサの可視性を変更する
アクセサの可視性は、デフォルトではプロパティと同じです。しかし、get もしくは set キーワードの前に可視性修飾子を置くことで、必要に応じて可視性を変更できます。
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter = word.length
}
}
「へぇ。でもこれ忘れちゃいそう。」
クラス委譲
Kotlin では、委譲パターンが第一級言語機能としてサポートされています。インタフェースを実装しているなら、by キーワードを使うことによって、いつでもインタフェースの実装を別のオブジェクトに委譲(delegate)できます。
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun contains(element: T): Boolean innerList.contains(element)
override fun iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean =
innerList.containsAll(elements)
}
↓
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
クラス内のすべてのメソッド実装がどこかに行ってしまいました。その部分はコンパイラが生成しますが、実装は 1 つ目と似たものになります。
いくつかのメソッドの振る舞いを変更する必要があるときは、そのメソッドをオーバーライドできます。
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {
var objectAdded = 0
override fun add(element: T): Boolean {
objectAdded++
return innerList.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectAdded += c.size
return innerSet.addAll(c)
}
}
「へぇ。これが Delegate Pattern ってやつか。」
コンパニオンオブジェクト
トップレベル関数はクラスの private メンバにアクセスすることができません。
したがって、クラスのインスタンスを持たずに呼び出しが可能で、クラスの内部にアクセスする必要がある関数を書く場合、そのクラスの内部のオブジェクト宣言のメンバとして関数を書くことができます。そのような関数の例は、ファクトリメソッドでしょう。
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) =
User(email.substringBefore('@'))
}
}
「へぇ。ファクトリは基本ここに書く感じだね。」
通常のオブジェクトとしてのコンパニオンオブジェクト
コンパニオンオブジェクトの名前を省略した場合は、Companion というデフォルトの名前が付けられます。
「へぇ。」
第 5 章 ラムダを使ったプログラミング
スコープ内の変数アクセス
Kotlin と Java との重要な違いの 1 つは、Kotlin では、final 変数への参照だけに限定されていないことです。ラムダ内から変数を変更することもできます。
fun printProblemCount(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {
if (it.startsWith("4")) {
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
例の prefix、clientErrors、serverErrors などのラムダから参照される外部変数は、ラムダによってキャプチャ(capture)されていると表現されます。
「へぇ。」
遅延コレクション操作・シーケンス操作の実行
map や filter といった関数は、中間のコレクションを先行評価して(eagerly)作成します。つまり、各ステップの中間結果は一時的なリストに保持されます。シーケンス(sequence)という別の方法を使うことで、そのような一時オブジェクトを生成しない計算を実行できます。
原則として、大きなコレクションに対してチェインした操作を行うときには、シーケンスを使用します。
print(listOf(1, 2, 3, 4).asSequence().map {it * it}.find {it > 3})
この例で注目すべき重要なことは、処理が実行される順番です。単純な手法では、最初に各要素に対して map 関数を呼び出してから、その結果の各要素に対して filter 関数を呼び出します。
シーケンスにおいては、すべての操作が各要素に順番に適用されます。最初の要素が処理され、2 番目の要素が処理され、3 番目以降についても同様です。
「へぇ。Kotlin Collectionってシーケンシャルじゃなかったんだ。」
with 関数
同じオブジェクトに対してその名前を繰り返すことなく複数の操作をするために使える特別な構文があります。
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) {
for (letter in 'A'..'Z') {
this.append(letter) // 明示的に this を使ってレシーバのメソッドを呼び出す
}
append("\nNow I know the alphabet!") // this なしでメソッドを呼び出す
this.toString() // ラムダから値を返す
}
}
with 関数は、最初の引数を 2 番目の引数のラムダのレシーバ(receiver)に変換します。このレシーバは、明示的に this 参照を用いて参照できます。通常は this 参照を省略し、追加の修飾子なしで、そのメソッドやプロパティを参照できます。
「へぇ。StringBuffer にいくつも append することあるから使えそう。」
apply 関数
apply 関数は、with とほぼ同じ動きをします。唯一の違いは、apply は常に引数の値として渡されたオブジェクト(つまり、レシーバオブジェクト)を返すことです。
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
「へぇ。apply って名前のイメージ通りですき。」
第 6 章 Kotlin の型システム
安全キャスト
as? 演算子は、指定された方にキャストしようとして、値がその方ではない場合に null を返します。安全キャストを使う一般的なパターンの 1 つは、エルビス演算子と組み合わせることです。
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false
return otherPerson.firstName == firstName &&
otherPerson.lastName == lastName
}
override fun hashCode(): Int =
firstName.hashCode() * 37 + lastName.hashCode()
}
fun main() {
val p1 = Person("Dmitry", "Jemerov")
val p2 = Person("Dmitry", "Jemerov")
println(p1 == p2)
println(p1.equals(42))
}
このパターンを使えば。変数が適切な型を持っているかをチェックし、それをキャストし、型が正しくない場合は false を返すといったことを、同じ式の中で簡単に行えます。
「へぇ。Java よりだーいぶ簡潔。スマートキャストが効いてるのが Good 」
非 null 表明
2 つの感嘆符というのは、いささか乱暴に見えるかもしれません。コンパイラに対して叫んでいるようにも見えますが、意図的なものなのです。Kotlin の設計者は、コンパイラによって検証できない non-null assertion を使わない、より優れた解決策へといざなっているのです。
心に留めておくべき注意点が、もう 1 つあります。!! を用いて例外が起きた場合、スタックとレースは例外がスローされた場所のしきではなく行番号を示します。どの値が null なのかを明確にするために、同じ行で複数の !! を使うことは避けてください。
person.company!!.address!!.country
この行で例外が発生した場合、null なのが company なのか address なのかを判別できません。
「へぇ。そんな設計思想があったのか。」
let 関数
let 関数は、null 許容型の式を扱いやすくします。セーフコールとともに使えば、式を実行し、その結果が null であるかチェックをして、変数に保持するといったことがすべて 1 行の簡潔な式で行えます。
最も一般的な用法の 1 つは、null 非許容型の引数をとる関数に null 許容型の引数を渡さなければならない場合です。
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
複数の値が null であるかをチェックする必要がある場合は、ネストされた let 呼び出しを使って処理できます。しかし、そういった場合は、たいていコードが冗長になり、追うのが難しくなります。一般的には、通常の if 式を使って、すべての値のチェックを一緒にしてしまった方が簡単になるでしょう。
「うん。いわゆる guard let もやめた方がいいということかな。」
Unit 型
Void ではなく Unit と違う名前にしたことを不思議に思うかもしれません。Unit という名前は、関数型言語において伝統的に「ただ 1 つのインスタンス」を意味するために使われています。
そして、これはまさに Kotlin の Unit と Java の void の違いなのです。
「うん、よくわからない。」
読み取り専用コレクションとミュータブルコレクション
コレクションのインタフェイスを扱う際に留意すべき重要な点は、読み取り専用コレクションが、必ずしもイミュータブルではないということです。読み取り専用インタフェイス型のコレクションを参照する変数があっても、同じコレクションへの数ある参照の 1 つに過ぎないということです。
コレクションに対する別の参照を持つコードを呼び出し、それを並列に走らせた場合、コレクションの作業中に他方のコードによってコレクションが変更される場面に出くわすかもしれません。それゆえ、読み取り戦勝コレクションはスレッドセーフではないことの理解が不可欠です。
「へぇ。読み取り専用コレクションはイミュータブルで、スレッドセーフって勝手にイメージしてたからちょっとビックリ。」
第 7 章 演算子オーバーロードとその他の変換の規約
二項演算子のオーバーロード
演算子のオーバーロードに使われているすべての関数には、operator というキーワードを付ける必要があります。これは規則に対応する実装として関数を利用することを意図しており、たまたま同じ名前の関数を定義してしまったわけではないということを明確にしています。
「へぇ。operator fun invoke をなんとなしに使っていたけど、そんな意味が。」
委任プロパティを使う
遅延初期化(lazy initialization)は、最初にオブジェクトが参照されたときに、初めてそのオブジェクトの一部を生成させる一般的なデザインパターンです。データの初期化処理が大きなリソースを消費し、オブジェクトの使用中、常にそのデータが必要ではない場合に、この遅延初期化が役立ちます。
class Person(val name: String) {
private val _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
fun main() {
val p = Person("Alice")
println(p.emails)
println(p.emails)
}
ここでは、いわゆるバッキングプロパティ(backing property)というテクニックを使います。まず、_emails という値を格納するプロパティと、それとは別の emails という _emails への読み取りアクセスを提供するプロパティを実装します。_emails は null 許容型、emails は null 非許容型と、これらのプロパティの型はことなっているので、2 つのプロパティを使う必要があります。
しかし、このコードは少し面倒です。遅延プロパティがいくつもある場合、コードがどれくらい長くなるだろうかと想像してみてください。そのうえ、スレッドセーフではないため、常に正しく動くとは限りません。
このコードは、委譲プロパティをつかうことで、はるかにシンプルになります。
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy 関数は、適切なシグネチャの getValue という名前のメソッドを持つオブジェクトを返すので、by キーワードと一緒につかうことで、委譲プロパティを生成できます。lazy の引数は、値を初期化するために呼び出されるラムダです。lazy 関数はデフォルトでスレッドセーフです。
「へぇ。ボイラープレートも排除できるうえ、スレッドセーフなのは強いな。」
第 8 章 高級関数
インライン関数
通常、ラムダは無名クラスにコンパイルされます。しかし、これはラムダ式を使う度に余分なクラスが作成されることを意味しています。ラムダがいくつかの変数をキャプチャすると、新しいオブジェクトがすべての呼び出しで作成されます。それによって実行時のおあーばーヘッドが発生し、同じコードを直接実行する関数よりもラムダを使用する実装の効率が低下してしまいます。
inline 修飾子で関数をマークすると、コンパイラはこの関数を使用するときには関数呼び出しを生成せず、代わりに関数のすべての呼び出しをその関数を実装する実際のコードに置き換えます。
関数を inline で宣言すると、その本体はインライン展開されます。言い換えると、通常の呼び出しの代わりに、関数が呼び出される場所に関数の本体が直接置換されます。
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
fun main() {
val l = Lock()
foo(l)
}
同じバイトコードにコンパイルされる同等のコードは次の通りです。
fun __foo__(l: Lock) {
println("Before sync")
l.lock()
try {
println("Action")
}
finally {
l.unlock()
}
println("After sync")
}
インライン展開は、synchronized 関数の実装だけではなく、ラムダ式にも適用されることに注意してください。ラムダから生成されたバイトコードは、呼び出し関数の定義の一部となり、関数インタフェイスを実装する無名クラスにはラップされません。
「へぇ。これだけ見ると、漏れなく inline 展開をした方がよさそうだけど、(次項を読むと)実際はそうではないみたい。」
いつ、インラインとして関数を宣言することを決定するか
inline キーワードの利点について学んだので、コードベース全体を通して inline を使って、より速く実行できるようしたくなったかもしれません。結論から言うと、これはよい考えではありません。inline キーワードを使用すれば、ラムダを引数として取る関数ではパフォーマンスが向上する可能性があります。しかし、それ以外の場面については、さらなる計測と調査が必要です。
ラムダを引数に持つ関数をインライン展開することは有益です。第一に、インライン展開で回避するオーバーヘッドはより重要な意味を持っています。呼び出しだけでなく、各ラムダの追加クラスとラムダのインスタンスのオブジェクトの作成を抑えます。第二に、現在のところ、JVM は呼び出しとラムダを介して常にインライン展開を実行できるほどスマートではありません。
「へぇ。高階関数のときは検討すればいい感じね。」
インライン化されたラムダをリソース管理に使用する
Kotlin は、Java 7の try-with-resources と同等の構文を備えていません。同じ絡子は、関数型を引数として持つ関数を通して、ほとんどシームレスに実行できるからです。その関数は use 関数と呼び誰、Kotlin 標準ライブラリに含まれています。
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
use 関数は、Closable インターフェイスを実装しているリソース上で呼び出される拡張関数です。引数としてラムダを受取ります。この関数はラムダを呼び出し、ラムダが正常に終了するか例外スローするかに関わらず、リソースが閉じられることを保証します。
「へぇ。try-with-resources は利用頻度高いから use はよく使うことになりそう。」
第 9 章 ジェネリクス
ジェネリック型パラメータ
Java とは異なり、Kotlin では型引数は常に明示的な指定か、あるいはコンパイラによる推論が必須です。
そのため、値がセットや他のオブジェクトではなくリストであることをチェックするには、どうしたらよいのでしょうか。スター投影(star projection)という特別な構文を使用することで可能になります。
if (value in List<*>) { ... }
「へぇ。Java でいうところの List> にあたるのかな。」
クラス、型、サブタイプ
時折、型とクラスという用語は同じものとして使ってきましたが、実際には別物です。
最も単純なケースとしては、非ジェネリッククラスでは、クラスの名前が型として直接使用されます。 var x: String?
のような null 許容型の宣言に同じクラス名を使用できることに注意してください。これは、Kotlin の各クラスが少なくとも 2 つの型を構築するのに使用されることを意味します。
サブクラスとサブタイプの違いは、ジェネリック型について説明するときに特に重要になります。「List<Any>
を期待する関数に対して、型 List<String>
の変数を渡すことは安全かどうか?」という問いは、サブクラスの観点からは「List<String>
は、List<Any>
のサブクラスなのだろうか?」と言い換えることができます。MuttableList<String>
は MuttableList<Any>
のサブクラスとして扱うことはできません。その逆も安全ではないことは明らかです。つまり、MuttableLisy<Any>
は MuttableList<String>
のサブタイプではないということです。
2 つの異なる任意の型AとBにおいて、MutableList<A>
が MutableList<B>
のサブタイプでもスーパータイプでない場合に、ジェネリッククラス MutableList
は、型パラメータについて不変(imnvariat)であるといいます。
「なるほど。あまりそこらへん意識して区別できてなかったし、ジェネリクスが入るとクラスの考え方が難しいな。」
共変と反変
Kotlin の List インタフェースは、読み取り専用コレクションを表現しています。AがBのサブタイプなら、List<A>
は List<B>
のサブタイプです。このようなクラスやインタフェースは、共変(covariant)と呼ばれます。
共変なクラスは、「AがBのサブタイプである場合、Producer<A>
は Producer<B>
のサブタイプである」が成り立つようなジェネリッククラスです。
反変(contravariance)の概念は、共変の鏡として考えることができます。反変クラスにとって、サブタイピングの関係は、型引数として使用されるクラスのサブタイピングの関係と正反対です。
クラスメンバの宣言における型パラメータの使用は、in と out のポジションに分けることができます。
共変 | 反変 | 不変 |
---|---|---|
Producer<out T> |
Consumer<in T> |
MutableList<T> |
クラスのサブタイピングは維持:Producer<Cat> は Producer<Animal> のサブタイプ |
サブタイプは逆:Consumer<Animal> は Consumer<Cat> のサブタイプ |
サブタイプなし |
T は out ポジションだけ | T は out ポジションだけ | T はどこでも |
「へぇ。Java でいう extend、super が in、out ってことか。」
#第 10 章 アノテーションとリフレクション
アノテーションの適用
アノテーションの引数を指定する構文は、Java とは少し違います。
-
クラスをアノテーションの引数として指定するためには、
@MyAnnotation(MyClass:cass)
のようにクラス名の後ろに ::class を付けます - 別のアノテーションを引数として指定するためには、アノテーションの前に @ を付けません。例えば、前の例の ReplaceWith はアノテーションですが、Deprecated アノテーションの引数として指定するときには @ を使いません。
-
配列をアノテーションの引数として指定するためには、
@RequestMapping(path = arrayOf("/foo", "/bar"))
のように、arrayOf 関数を使います。アノテーションクラスが Java 内に宣言されている場合は、value という名前の引数は必要な時に vararg 引数へど自動で変換されます。したがって、arrayOf 関数を使うことなく引数を渡せます。
「へぇ。Java でいう .class が ::class なのかな。」
アノテーションの対象
アノテーションを付ける要素は使用場所対象(use-site target)宣言を使って指定します。対象の指定は、@ とアノテーション名との間に置いて、転んで名前と区切ります。
- property:この使用場所対象を使って Java のアノテーションは適用できない
- field:プロパティ用に生成されるフィールド
- get:プロパティの getter
- set:プロパティの setter
- receiver:拡張関数や拡張プロパティのレシーバ引数
- param:コンストラクタ引数
- setparam:プロパティの setter の引数
- delegate:デリゲートプロパティのデリゲートインスタンスを保持するフィールド
- file:ファイル内に宣言されたトップレベル関数やプロパティを含むクラス
「へぇ。」
アノテーションを使って Java の API を制御する
Kotlin では、Kotlin で書かれている宣言をどのように Java のバイトコードにコンパイルし、Java の呼び出し側からどのように見えるかを制御するさまざまなアノテーションが提供されています。
- @JvmName は、Kotlin での宣言から生成される Java のメソッドやフィールドの名前を変更する
- @JvmStatic をメソッドに適用することで、オブジェクト宣言やコンパニオンオブジェクトのメソッドが static な Java のメソッドとして見えるようになる
- @JvmOverloads は、引数のデフォルト値を持った関数に対してオーバーロードメソッドを生成するように Kotlin のコンパイラに伝える
- @JvmFiled をプロパティに適用することで、プロパティが getter や setter のない public な Java のフィールドとして見えるようになる
「なるほど。ここらへんは、実際に Java のバイトコードにしたら分かりやすいかも。」
アノテーションの引数としてのクラス
KClass 型は、Java の java.lang.Class 型に相当する Kotlin の型で、Kotlin のクラスへの参照を保持するために使います。KClass の型パラメータは、この参照によって Kotlin のどのクラスを参照できるかを指定しています。
「へぇ。リフレクションで使えそう。使わない方が安全ではあるけど。」
Kotlin のリフレクション
Kotlin のリフレクション API の主なエントリポイントは、クラスを洗わず KClass です。KClass は、クラスやスーパークラスなどに含まれるすべての宣言を列挙して参照するために使うことができます。
KCallable は、関数とプロパティのスーパーインタフェースなのです。call メソッドが宣言されていて、それを使って対応する関数もしくはプロパティの getter を呼び出すことが可能です。
KFunction1 のような方は、引数の数が違う関数を表しています。それぞれの型は KFunction を継承し、対応する数の引数を持った invoke という KFunction2 には operator fun invoke(p1: P1, p2: P2): R
が宣言されています。これらの関数はコンパイラによる生成型(synthetic compiler-generated types)ですが、その宣言は kotlin.reflection パッケージ内にはありません。つまり、そのインタフェースは、任意の数の引数を持つ関数に対して使えるということです。
「へぇ。Kotlin のリフレクションはずいぶん細かく痒いところに手が届く感じあるな。」
#第 11 章 DSL の作成
API から DSL へ
ドメイン固有言語(Domain-Specific Language)を使って、Kotlin のクラスで表現豊かな API を設計する方法について説明します。
洗練された API を構築するために利用できる Kotlin の機能には、例えば、拡張関数、中置呼び出し、ラムダ構文のショートカット、演算子のオーバーロードなどがあります。
通常の構文 | 洗練された構文 | 使われている機能 |
---|---|---|
StringUtil.capitalize(s) | s.capitalize() | 拡張関数 |
1.to("one") | 1 to "one" | 中置呼び出し |
set.add(2) | set += 2 | 演算子オーナーロード |
map.put("key") | map("key") | get メソッド規約 |
file.use({ f -> fread() }) | file.use { it.read() } | 括弧の外側のラムダ |
sb.append("yes") sb.append("no") |
with (sb) { append("yes") append("no") } |
レシーバー付ラムダ |
「まあ、Kotlin のイディオムでこんな簡潔で自由に表現できるよ!ってことかな。」
invoke 規約による柔軟なブロックのネスト
invoke メソッドが operator 修飾子付きで定義されているクラスは、関数のように呼び出せます。
class Greeter(val greeting: String) {
operator fun invoke(name: String) {
println("$greeting, $name")
}
}
fun main() {
val bavarianGreeter = Greeter("Servus")
bavarianGreeter("Dmitry")
}
このコードは、Greeter クラスに invoke メソッドを定義しています。このメソッドによって、Greeter インスタンスが関数であるかのように呼び出すことができるのです。内部では、bavarianGreeter("Dmitry")
の式は、bavarianGreeter.invoke("Dmitry")
のメソッド呼び出しにコンパイルされます。
「へぇ。省略は楽だけど、文脈に気を付けて使わないとクラス利用者にミスリードさせちゃいそう。」
#おわりに
Kotlin の生の思想に触れることができる良本でした。
また、「Kotlin の味見」や「Kotlin を愛でる」など、表題にさえも Kotlin のコンセプトやイメージみたいなものが詰まっており、Kotlin に対する愛を随所に感じました。