KotlinのNon-Null型にnullを代入する方法

Kotlinではnullを扱いやすくするためにNullable、Non-Nullを型で制限することが出来ます。 KotlinのNon-Null型に対してnullを代入しようとすると、代入するタイミングで例外を吐きます。 JavaとKotlinを一緒に使っていると、この例外に遭遇することがあると思います。

具体的には以下の挙動をします。

public class Hoge {
    // nullを返すメソッド
    static String getName() {
        return null;
    }
}
val d: String = Hoge.getName() // ここで例外が投げられる
println(d.length) // これは実行されない

このコードは、val d: String = Hoge.getName()IllegalStateExceptionを投げます。 Non-Null型にnullを代入しようとしているからです。

次に、代入するタイミングで例外を投げなくする方法を紹介します。

具体的には以下のコードで達成できます。

fun <T> castNull(): T = null as T


val d: String = castNull() // ここでは例外が投げられない
println(d.length) // ここで例外が投げられる

なぜこのような挙動になるのかを説明します。まず castNull()メソッドに定義されたジェネリック型TはAny?をupperに持ちます。 Kotlinはジェネリックを使い、かつNullable型をupperに持つとき、nullかどうかのチェックをしません。これはバイトコードを見れば分かります。

GETSTATIC sample/SampleTestsJVMKt.a : Ljava/lang/String;
CHECKCAST java/lang/Object

CHECKCASTしかしておらず、nullかどうかのチェックをしていません。 なので、val d: String = castNull()ではnullチェックがされないため例外が投げられず、実際にメソッドをコールするprintln(d.length)のタイミングで例外が投げられます。

Non-Null型であるStringにnullを代入することが出来ました。

次に、fun <T: Any> castNull(): T = null as Tと、Non-Null型をupperにした場合のバイトコードは次になります。

GETSTATIC sample/SampleTestsJVMKt.a : Ljava/lang/String;
DUP
IFNONNULL L1
NEW kotlin/TypeCastException
DUP
LDC "null cannot be cast to non-null type T"
INVOKESPECIAL kotlin/TypeCastException.<init> (Ljava/lang/String;)V

こちらのバイトコードには、nullチェックが入っていることが分かります。nullチェックをしているため、val d: String = castNull()のタイミングで例外が投げられます。

Nullableな型をupperに持つ、持たないでnullチェックをするかどうかが決まる、という話でした。

この挙動は https://youtrack.jetbrains.com/issue/KT-8135 ISSUEにバグとして報告されているので、今後異なるバイトコードが生成される可能性があります。 もし、この挙動に依存するようなコードを書いている場合は注意が必要です。

実世界では、mockito-kotlinがこの挙動に依存しているので、より興味がある方はぜひ読んでください😃 ここのあたりで使っています

参考

Written by
あんどろいどでぃべろっぱぁー🍎