Kotlin の演算子オーバーロード

Kotlin では +- などの演算子をオーバーロードできる。例えば、a + b は Kotlin コンパイラによって a.plus(b) に置換されて実行される。つまり a + b を実現するためには aplus() メソッドを実装すればよい。どのメソッドがどの演算子に置換されるかについては、公式ドキュメントを参照されたい。

演算子オーバーロードの機能を例示するため、 plus() の実装例を次に示そう。

演算子を定義するときは operator キーワードを fun の前に置く。 plus() の実装そのものは、単にプロパティの値を加算するだけのもので、特に何の説明も必要な無いだろう。

実に簡単だ。

演算子オーバーロードの「オーバーライド」

演算子オーバーロードに関する Kotlin Koans の問題が秀逸であったので、紹介したい。

端的に言えば、次のようなクラス定義があるときに date + YEAR * 2 + WEEK * 3 + DAY * 15 のような演算を実現したい、というものだ (このとき dateMyDate 型のオブジェクト)。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
enum class TimeInterval { DAY, WEEK, YEAR }

模範解答にコメントを追記して、実行可能な状態にしたのが次のコードだ。

TimeInterval* (times) を演算子オーバーロードして、その結果を RepeatedTimeInterval とすることと、 MyDate.plus をオーバーロードして引数が TimeIntervalRepeatedTimeInterval のときで処理を分岐させることは、なかなか自分にはできない発想だなあ、と感じた。

Kotlin の SAM 変換

この記事では Kotlin の SAM 変換について説明する。まず、前準備として SAM インタフェースについて触れる。

SAM インタフェースとは Single Abstract Method インタフェースの略称である。つまり、定義する抽象メソッドがひとつのインタフェースを SAM インタフェースと呼ぶ (後述の通り、この定義は厳密ではない)。

JDK に標準で含まれる SAM インタフェースには RunnableComparator がある。

Kotlin における SAM 変換とは、関数リテラルで SAM インタフェースを置き換えることである。とはいえ、言葉だけでは理解しづらいと思うので、Kotlin Koans のスニペットで解説しよう。

次は ArrayListComparator を実装した無名オブジェクトでソートするサンプルである。この無名オブジェクトは compare() を実装している。

同様のことを Kotlin の SAM 変換機能で次のように簡潔に書ける。

Collections.sort() の第二引数は単なる関数リテラルだが、これが SAM インタフェースの実装として扱われる。

補足

Comparator は本当に SAM インタフェースなのだろうか? Oracle のドキュメントによると、このインタフェースには compare()equals() のふたつの抽象メソッドがある。

この疑問が FAQ なのか分からないけれど、「Maurice Naftalin’s Lambda FAQ」には次のような記載がある。

The interface Comparator is functional because although it declares two abstract methods, one of these—equals— has a signature corresponding to a public method in Object. Interfaces always declare abstract methods corresponding to the public methods of Object, but they usually do so implicitly. Whether implicitly or explicitly declared, such methods are excluded from the count.

Comparator は functional interface (i.e. SAM interface) である。抽象メソッドがふたつ宣言されているものの、そのうち equals()Object クラスのパブリックメソッドとシグネチャが同じだ。interface は常に Object のパブリックメソッドを暗黙的に宣言している。暗黙的であれ明示的であれ、こういうメソッドは数えあげる対象ではない。

なるほど。

Kotlin における無名オブジェクトのスコープ

Kotlin は無名オブジェクトをサポートしている。コード中で object { ... } として、オブジェクトを生成できる。

無名オブジェクトの型は、ローカル (もしくはプライベート) なスコープでしか使えない。このことを説明するため、公式ドキュメントには次のスニペットが掲載されている (日本語のコメントは筆者によるもの)。

知らないとハマりそうだ。

Kotlin の nullable 型

nullable 型の宣言

Kotlin では明示的に指定した変数でなければ、null を代入することはできない。null を許容する変数を「nullable」と表現する。これは null-able (null 可能) という意味である。

公式ドキュメントから、コード断片を引用しよう。

このように型名の末尾に ? をつけることで、その変数を nullable にできる。

変数が null かどうかの確認

Kotlin でも Java のように if (variable != null) { ... } として null チェックを行うことができる。しかし、多くの場合は次のような記法を用いることになるだろう。

variable?.something

これは variable が null でないときは variable.something を返し、そうでないときは null を返す。この ?. というオペレータは連続させることもできる。公式ドキュメントには次のような例がある。

bob?.department?.head?.name

これは、bobbob.departmentbob.department.head も null でないときは bob.department.head.name を返す。そうでなければ null を返す。

変数が null でないときに特定の値を返したいときは ?: というオペレータを使う。次のふたつの表現は等価だ。

// b が null でなければ b.length を返し、そうでなければ -1 を返す
val l: Int = if (b != null) b.length else -1

// 同じことを簡潔に表現している
val l = b?.length ?: -1

Kotlin の nut-null assertion

