8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kotlin 1.9 新機能解説(抄)

Last updated at Posted at 2023-07-30

Kotlin 1.9 がリリースされました。

本稿では Kotlin 1.9 で stable になった、言語仕様および Kotlin/JVM で使える標準ライブラリーの API について、紹介・説明します。

Enumentries プロパティ

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 とよく似ていますが、
そのパス自身ではなく親ディレクトリーまでを生成します。
返値は、生成対象である親ディレクトリーではなく、レシーバーであるパス自身です。

/以上

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?