Search code examples
javaannotationskotlinkotlin-interopkotlin-null-safety

Null safety in legacy Java libraries used in Kotlin projects


Let's say I have particular code in old/legacy Java library:

public class JavaClass {
    private String notNullString;
    private String nullableString;
    private String unannotatedString;

    public JavaClass(@NotNull String notNullString,
                     @Nullable String nullableString,
                     String unannotatedString) {

        this.notNullString = notNullString;
        this.nullableString = nullableString;
        this.unannotatedString = unannotatedString;
    }

    @NotNull
    public String getNotNullString() {
        return notNullString;
    }

    @Nullable
    public String getNullableString() {
        return nullableString;
    }

    public String getUnannotatedString() {
        return unannotatedString;
    }
}

The first two parameters are properly annotated with @NotNull and @Nullable annotations (using jetbrains.annotations). The third one (unnanotatedString) is left without proper annotation.

When I use this class in my Kotlin code and set all the constructor arguments to non-null values, everything is fine:

val foo = JavaClass("first string", "second string", "third string")

println("Value1: ${foo.notNullString.length}")
println("Value2: ${foo.nullableString?.length}")
println("Value3: ${foo.unannotatedString.length}")

The first value is non-null so I can access it without a safe call. Second value and I need to use safe call (nullableString?.length), if not, I have a compile-time error, so far so good. On the third value (unannotatedString) I can use it without a safe call, it compiles fine.

But when I set the third parameter to "null" I don't get a compile-time error (no safe call required, only runtime NullPointerException:

val bar = JavaClass("first string", "second string", null)

println("Value4: ${bar.unannotatedString.length}") // throws NPE

Is that expected behaviour? Is Kotlin's compiler treating not annotated Java methods same as the ones annotated with @NotNull?


Solution

  • The type of that variable from Kotlin's view will be String!, which is a platform type.

    They initially made every variable coming from Java nullable, but they changed that decision later during the design of the language, because it required too much null handling and required too many safe calls that cluttered the code.

    Instead, it's up to you to assess whether an object coming from Java might be null, and mark their type accordingly. The compiler doesn't enforce null safety for these objects.


    As an additional example, if you're overriding a method from Java, the parameters will be platform types yet again, and it's up to you whether you mark them nullable or not. If you have this Java interface:

    interface Foo {
        void bar(Bar bar);
    }
    

    Then these are both valid implementations of it in Kotlin:

    class A : Foo {
        fun bar(bar: Bar?) { ... }
    }
    
    class B : Foo {
        fun bar(bar: Bar) { ... }
    }