Kotlinの好きなところ

tl;dr

  • kotlinを勉強していて特に気に入った言語仕様と例をひたすら紹介します
  • 紹介するコードは実際に書いてgithubにあげています
  • 2019/08/04に改訂しました。

気に入った言語仕様

  1. 演算子オーバーロード
  2. ifとwhen(switch)が式
  3. null安全
  4. イミュータブルにしやすい
  5. 拡張関数

紹介する言語仕様です。 他にも気に入っているものはあるのですが、サンプルコードや例を思いついたものだけ今回は紹介していきます。

1. 演算子オーバーロード

  • 独自定義した型で算術演算子を使える
    • + - * / % > < >= <= など
  • 例)
    • 有効期限日型と現在日付型の比較
      • 有効期限 < 現在日付
    • 有効期限の延長を足し算っぽく書ける
      • 有効期限 + 7L

言語固有の演算子の一部をメソッドとして利用できます。 +-といった記号ですね。

例に書いてあるとおり、プログラムをより直感的に表現することができるようになる仕様です。

コード例

data class ExpirationDate(val value: LocalDate) {
    operator fun compareTo(value: CurrentDate): Int { // <,>,<=,>=
        return this.value.compareTo(value.value)
    }

    operator fun plus(value: Long): ExpirationDate { // +
        return ExpirationDate(this.value.plusDays(value))
    }
}

class CurrentDate(val value: LocalDate) {
    operator fun compareTo(value: ExpirationDate): Int { // <,>,<=,>=
        return this.value.compareTo(value.value)
    }
}

実際の例です。 特定のシグニチャの条件を守ることで、メソッドが演算子として書けるようになります。

不等号の例でいくと、operatorとcompareToとIntの返り値といったシグニチャがそれです。 プラスの例で行くと、operatorとplusが定義されてるのがそれですね。

ちなみに引数にとれる変数の型は自分自身のクラスじゃなくても良かったりするところが使い勝手が良いポイントです。

実際にこのサンプルコードでも自分と違うクラスを受け取ってます。

使い方

// set up
val currentDate = CurrentDate(LocalDate.of(2019, 8, 4))
val expirationDate = ExpirationDate(LocalDate.of(2019, 8, 5))

// exercise
val actual = currentDate < expirationDate

// verify
assertTrue(actual) // 有効期限が切れてない

使い方の例です。 現在日付も有効期限も人が頭で考えるときは日付として捉えてますよね。

この2つの比較は日付の比較なので、どっちが未来でどっちが過去かって捉え方をしているはずです。 それって不等号を使えれば数式として自然に表現できませんか?

それをプログラムで表現するとこうなるわけです。

// set up
val expected = ExpirationDate(LocalDate.of(2019, 8, 12))
val expirationDate = ExpirationDate(LocalDate.of(2019, 8, 5))

// exercise
val actual = expirationDate + 7L

// verify
assertThat(actual, equalTo(expected))

次の例は有効期限の延長です。

有効期限を1週間伸ばすときはこんな感じですね。

有効期限 + 7と書けています。

自然言語や数式って慣れ親しんでいるものなので、こうかけるととても自然にプログラムを読むことが出来るようになります。

ただ、演算子オーバーロードという概念を知らない人には意味不明であり、混乱の元になるかもしれません。

しかし、みなさんはこの説明を読んだので、もうこういうコードを見てもすぐに理解できるはずです。 1upおめでとうございます。

何が嬉しいのか

  • 算術演算子は直感的に使える
    • 算数は誰でもできるので、数式でかけると素直に理解できる
  • 直感的な表現で数値計算や期間比較のコードを表現できるようになる

ifとwhen(switch)が式

kotlinではifやwhenは句ではなく、式です。 たぶん、言葉で表現してもわからないと思うのでコードの例を紹介します。

コード例

fun loveIf(language: String): String { // 関数名は適当
    val result = if (language.equals("KOTLIN", ignoreCase = true)) {
        "❤️" // return "❤️"と同じ意味
    } else {
        "💔" // javaならここで result = "💔"; とする必要がある
    }
    return "$language $result" // language + " " result と同じ意味
}
fun loveWhen(language: String): String {
    val result = when (language.toUpperCase()) {
        "KOTLIN" -> "❤️"
        else -> "💔"
    }
    return "$language $result"
}

まずifです。resultというローカル変数を定義していますが、ifのブロックの中でアサインするのではなくてifからreturnした値をresultにアサインしています。 これがifが句ではなくて式と言われている所以です。

whenでも全く同じことがおきています。

動作確認

// set up
val expected = "Kotlin ❤️"

// exercise
val actual = loveIf("Kotlin")

// verify
assertThat(actual, equalTo(expected))

returnされた値がちゃんと使われているかテストしています。

kotlin loveが期待値です。 loveIf関数にkotlinを与えて、その返り値がkotlin loveになっているか検証しています。

何が嬉しいのか

  • 特定の値の場合に既定値を与えるような式が簡潔になる