Kotlin には !! というオペレータがある。これを null でない変数に適用すると値が取得できる。null の場合は Null Pointer Exception が投げられる。

このように使う。

// b が null でなければ b.length を返し、そうでなければ Null Pointer Exception が投げられる
val l = b!!.length

Kotlin の安全なキャスト

Kotlin では as? オペレータで型キャスト時の ClassCastException を避けられる。

変数 a をキャストする例をみてみよう。

// a が Int にキャスト可能でなければ null が代入される
val aInt: Int? = a as? Int

null を許容する Collection 型

null を許容する Collection 型は、型変数の末尾に ? をつける。次のスニペットは公式ドキュメントからの引用である。

Java の null チェックを置き換える例

Kotlin Koans に Java の null チェックを Kotlin で置き換える問題がある。具体的には、次の Java コードを Kotlin に置き換える、というお題だ。

お題には制約があり、if は一箇所だけで使いましょうとのことだ。

模範解答はこうだ。

Kotlin なら Java よりだいぶ簡潔に書ける。

Kotlin のクラス

Kotlin のクラス定義についてまとめていく。

(プライマリ) コンストラクタとプロパティ

Kotlin のコンストラクタ定義は簡単で、たとえば次のように書ける。

class Person(firstName: String)

これだけで Person クラスと、そのコンストラクタを定義できる。しかし、これだけではコンストラクタは文字列 firstName を受け取るものの、オブジェクトの生成時に捨てられてしまう。

firstName を外からアクセス可能にするためには、次のようにすればよい。

class Person(firstName: String) {
    val firstName = firstName
}

これで、次のようにして firstName に外からアクセス可能になる。

val person = Person("MyFirstName")
println(person.firstName) // MyFirstName

同じ内容を次のように簡潔に書くこともできる。

class Person(val firstName: String)

Kotlin には data という修飾子があり、データを保存するためのクラスに使われる。 data 修飾子をつけると、Kotlin コンパイラが自動的に次のメソッドを実装してくれる。

  • equals()hashCode()
  • (読みやすい形式の) toString()
  • componentN() (N は整数: e.g. component1() は1番目のプロパティを返す)
  • copy()

これらのメソッドの詳細は公式ドキュメントに譲る

data を使う場合、コンストラクタで受け取る引数に valvar の指定が必須になる。先ほどと同じ内容を data class として書き直すと次のようになる。

data class Person(val firstName: String)

セカンダリコンストラクタ

コンストラクタは単なるクラス定義なので、オブジェクト生成時に実行するコードを記述できない。オブジェクトの生成時にコードを走らせたい場合は、init ブロックの中に書く。

class Person(val firstName: String) {
    init{
        // インスタンス作成時に実行される。
    }
}

なお、ここまで単に「コンストラクタ」と呼んできたものには、実は「プライマリコンストラクタ」という名称がある。プライマリと言うくらいなので、セカンダリコンストラクタというものもある。セカンダリコンストラクタを使い、オブジェクトの生成時にコードを実行することもできる。

次のコードを眺めてみよう。

class Person {
    constructor(param: String) {
        println("I'll be called when I'm instantiated.")
    }
}

public fun main(args: Array<String>) {
    Person("MyFirstName")  // "I'll be called when I'm instantiated."
}

constructor(...) の部分がセカンダリコンストラクタだ。このコードで “I’ll be called when I’m instantiated.” という文字列を出力できる。

プライマリコンストラクタとセカンダリコンストラクタの両方が定義されている場合、セカンダリコンストラクタはプライマリコンストラクタを this で呼ぶ必要がある。次のように書く。

class Person(val myFirstName: String) {
    constructor(param1: String, param2: String): this(param1) {  // プライマリコンストラクタに param1 を渡している
        println(param2)
    }
}

public fun main(args: Array<String>) {
    val person = Person("MyFirstName", "MySecondName")
    println(person.myFirstName)
}

継承

親クラスを明示的に指定しないクラスは Any の子クラスになる。

明示的に親クラスを指定するには、クラス定義の末尾に : BaseClassName(...) と書く。次に具体的な例を示す。

open class Base(p: Int)

class Derived(p: Int) : Base(p)

public fun main(args: Array<String>) {
    val derived = Derived(5)
}

これで Derived クラスは Base クラスを継承できる。 Base クラスの先頭にある open は、このクラスが継承可能であることを示している。 Java では final をつけない限りは全てのクラスが継承可能である。 Kotlin はその逆で、 open をつけない限りは全てのクラスで継承を許可しない。公式ドキュメントによると、これは Effective Java の “Design and document for inheritance or else prohibit it” の項に由来するポリシらしい。

一時ファイルを確実に削除するシェルスクリプト

途中で mktemp を実行するシェルスクリプトを運用していたら、SRE に「/tmp/temp.* がたくさんできているよ」とおこられた。

こういうイディオムがあるらしい。

SIGINT などのシグナルを受けたときにもきちんと一時ファイルを消せるようにするためのものだ。なるほどなあ。