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

610 views Asked by At

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;
   [...]
2

There are 2 answers

5
voddan On BEST ANSWER

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.

1
crgarridos On

The problem isn't if the function is inlined or not.

From Kotlin's documentation:

Any reference in Java may be null, which makes Kotlin's requirements of strict null-safety impractical for objects coming from Java. Types of Java declarations are treated specially in Kotlin and called platform types. Null-checks are relaxed for such types, so that safety guarantees for them are the same as in Java

Using a @Nullable annotation, in java side, you can enforce kotlin to check if the type can either be null or not (using @NotNull)

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