// Javaの例
String name = "Anonymous";
if (user.isLoggedIn) {
    name = user.name;
}
// Kotlinの例、この程度なら三項演算子でもできるが…
val name: String = if (user.isLoggedIn) user.name else "Anonymous"
  • Javaだとifやswitchで判定した結果を変数に代入する場合は見通しが悪くなりがちだし、変数をfinalにできなかった
    • final(イミュータブル)にするためにprivateメソッドなどでカプセル化して返り値を代入してた

既定値の割当でちょっと楽になります。

ただし注意なのは説明的メソッドや説明的変数の手法を使ったほうが可読性が高まるケースのほうが多いということです。

例の程度であれば十分短いので、有効なシーンといって良いかと思います。

ちなみにこのサンプルコードはネットに転がってたのをコピペしてます😅

2. null安全

  • null許容型とnull非許容型がある

Java何かと違って、nullableな変数かどうかが言語的に明確に表現されます。実際にコードの例を見てみましょう。

コード例

val notNullable: String = null // コンパイルエラー
var nullable: String? = null // null代入可

if (nullable != null) { // null check必須
    println(nullable.toUpperCase())
}

null非許容型は代入すらできません。コンパイルエラーになります。

null許容型はクエスチョンでnullかもしれないことを明示的に表現する必要があります。 さらにいうとnull許容型はnullではないことを検証するコードを書いたあとでないと、その変数を利用できない徹底っぷりです。

何が嬉しいのか

  • null非許容型にはnullが入らないので冗長なnullチェックがなくなる
  • null許容型はnullチェックしないと使えないので、うかつにメソッドを呼び出してNPEがおきにくい
  • nullかもしれない箇所ではnullを強く意識させられるのでnullの取り扱いを間違えにくい

ちなみにJavaにはOptionalがあるが、Optional自体がnullになりえるし、そもそも冗長なので私は仲良く慣れませんでした。

まだ良くわかってないこと

  • Null Object Patternとの明確な違い
    • 利用されるときの文脈によってはNOPは使いづらい
      • null許容型だとそれがマシになりそうな予感
  • Nullのときどう振る舞わせるか
    • 文脈によってnullのときどうするかが違うのが悩みどころではある
      • NOPのときに悩むことと同じ
      • これは業務で利用しながら考えるしかなさそう

3. イミュータブルにしやすい

  • data class x copy関数 x 名前付き引数の組み合わせで簡単にイミュータブルなオブジェクトを作れる
    • data classは後で単体でもう一度でてきます
  • List/MutableListとMap/MutableMapがある

みなさん、イミュータブルって言葉の意味わかります?反対語はミュータブルです。

状態を持たない、という意味ですね。プログラムならクラスのプロパティの値を書き換えられないとか、変数に再代入できないとかそんなことを意味します。

イミュータブルだとバグが入り込みにくくなるとされているのですが、そのイミュータブルを実現しやすい仕様が揃っています。イミュータブルだとなぜバグを生み出しにくくなるのかについては、別途ググってください。

data classってこんなやつ

// varにするとミュータブルになるので注意
data class ValueObject(val mainValue: String, val otherValue: String)

val valueObject = ValueObject("value object")
valueObject.mainValue = "other" // 代入不可、コンパイルエラーになる

classのprefixにdataとはいっているところと、プライマリコンストラクタの変数がvalで宣言されているところがポイントです。これで完全にイミュータブルなクラスが宣言できています。

この例では、mainValueはvalで宣言されており、javaで言うところのfinalがついている状態になるので代入ができません。

data class x copy関数 x 名前付き引数の組み合わせで簡単にイミュータブルなオブジェクトを作れる

  • Javaだとイミュータブルにしようとすると、とあるインスタンスの一部のプロパティだけ更新するときが面倒くさい
    • その他のプロパティはオリジナルを維持しつつ、更新対象プロパティのみ変わった新しいインスタンスを生成する必要がある
    • kotlinならそれを簡単に実現できるcopy関数がある

data classはイミュータブルなので、何かしらの状態を更新したい場合は更新された状態の新しいインスタンスを生成する必要がありますが、kotlinにはそれを簡単に実現できるcopy関数が存在します。

コード例
// set up
val valueObject = ValueObject(mainValue = "value object",
                              otherValue = "other value")

// exercise
// otherValueのみ書き換える
val otherValueObject = valueObject.copy(otherValue = "copied object")

// verify
assertThat(valueObject.mainValue, equalTo(otherValueObject.mainValue))
assertThat(valueObject.otherValue,
           not(equalTo(otherValueObject.otherValue)))
