Kotlin 1.9 がリリースされました。
本稿では Kotlin 1.9 で stable になった、言語仕様および Kotlin/JVM で使える標準ライブラリーの API について、紹介・説明します。
Enum
の entries
プロパティ
Enum
派生クラスの関数 values()
が非推奨になり、代わりにプロパティ entries
が追加されました。
enum class Sign {
PLUS,
MINUS,
}
// 非推奨になった方法
val values: Array<Sign> =
Sign.values() // 警告が出る
println(values.asList()) // > [PLUS, MINUS]
// 推奨される方法
val entries: EnumEntries<Sign> =
Sign.entries
println(entries) // > [PLUS, MINUS]
values()
関数と同様に、entries
プロパティはコンパイラによって自動実装されます。
entries
プロパティの返値型は EnumEntries<E>
であり、これは List<E>
を継承するインターフェイスです。
entris
プロパティの値に入っている要素は values()
の返値に入っている要素と変わりません。
動機
values()
関数は配列を返します。
配列は可変です。
そのため、呼び出し元で変更されてしまってもいいように、values()
関数が呼び出されるたびにコピーを作ってそれを返します。
大量に呼び出されるとそれがパフォーマンスに影響します。
これを、不変の List
を一度だけ生成し常に同じものを返すようにすることで、パフォーマンスを改善します。
また、多くの API はコレクションを使用します。
配列を受け取っても結局 List
などのコレクションに変換する必要があります。
その実装の手間や処理コストも軽減します。
既存の Enum
派生クラスではどうなるのか
コンパイル済みであったり Java で実装された Enum
派生クラスに対しても、entries
プロパティを呼び出すことができます。
これらはプロパティの呼び出し元にリストを遅延初期化するコードが埋め込まれるような仕組みで実現されます。
EnumEntries
インターフェイスを実装するクラス
EnumEntries
型は sealed interface
です。
これを継承する型は private な EnumEntriesList
クラスだけです。
このクラスでは、次の関数がオーバーライドされ、Enum.ordinal
プロパティを使って最適化されています。
contains()
indexOf()
lastIndexOf()
参考
データオブジェクト
データクラスの object
版であるデータオブジェクトを定義できるようになりました。
data object MyDataObject
データオブジェクトはデータクラスと同様に次の関数が自動的にオーバーライドされます。
toString()
equals()
hashCode()
toString()
はパッケージ名修飾なしの短いクラス名を返します。
println(MyDataObject) // > MyDataObject
// 参考
data class MyDataClass(val value: String)
println(MyDataClass("myValue")) // > MyDataClass(value=myValue)
equals()
関数は自身と同じクラスのインスタンスとの比較でのみ true
を返します。
通常、object
はシングルトンなので同じクラスのインスタンスは自身以外には存在しませんが、
リフレクションなどにより作られてしまった場合に、それとの比較でも true
を返します。
hashCode()
関数は当然 equals()
関数に合わせた挙動をします。
なおデータオブジェクトにはデータクラスにある次の関数はありません。
copy()
componentN()
動機
データクラスとの対称性のために導入されました。
特にシールドインターフェイス・シールドクラスで使用すると効果的です。
sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult
fun main() {
println(Number(7)) // Number(number=7)
println(EndOfFile) // EndOfFile
}
参考
インラインクラスのセカンダリーコンストラクターのボディ
インラインクラスのセカンダリーコンストラクターにボディを持たせられるようになりました。
@JvmInline
value class ARGB(
private val value: UInt
) {
constructor(
alpha: Int,
red: Int,
green: Int,
blue: Int,
) : this(
(alpha.toUInt() shl 8 * 3)
+ (red.toUInt() shl 8 * 2)
+ (green.toUInt() shl 8 * 1)
+ (blue.toUInt() shl 8 * 0)
) {
// ここが書けるようになった。
require(alpha in 0..0xFF)
require(red in 0..0xFF)
require(green in 0..0xFF)
require(blue in 0..0xFF)
}
}
..<
演算子
2項演算子 ..<
とそれに関連する API が追加されました。
for (i in 0..<3) {
println(i)
} // 0 1 2
..
演算子との比較
おなじみの演算子 ..
は閉区間を表します。
閉区間はその両端を区間内に含みます。
たとえば 0.0..3.0
は 0.0 から 3.0 までの区間であり、0.0 も 3.0 も区間内に含みます。
数学では [0.0, 3.0]
のように書いたりします。
新しい演算子 ..<
は右半開区間を表します。
右半開区間は左端は区間内に含みますが、右端は含みません。
たとえば 0.0..<3.0
は 0.0 から 3.0 までの区間であり、0.0 は区間内に含みますが、3.0 は含みません。
数学では [0.0, 3.0)
のように書いたりします。
..
演算子は ClosedRange
型オブジェクトを返し、
..<
演算子は新しく追加された OpenEndRange
型オブジェクトを返します。
..
演算子の関数表記は rangeTo
、
..<
演算子の関数表記は rangeUntil
です。
(関数表記は、演算子オーバーロードや、式の結合順を変えるためにあえて2項演算子ではなく関数として書くときに使います。)
演算子 | .. |
..< |
---|---|---|
区間 | 閉区間 | 右半開区間 |
区間の左端を範囲内に含むか | 含む | 含む |
区間の右端を範囲内に含むか | 含む | 含まない |
数学での表記 | [a, b] | [a, b) |
関数表記 | rangeTo |
rangeUntil |
返値型 | ClosedRange |
OpenEndRange |
動機
整数のように「1つ前」の値が明確な型の場合、半開区間を閉区間で代替することが可能です。
たとえば [0, 3)
は [0, 2]
で代替できます。
しかし実数の場合はどうでしょうか。
[0.0, 3.0)
は [0.0, 2.0]
では代替できません。(前者には 2.9 が含まれますが、後者には含まれません。)
このように開区間は閉区間では代替しきれません。
特にプログラミングでは右半開区間を使用することがよくあります。
そこで右半開区間を表す型 OpenEndRange
(と、それを便利に扱うために ..<
演算子)が必要になります。
中置関数 until
中置関数 until
は皆さんおなじみでしょう。
until
関数は ..<
演算子と同じく右半開区間を表します。
for (i in 0 until 3) {
println(i)
} // 0 1 2
for (i in 0..<3) {
println(i)
} // 0 1 2
しかしこれまでは OpenEndRange
インターフェイスがなかったため、ClosedRange
インターフェイスを実装した型(IntRange
クラスなど)を返して、閉区間で代替していました。(unitl
関数は整数型に対してのみオーバーロードされています。)
これからは返値型が OpenEndRange
インターフェイスも実装しましたので、正しく右半開区間を表すものになりました。
なお整数型の ..<
演算子は until
関数と同じ型のオブジェクトを返すようにオーバーロードされています。
until
は直観的ではないので、間違いを防ぐため、今後は ..<
演算子を使うようにしていくのがよいでしょう。
参考
時間計測 API
時間計測 API が追加されました。
コード実行時間計測
コードブロックの実行に要した時間を計測します。
measureTimedValue
関数はコードブロックが返した値と所要時間を TimedValue
型オブジェクトとして返します。
val timedValue = measureTimedValue {
// 所要時間を計測したい処理
buildList<Int> {
(2..<100).forEach { i ->
if (this.none { i % it == 0 }) {
this += i
}
}
}
}
println("duration: ${timedValue.duration}, value: ${timedValue.value}")
// ^ > duration: 24.032068ms, value: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
TimedValue
はデータクラスなので、結果を分解して受け取ることもできます。
val (value, duration) = measureTimedValue {
// ...
}
コードブロックが返す値がない場合は measureTime
関数を使います。
val duration = measureTime {
// ...
}
println("duration: $duration")
// ^ > duration: 67.798286ms
時間をマークして差を計測する
任意の瞬間の時間をマークしておき、後でそれらの間の差を求めることができます。
val timeSource = TimeSource.Monotonic
val mark1 = timeSource.markNow()
Thread.sleep(100)
val mark2 = timeSource.markNow()
Thread.sleep(200)
val mark3 = timeSource.markNow()
println("mark2 - mark1: ${mark2 - mark1}") // > mark2 - mark1: 102.062205ms
println("mark3 - mark2: ${mark3 - mark2}") // > mark3 - mark2: 205.087097ms
println("mark3 - mark1: ${mark3 - mark1}") // > mark3 - mark1: 307.149302ms
TimeSource.markNow()
関数は TimeMark
オブジェクトを返します。
TimeMark
オブジェクト同士の差を取ると、期間を表す Duration
オブジェクトが返ります。
TimeMark
オブジェクトに Duration
オブジェクトを加えることで、新しい TimeMark
オブジェクトを作ることができます。
TimeMark.hasPassedNow
関数を使えば、現在時刻がその TimeMark
オブジェクトが表す時刻を過ぎているかどうかを判定できます。
val timeSource = TimeSource.Monotonic
// 現在
val markStart: TimeMark = timeSource.markNow()
// 1秒間
val duration1Sec: Duration = 1.seconds
// 1秒後
val mark1SecAfter: TimeMark = markStart + duration1Sec
for (i in 0..<100) {
Thread.sleep(100)
// 「1秒後」の時間を過ぎていたら現在の値を出力して終了する。
if (mark1SecAfter.hasPassedNow()) {
println(i) // > 9
break
}
}
動機
以前からコードブロックの所要時間を計測する関数として measureTimeMillis
関数と measureNanoTime
関数がありました。
これらは名前が似ており、計測の精度だけが違うように見えますが、実際には仕組みが全く異なります。
measureTimeMillis
関数はコードブロックの開始時と終了時のエポックタイム(1970-01-01T00:00:00 からの時間)を記録し、その差を結果とします。
そのため計測中にシステムの日時設定が変更されると正しい経過時間を返しません。
measureNanoTime
関数はシステムが起動してからの経過時間に基づくため、正確な時間を計測できます。
この違いが混乱を招くため、またこれらはコードブロックが返した値を呼び出し元に返せないなどの不便があったため、これらを置き換える新しい API が作られました。
新しい API では measureNanoTime
と同様、システムが起動してからの経過時間に基づいた、システムの日時設定に依存しない計測を行います。
参考
親ディレクトリーを生成するためのパスユーティリティ
java.nio.file.Path
クラスに、そのパスの親ディレクトリーまでを生成する拡張関数 createParentDirectories
が追加されました。
// 既存の移動させたいファイルのパス
val srcPath = Path(".") / "text.txt"
// 移動先のパス
val dstPath = Path(".") / "grand_parent" / "parent" / "child.txt"
// 移動先のパスの親ディレクトリーまでを作って、そのディレクトリーにファイルを移動させる。
srcPath.moveTo(
dstPath.createParentDirectories()
)
既存の関数 createDirectories
とよく似ていますが、
そのパス自身ではなく親ディレクトリーまでを生成します。
返値は、生成対象である親ディレクトリーではなく、レシーバーであるパス自身です。
/以上