Search code examples
kotlinkotlin-null-safety

Null checks not inserted for reified type when param is non-nullable


TL;DR Should functions with reified types take account of type-parameter nullability when generating code?

Test case

Consider the following Kotlin code; the only difference between the two methods is whether the type bound is nullable (Any?) or not (Any).

@Test
fun testNonNullableBound() {
    val x: Int = nonNullableBound()
}

@Test
fun testNullableBound() {
    val x: Int = nullableBound()
}

private inline fun <reified T : Any> nonNullableBound(): T {
    return unsafeMethod()
}

private inline fun <reified T : Any?> nullableBound(): T {
    return unsafeMethod()
}

where unsafeMethod subverts the type system by being defined in Java:

public static <T> T unsafeMethod() { return null; }

This is Kotlin 1.1.4.

Expected behaviour

I'd expect these to behave equivalently - the type is reified, so the actual value of T is known to be non-nullable, so a null check ought to be applied inside the function before the return statement.

Observed behaviour

The two cases fail in different ways:

  • testNonNullableBound behaves as expected (fails due to a null check on the value returned by unsafeMethod()).
  • testNullableBound doesn't behave as expected - it fails with an NPE when doing the assignment to x.

So it appears the insertion of null checks is based on the type bound, rather than the actual type.

Analysis

For reference, the relevant bytecode is as follows. Note the null check added in testNonNullableBound.

testNonNullableBound

public final testNonNullableBound()V
  @Lorg/junit/Test;()
   [...]
   L1
    LINENUMBER 28 L1
    INVOKESTATIC JavaStuff.unsafeMethod ()Ljava/lang/Object;
    DUP
    LDC "unsafeMethod()"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   [...]

testNullableBound

public final testNullableBound()V
  @Lorg/junit/Test;()
   [...]
   L1
    LINENUMBER 27 L1
    INVOKESTATIC JavaStuff.unsafeMethod ()Ljava/lang/Object;
   [...]

Solution

  • So it appears the insertion of null checks is based on the type bound, rather than the actual type.

    Exactly right, this is how it is supposed to work!

    A JVM function cannot have different byte code depending on the actual generic type, so the same implementation has to satisfy all possible outcomes.

    Inlining doesn't affect this case because it follows the same rules as normal functions. It was designed in this way so that the developer is less surprised.