前回に続いて、今回も簡単にKotlinで色々書いてみましたのでその紹介となります。Kotlinではスタンダードライブラリや言語仕様として提供している機能がかなり多いので、これらを使いこなすだけでも生産性やコードのクォリティが大幅に上がるのではないかと思います。なので、今回もJava的な書き方を、Kotlinではどんな方法で効率よく実現できるかを中心に紹介したいと思います。
もちろんKotlinでは基本的にJavaの書き方でも全く問題なく動くコードを書けますが、Kotlinならではのコードに変えた方がより簡単で短いコードを書ける場合が多く、色々と手間を省けることができるので(そして大抵の場合、スタンダードライブラリの実装の方が自分の書いたコードよりクォリティ高いような…)こういう工夫はする価値が十分にあるのではないかと思います。
なので、今回は自分が調べたKotlinの小技を少し紹介したいと思います。
Sequentialなデータを作成する
よくユニットテストなどでテスト用データを作成して使う場合がありますね。こういう時に必要となるデータの種類は色々とあるかと思いますが、複数のレコードを番号をつけて順番に揃えた感じのものを作りたい場合もあると思います。例えばData01、Data02、Data03…といったデータを作りたい場合ですね。
この場合は、ループでデータを作り、Listにまとめるというのが一般的ではないかと思います。例えば以下のような例があるとしましょう。
// テスト用データを作成する
fun createTestDatas(): List<String> {
// テスト用データのリスト
val testDatas = mutableListOf<String>()
// 10件のデータを追加
for (i in 0 until 10) {
testDatas.add("テスト$i")
}
// read-onlyに変換して返却
return testDatas.toList()
}
ただ、どちらかというとこれはJavaのやり方に近いので、まずはこれをベースに、Kotlinらしきコードではどうやって同じことができるかを考えてみたいと思います。
repeat
まず考えられる方法は、ループの単純化ですね。サイズが10のリストを作りたいということは、ループが10回であることなので、それに相応しい関数を使います。例えばrepeatがありますね。repeat
を使うと、スコープ内のパラメータとしてインデックスが渡されるので、簡単に
fun createTestDatas(): List<String> {
val testDatas = mutableListOf<String>()
// 10回繰り返す
repeat(10) {
testDatas.add("テスト$i")
}
return testDatas.toList()
}
次に考えたいのは、MutableList
をImmutable
に変えることです。テストで使うデータとしては問題ない場合はありますが、変更する必要のないデータをそのままMutable
にしておくのはあまり良い選択ではありませんね。なので、データの作成を最初からList
にできる方法を取りたいものです。
ここでは二つの道があって、最初からサイズを指定したList
を宣言するか、ループの範囲、つまりRangeを指定する方法があります。
List
まずはサイズを指定したList
を作る方法からみていきましょう。インスタンスの作成時に、サイズと要素に対してのイニシャライザを引数として渡すことで簡単に指定したサイズ分の要素を作ることができます。例えば、上で紹介したコードはList
を使うことで以下のように変えることができます。
fun createTestDatasByList(): List<String> =
List(10) { "テスト$it" }
この方法は、実は先に紹介した方法と根本的に違うものではありません。実装としては、以下のようになっているので、Syntax sugarとして使えるということがわかります。
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
val list = ArrayList<T>(size)
repeat(size) { index -> list.add(init(index)) }
return list
}
他にもList
を使う場合は、itnit
としてどんな関数を渡すかによって、step
の設定などができるのも便利ですね。例えば以下のようなことができます。
List(5) { "Test${ it * 2 }" }
// [Test0, Test2, Test4, Test6, Test8]
List(5) { (it * 2).let { index -> "$index は偶数" } }
// [0 は偶数, 2 は偶数, 4 は偶数, 6 は偶数, 8 は偶数]
ただ、結果的に作られるList
のインスタンスはMutableList
なので、生成したデータをread-onlyにしたい場合はまたこれをtoList()
などで変換する必要があるという問題があります。
Range
では、もう一つの方法をまた試してみましょう。Kotlinでは数字の範囲を指定することだけで簡単にRange
オブジェクトを作成することができます。Range
を使う場合、上記のコードは以下のように変えられます。
// Rangeを使ってテストデータを作る
fun createTestDatasByRange(): List<String> =
(0..10).map { "テスト%it" }
List
の時とは違って、Range
にはIntRangeやLongRange、CharRangeなどがあり、引数の数字や文字を調整することで簡単にアレンジができるということも良いです。
また、一般的に性能はList
よりRange
の方が良いようです。以下のようなコードでベンチマークした際、大抵Range
の方がList
の倍ぐらい早いのを確認できました。
import kotlin.system.measureTimeMillis
data class Person(val name: String, val Num: Int)
fun main() {
benchmark { list() }
benchmark { range() }
}
fun benchmark(function: () -> Unit) =
println(measureTimeMillis { function() })
fun list() =
List(200000) { Person("person$it", it) }
fun range(): List<Person> =
(0..200000).map { Person("person$it", it) }
一つ気にしなくてはならないのは、Range
の場合は基本的に値が1づつ増加することになっているので、for
やList
のようなstep
の条件が使えません。なので場合によってどちらを使うかは考える必要があります。
Check
Validationなどで、パラメータの値を確認しなければならない場合があります。KotlinではNullable
オブジェクトとそうでないオブジェクトが分けられているので、Javaと違って引数にnull
が渡される場合はコンパイルエラーとなりますが、ビジネスロジックによってはそれ以外のことをチェックする必要もあり、自前のチェックをコードで書くしかないです。
まず、お馴染みのJavaのやり方を踏襲してみると、以下のようなコードを書くことができるでしょう。関数の引数と、その戻り値のチェックが含まれている例です。
fun doSomething(parameter: String): String {
if (parameter.isBlank()) {
throw IllegalArgumentException("文字列が空です")
}
val result = someRepository.find(parameter)
if (result == null) {
throw IllegalStateException("結果がnullです")
}
return result
}
ここで少し違う言語の例をみていきたいと思います。Kotlinとよく似ていると言われているSwiftの場合、ここでGuard Statementを使うのが一般的のようです。チェックのための表現が存在することで、ビジネスロジックとチェックが分離されるのが良いですね。Swiftをあまり触ったことがないので良い例にはなっていないかもしれませんが、イメージ的には以下のようなコードになります。
func doSomething(parameter: String) throws -> String {
guard !parameter.isEmpty else {
throw ValidationError.invalidArgument
}
guard let result = someRepository.find(parameter) else {
throw ValidationError.notFound
}
return result
}
同じく、Kotlinでもチェックのための表現とビジネスロジックが分離できれば、コードの意味がより明確になるはずです。Kotlinではどうやってそれを実現できるのでしょうか。例えば以下のようなことを考えられます。
fun doSomething(parameter: String?): String {
val checkedParameter = requireNotNull(parameter) {
"文字列がnullです"
}
val result = someRepository.find(checkedParameter)
return checkNotNull(result) {
"結果がnullです"
}
}
requireNotNullは、渡された引数がnull
である場合はIllegalArgumentException
を投げ、そうでない場合は引数をnon-null
タイプとして返します、明確にnull
チェックをしていることが解るだけでなく、以降チェックがいらないので便利です。また、lazy message
としてIllegalArgumentException
が発生した時のメッセージを指定できるのも良いですね。
checkNotNullの場合も機能的にはrequireNotNull
と変わらないですが、null
の場合に投げる例外がIllegalStateException
となります。なので、用途に合わせてこの二つを分けて使えますね。
他に使えるものとしてはrequireがあります。こちらは条件式を渡すことで、null
チェック以外のこともできます。なので、以下のコードのように、Int
型のデータに対して範囲をチェックするということもできるようになります。
fun doSomething(parameter: Int) {
require(parameter > 100) {
"$parameterは大きすぎます"
}
// ...
}
他にも、Elvis operatorを使う方法もありますね。この場合は、null
の場合にただ例外を投げるだけでなく、代替となる処理を書くことができますので色々と活用できる余地があります。例えば以下のようなことができますね。
fun doSomething(parameter: String?): String {
val checkedParameter = parameter ?: "default"
val result = someRepository.find(checkedParameter)
return result ?: throw CustomException("結果がnullです")
}
Listの分割
とある条件と一致するデータをListから抽出したい場合は、filter
のようなoperationを使うことでできます。しかし、条件が二つだとどうすればいいでしょうか。正確には、一つのリストに対して、指定した条件に一致する要素とそうでない要素の二つのリストに分離したい場合です。
こういう場合はとりあえず下記のように2回ループさせる方法があると思いますが、これはあまり効率がよくないです。
val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 奇数を抽出
val odd = origin.filter { it % 2 != 0 }
// 偶数を抽出
val even = origin.filter { it % 2 == 0 }
ループを減らすためには、あらかじめ宣言したリストに対してループの中で分岐処理を行うという方法があるでしょう。例えば以下のようにです。
val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 奇数と偶数のリストを宣言しておく
val odd = mutableListOf<Int>()
val even = mutableListOf<Int>()
// ループ処理
origin.forEach {
if (it % 2 != 0) {
odd.add(it) // 奇数のリストに追加
} else {
even.add(it) // 偶数のリストに追加
}
}
幸い、この状況にぴったりな方法をKotlinのスタンダードライブラリが提供しています。partitionというoperationです。このopreationを使うと、元のリストの要素を条件に一致するものとそうでないもので分割してくれます。
また、partition
戻り値はPair<List<T>, List<T>>
なので、destructuring-declarationと組み合わせることでかなり短いコードになります。実際のコードは以下のようになるりますが、かなりスマートですね。
val origin = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (odd, even) = origin.partition { it % 2 != 0 } // 条件に一致するものと一致しないものでリストを分離
最後に
Kotlinは便利ではありますが、言語自体が提供する便利さ(機能)が多いゆえに、APIの使い方を正しく活用できるかどうかでコードのクォリティが左右される部分が他の言語と比べ多いような気がしています。さらにバージョンアップも早く、次々と機能が追加されるのでキャッチアップも大事ですね。
でも確かに一つづつKotlinでできることを工夫するうちに、色々とできることが増えていく気もしていますね。研究すればするほど力になる言語を使うということは嬉しいことです。ということで、これからもKotlinで書いてみたシリーズは続きます。
では、また!