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を指定してみます。
putとgetの引数と戻り値を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なので型パラメータはStringもString?も指定可能ですが、メソッドでは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
サンプルコードは下記にあげました。
おわり。
【参考】
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