assertThat(otherValueObject.toString(),
           equalTo("ValueObject(mainValue=value object,
                                otherValue=copied object)"))

例だとother valueのみ書き換えています。

verifyの箇所で、copy前後でmainValueが同じこと、copy前後でother valueが変わっていること、copy前後でtoStringの結果がちゃんと変わっていること、などを検証しています。

List/MutableListとMap/MutableMapがある

// listOfだとイミュータブルなリストになる
val immutableList = listOf("a", "b", "c")
immutableList += "d" // コンパイルエラーになる
val added = immutableList + "d" // 既存Listにdをaddした新しいインスタンスを生成

// mutableListOfだとミュータブルなリストになる
val mutableList = mutableListOf("a", "b", "c")
mutableList += "d" // 内部的にはaddが呼び出される

あとは良い例だと思うので取り上げていますが、listやmapはイミュータブルとミュータブルを厳密に区別されています。

例えば、この例のimmutableListはimmutableだからaddできません。そのため、自身を更新するのではなくて、自身の要素と追加対象の要素をマージした新しいインスタンス+の結果として返しています

mutableは普通のArrayListですね

ちなみにlistの操作に+とか+=とかでてきてますがいったん気にしないでください。すぐ後で簡単に説明します

集合の操作はインスタンスが生成されてから実際に利用されるまでの間に足したり引いたりといった操作がされがちで、バグり易い気がします。

immutable操作させないことを強制できるので、安定したコードを書きやすいでしょう。逆にmutableListであれば操作されている前提を持つことができるので利用時にバグを埋め込みにくいのではないかと思います。

4. 拡張関数

  • 継承をせずとも既存のクラスを拡張できる言語機能
  • 基本型にすら新しい関数を生やすことができる

intだろうがStringだろうが関数を新しく生やすことができます。実際に少し後でコードをおみせします。

Listのコード例

  • 実は先程のlistの+もこれで実現されています
// Collection<T>.plusでCollectionに独自関数を追加している
public operator fun <T> Collection<T>.plus(element: T): List<T> {
    val result = ArrayList<T>(size + 1)
    result.addAll(this)
    result.add(element)
    return result
}

Collectionにplusが生えているところがポイントです。ちなみにoperatorという修飾子がついていますが、演算子オーバーロードになっていて、+演算子でも呼び出せるようになってます。

何が嬉しいのか

  • このクラスにこんなメソッドあったらなーってときに使えます
例えば

JavaならStringのnull or emptyなチェックってGuavaでこう書きますよね?

Strings.isNullOrEmpty(target)

これ、こう書けたほうが英語っぽいです

target.isNullOrEmpty()

まんまこんな感じで書けます。そうkotlinならね。

こう書いて

fun String?.isNullOrEmpty(): Boolean {
    return this == null || this.isEmpty()
}

こう使います

// set up
val sut:String? = null

// exercise
val actual = sut.isNullOrEmpty();

// verify
assertTrue(actual)

すごいでしょ?

5. 値オブジェクトに適した仕様がある

  • data classのことです

有効期限を例にとる

  • data classはたぶんこんな感じ
data class ExpirationDate(val value: LocalDate)
  • 以下が自動生成される
    • equals/hashCode()
    • toString
  • 文字列表現を手軽に得られるし、面倒なequals/hashCodeの実装がない
    • 値オブジェクトをequalsで比較するときは同値のチェックが目的なので、自動生成で良い

実証

equals()
// set up
val mockDate = LocalDate.of(2018, 8, 5).plusDays(1)
val expirationDate1 = ExpirationDate(mockDate)
val expirationDate2 = ExpirationDate(mockDate)

// exercise & verify
assertEquals(expirationDate1, expirationDate2) // 同値なのでtrue

equalsが自動で生成されていることを検証するコードです。Javaなんかだと、equalsのデフォルト実装は同一性の検証となるため、このコードは失敗するはずですが、kotlinではdata classのequalsは同値性を検証するコードを自動生成するので、このテストは成功します。

hashCode()
val mockDate = LocalDate.now().plusDays(1)
for (i in 0..10) {
    // set up
    val expected = 4135425
    val sut = ExpirationDate(mockDate)

    // exercise
    val actual = sut.hashCode()

    // verify
    assertEquals(actual, expected) // 何度実行しても同じHash値
}

hashCode()もequalsと同様です。検証コードをお見せしましたが解説は飛ばします。

toString()
// set up
val expected = "ExpirationDate(value=2019-08-05)"

// exercise
val actual = ExpirationDate(LocalDate.of(2019, 8, 5)).toString()

// verify
assertEquals(actual, expected)

JavaでもtoStringは自動生成されますが、ハッシュ値がでてくる実装になっています。kotlinではdata classは人間が見て意味がわかる文字列表現を自動生成してくれます。

何が嬉しいのか

  • 値オブジェクトのときのボイラープレートなコードを自動生成してくれること
  • 前のスライドで示した通り、イミュータブルであること

ちなみに...

Javaで値オブジェクトを実装するときは値の妥当性をコンストラクタで検証しますが、Kotlinでも同じことができます。initializerブロックがそうです。

JavaでもできることをKotlinでも出来ますという紹介にしかならないので、今回は割愛します。

5. その他の気になってる言語仕様

  • スマートキャスト
    • javaでいうinstanceOfをしたら、もうその変数がキャスト済みになる
  • エルビス演算子 ?:こんなやつ
  • エイリアス
  • in句
    • list#containsよりこっちのほうがわかりやすい
  • tryが式
    • if/whenと同様
  • Stringをregexに変換するtoRegexメソッド
    • regexをより直感的に使いやすくなる
  • トリプルクォート
    • 面倒なエスケープを入れなくてよくなるらへんが気に入りそう