Kotlin

特徴

拡張関数

既存のクラスに関数を追加できる。

fun String.hoge() {
    println("hello")
}
"aaa".hoge() // "hello"

ラムダ引数の前でカッコを閉じる

最後の引数が関数の場合に限り、最後の引数の前でカッコを閉じられる。

fun hoge(str: String, func: () -> Unit){
    // 任意の処理
}
hoge("aaa") { println("hoge") }

ラムダ式内のreturn省略

hoge { "aaa" }
// hoge { return "aaa" } と同じ

レシーバーの指定

[レシーバーの型].() -> [戻り値]

class Person(name: String)

fun doSomething(func: Person.() -> Unit){
    val person = Person("hoge")
    person.func()
}

doSomeThing {
    // ここに`Person`の匿名拡張関数を書くイメージ

    // ここで`this`は`Person`なので、`$name`は`${this.name}`と同じ
    println("my name is $name)
}

中置関数

infixキーワードを使って関数を定義する。言葉っぽく書けるようになる。
1 add 2のような書き方ができる。

中置関数の引数は1つである必要がある。

data class Point(x: Int, y: Int){
    infix fun add(other: Point) = Point(x + other.x, y + other.y)
}
val p1 = Point(1, 1)
val p2 = Point(2, 2)
println(p1 add p2) // Point(3, 3)

with的な関数も作れる。

// 第1引数で`T`型のインスタンスを受け取り、第2引数で指定した関数を適用する
fun <T> (receiver: T, block: T.() -> Unit): Unit

with("aaa") {
    println(this)
    println(this.toInt())
}

演算子オーバーロード

operator funで関数定義する。

data class Point(x: Int, y: Int)
operator fun Point.plus(that: Point) = Point(x + that.x, y + that.y)

val p1 = Point(1, 1)
val p2 = Point(2, 2)
println(p1 + p2) // Point(3, 3)

@DslMarkerアノテーション

ブロックをネストしたときにthisが外側のthisを指すようにしたいときに使う。

html {
    head {
        // ここで`this`が`this@html`になってしまうのを防ぎたいときに
    }
}

reified: 型パラメータでクラスオブジェクトを受け取る

クラスオブジェクトを受け取るとき、引数で受け取る代わりに型パラメータで受け取ることができる。

// 引数で渡すいつものパターン
val name = getClassName(Hoge::class)

// `reified`を使うとこう書ける
val name = getClassName<Hoge>()

関数はinline fun <reified T>で定義する。
interfaceなど、overrideすることのできる関数には使用できない。
また、inlineなのでprivateメンバを参照する関数にも使用できない。

class MyComponent {
    val mapper: ObjectMapper = jacksonObjectMapper()
    final inline fun <reified T> deserialize(value: String) = mapper.readValue<T>(value)
}

val component = MyComponent()
component.deserialize<String>("hoge")

reifiedを書かない場合次のエラーが表示される。

Cannot use 'T' as reified type parameter. Use a class instead.

Classはデフォルトでfinal

継承するためにはopenをつける。

open class Hoge

class ExtendedHoge: Hoge()
### `out`: "*型のサブクラス"を指定する

Javaでいう`<? extends Hoge>`。

```kotlin
// 戻り値に`Person`のサブクラス(Personはだめ)を指定
fun hoge(person: Mono<Person>): Mono<out Person>

関数をもらう関数

例。

fun <T> hoge(func: () -> T){}

apply, let, also, runの違い

Function Target passed as Returns
also it target
apply this target
let it block return value
run this block return value

Ref:
https://proandroiddev.com/the-difference-between-kotlins-functions-let-apply-with-run-and-else-ca51a4c696b8

文法

メソッド参照

他クラスのメソッドはHogeClass::function 関数や、自クラスのメソッドは::function

Kotlinらしい書き方

ifで条件によりオブジェクトに設定する場合はapply()を使う

// bad
val hoge = if(isHoge) Fuga().enable() : Fuga()

// good
val hoge = Fuga().apply { if(isHoge) enable() }

拡張関数を使って引数を減らす

// bad
fun publishUser(user: User)

// good
fun User.publish()

nullの場合にデフォルト値を返す

// age == nullなら0
val age = user.age ?: 0 

lambda

コンストラクタをメソッド参照で書く

JavaでいうHoge::new
Kotlinでは::Hogeで書く。

パフォーマンス

繰り返しですべての要素を処理する必要がないときはSequenceを使う

特に、大量の要素に対する処理があって最後でtake()とかfirst()とかしている場合は必須。

// List: 全件に対して`map()`の変換処理をする
(1..10).map { it * 10 }
      .take(3)

// Sequence: `take(3)`があることにより先頭3件取得すると終了できるので、変換処理は3回のみ行う
(1..10).asSequence()
      .map { it * 10 }
      .take(3)
      .toList()

Tips

分割代入

()を使うと、オブジェクトから複数の変数を取り出して一度に定義できる。

data class Hoge(val a: String, val b: Int)

val (a, b) = Hoge("fuga", 1)

関数の戻り値がオブジェクトで、単に中の値を使いたいだけの場合などに便利。

val (result, status) = myFunc()

Ref:
(Destructuring Declarations)[https://kotlinlang.org/docs/reference/multi-declarations.html]

複数アノテーションを1つにまとめる

長いアノテーションを毎回書くのが面倒なときに。

@Hoge(name = "hoge")
@Fuga()
@Piyo()
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyAnno

ZonedDateTimeのZone変換

withZoneSameInstant()を使う。
入力はJSTだけど処理の中でUTCに変換して使うときとか。

// JSTの日時に変換するフォーマッタ
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Tokyo"))

// JSTのZonedDateTimeを生成
val jst = ZonedDateTime.parse("2000-10-10 00:00:00")

// UTCに変換
val utc = jst.withZoneSameInstant(ZoneOffset.UTC)

ZonedDateTimeから"YYYY/MM/ddTHH:mm:ssZ"(ISO-8601)の文字列を取得する

Instant.toString()で取得できる。

ZonedDateTime
    .now()
    .toInstant()
    .toString()

ISO-8601について:
https://www.coppermine.jp/docs/notepad/2016/12/iso-8601.html

LocalDateTimeをエポックミリ秒へ変換する

LocalDateTime.toEpochSecond()を利用する。
引数にZoneOffset.UTCを入れるとUTCになる。
ZonedDateTimeから変換する場合、ZoneOffsetがすでに入っているため引数なしでtoEpochSecond()を呼べばいい。

val d = LocalDateTime.now()
// 秒で取得
val EpochSeconds: Long = d.toEpochSecond(ZoneOffset.UTC)
// ミリ秒で取得
val EpochMillis: Long = d.toInstant(ZoneOffset.UTC).toEpochMilli()

// エポックミリ秒からLocalDateTimeを生成
LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(longValue), ZoneOffset.UTC);
// エポックミリ秒からLocalDateを生成
LocalDate date = Instant.ofEpochMilli(longValue).atZone(ZoneOffset.UTC).toLocalDate();

Ref: How can I create a Java 8 LocalDate from a long Epoch time in Milliseconds?

ListをMapに変換

associateBy()を使う。
第二引数を使ってキーの変換もできる。

val idUserMap = users.associateBy { it.id }

ref:
https://stackoverflow.com/questions/32935470/how-to-convert-list-to-map-in-kotlin

条件に一致するときに処理

takeIf()を使うとif文を省略できる。

"hoge".takeIf { isEmpty() }.let { print(it) }

// nullチェックの代わりに書くと超便利
val hoge: String? = null
hoge?.takeIf { isEmpty() }.let {
    doSomething(it)
}

Listを任意の件数ごとに分割して処理

val list = listOf(1, 2, 3, 4, 5, 6, 7, 8)
    .withIndex()
    .groupBy { indexedValue -> indexedValue.index / 5 } // 5件ごと
    .map { group -> group.value.map { println(it.value) } } // 分割リストごとの処理

例外が起きたらnullを返す関数

// Exceptionが起きたらnull、そうでなければ引数の戻り値をそのまま返す
fun <T> nullIfError(func: () -> T?): T? = try { func() } catch (e: Exception) { null }

Unit Test

引数で受け取った値をモック戻り値に利用する。

doAnswer()を使う。

private val hogeRepo = mock<HogeRepository> {
    on { save(any()) } doAnswer { (it.arguments[0] as Hoge) }
}

モックオブジェクトの呼び出し検証

argumentCaptor<>を使う

val actual = argumentCaptor<Hoge>().apply { verify(hogeRepo, times(1)).save(capture()) }.firstValue

validation

Kotlinで@NonNullを使うときは一工夫いる。

class User(
    id: Int?
    val name: String
) {
    // nullを入れてからバリデーションが動くので、先にGetterを持つNullableのダミーのフィールドに入れてから、
    // 遅延評価でフィールドにNonNullableにキャストして代入する
    private val _id: Int? = id @NotNull get() = field
    val id: Int by lazy { _id!! }
}

タイムアウト処理

coroutineを使うと簡単にできる。

build.gradleに依存関係を追加する。

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core"
with

Ref:
https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#timeout

ハマりどころ

Error

calls to static methods in Java interfaces are prohibited in JVM target 1.6. Recompile with '-jvm-target 1.8'

8に設定
Settings > Build, Execution, Deployment > Compiler > Kotlin Compiler

Warning

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.

Javaのメソッドを呼んでいる箇所で、戻り値がnull許容かどうかわからないときに起こる。
関数の戻り値を明示的に指定してあげるとでなくなる。