Kotlinのdefinitely non-nullable typesを試す

Kotlinのdefinitely non-nullable typesを試す

Kotlinのdefinitely non-nullable typesを試してみたメモです。

definitely non-nullable types

Kotlin 1.7.0からdefinitely non-nullable typesがstableになりました。

What's new in Kotlin 1.7.0 | Kotlin

元々どのような問題があって、それをどう解決しているのかを調べつつ試してみます。

下記のissueとproposalを参考にしています。
https://youtrack.jetbrains.com/issue/KT-26245
https://github.com/Kotlin/KEEP/blob/c72601cf35c1e95a541bb4b230edb474a6d1d1a8/proposals/definitely-non-nullable-types.md
https://youtrack.jetbrains.com/issue/KT-36770
https://github.com/Kotlin/KEEP/issues/268

 
 
下記バージョンで試してみます。

  • kotlin 1.7.0

Background

kotlinでは下記のように型変数としてTを指定すると、デフォルトの上限(Upper bounds)がAny?のため、T?を指定したのと同じことになります。

fun <T> printT(t: T) {
    println(t)
}

Stringを指定するとabcは指定できますが、nullはエラーになります。
String?を指定すると当然nullも指定できます。

fun main() {
    printT<String>("abc")
    // error
    // printT<String>(null)

    printT<String?>(null)
}

ジェネリクスの型変数でnullableかnon nullかを指定できるようにしたいです。

Problems

上記のprintTのようにほとんどのケースでは型推論が働くので、明示的に型を指定する必要はありません。

fun main() {
    printT("abc")
    printT(123)
    printT(null)
}

特に問題となるケースとしては、Javaでnon nullとしてアノテートされた下記のようなインターフェースを
kotlinで継承または実装した場合です。

public interface JBox {
    <T> void put(@NotNull T t);
}

上記の例では@NotNullでnon nullであることを明示していますが、
kotlinではTはnullableなのでnullが許容されてしまいます。

Proposal

言語の初期設計時であれば、
- T -> non nullの型
- T? -> nullableの型
のように設計できたかもしれませんが、kotlinはすでにstable versionになっているので、
新しい方法を導入する必要があります。

下記のdiscussionを見ると、T!!T & Anyなどが提案されてましたが最終的にT & Anyが採用されたようです。
https://github.com/Kotlin/KEEP/issues/268
https://youtrack.jetbrains.com/issue/KT-26245

これはintersection type(インターセクション型、交差型)というようです。
https://en.wikipedia.org/wiki/Intersection_type

Example

実際に試してみます。

下記のjavaのインターフェースを定義します。
putの引数とgetの戻り値には@NotNullをつけてます。

JavaInterface.java

import org.jetbrains.annotations.NotNull;

public interface JavaInterface<T> {
    void put(@NotNull T value);
    @NotNull T get(int index);
}

デフォルト

JavaInterfaceを素直にkotlinのクラスで実装してみます。
このTはJavaInterfaceで@NotNullでアノテートされているにもかかわらずnullableです。

JavaInterfaceImpl.kt

class JavaInterfaceImpl<T> : JavaInterface<T> {
    private val list = mutableListOf<T>()

    override fun put(value: T) {
        list.add(value)
    }

    override fun get(index: Int): T {
        return list[index]
    }
}

下記のように型パラメータにString?を指定してnullをputできます。

Main.kt

fun main(){
    val implString = JavaInterfaceImpl<String>()
    implString.put("abc")
    implString.put("")
    // error
    // implString.put(null)

    val implStringNullable = JavaInterfaceImpl<String?>()
    implStringNullable.put("abc")
    implStringNullable.put("")
    implStringNullable.put(null)
}

上限(Upper Bounds)

今度は上限にAnyを指定して実装してみます。

JavaInterfaceImplUsingUpperBounds.kt

class JavaInterfaceImplUsingUpperBounds<T : Any> : JavaInterface<T> {
    private val list = mutableListOf<T>()

    override fun put(value: T) {
        list.add(value)
    }

    override fun get(index :Int): T {
        return list[index]
    }
}

この場合、上限がnon nullなので、Stringは指定できますがString?は指定できません。
しかし型変数はTではなく上限付きのT : Anyとなってしまいます。(それで問題ない場合やその方が好ましい場合も多いと思います)

Main.kt

fun main() {
    val implString = JavaInterfaceImplUsingUpperBounds<String>()
    implString.put("abc")
    implString.put("")
    // error
    // implString.put(null)

    // error
    // val implStringNullable = JavaInterfaceImplUsingUpperBounds<String?>()
}

definitely non-nullable types

1.7.0から導入されたdefinitely non-nullable typesを指定してみます。
putgetの引数と戻り値をT & Anyにします。

JavaInterfaceImplUsingIntersection.kt

class JavaInterfaceImplUsingIntersection<T> : JavaInterface<T> {
    private val list = mutableListOf<T>()

    override fun put(value: T & Any) {
        list.add(value)
    }

    override fun get(index: Int): T & Any {
        // error 
        // return list[index]
        return list[index]!!
    }
}

型変数はTなので型パラメータはStringString?も指定可能ですが、メソッドではT & Anyとして定義しているので、
nullをputすることはできません。

Main.kt

fun main() {
    val implString = JavaInterfaceImplUsingIntersection<String>()
    implString.put("abc")
    implString.put("")
    // error
    // implString.put(null)

    val implStringNullable = JavaInterfaceImplUsingIntersection<String?>()
    implStringNullable.put("abc")
    implStringNullable.put("")
    // error
    // implStringNullable.put(null)
}

型変数がTなので、下記のようにnullを引数で取るメソッドや、nullを返すメソッドも定義できます。

JavaInterfaceImplUsingIntersection.kt

fun main() {
    fun putNullable(value: T) {
        list.add(value)
    }

    fun getNullable(index: Int): T {
        return list[index]
    }
}

nullを引数で取るメソッドや、nullを返すメソッドを利用してみます。

Main.kt

fun main() {
    val implStringNullable = JavaInterfaceImplUsingIntersection<String?>()
    implStringNullable.put("abc")
    implStringNullable.put("")
    // error
    // implStringNullable.put(null)

    println("0: " + implStringNullable.get(0))
    println("1: " + implStringNullable.get(1))

    implStringNullable.putNullable(null)
    // error: NullPointerException
    // println("2: " + implStringNullable.get(2))
    println("2: " + implStringNullable.getNullable(2))
}

実行

0: abc
1: 
2: null

サンプルコードは下記にあげました。

github.com

おわり。

【参考】
https://kotlinlang.org/docs/whatsnew17.html#stable-definitely-non-nullable-types
https://youtrack.jetbrains.com/issue/KT-26245
https://github.com/Kotlin/KEEP/blob/c72601cf35c1e95a541bb4b230edb474a6d1d1a8/proposals/definitely-non-nullable-types.md
https://youtrack.jetbrains.com/issue/KT-36770
https://github.com/Kotlin/KEEP/issues/268