Null safety in legacy Java libraries used in Kotlin projects
Asked Answered
W

3

7

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?

Whitewall answered 14/6, 2017 at 17:30 Comment(0)
D
10

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) { ... }
}
Diet answered 14/6, 2017 at 17:38 Comment(0)
O
5

Whenever the Kotlin compiler does not know what the nullability of a type is, the type becomes a platform type, denoted with a single !:

public String foo1() { ... }
@NotNull public String foo2() { ... }
@Nullable public String foo3() { ... }

val a = foo1() // Type of a is "String!"
val b = foo2() // Type of b is "String"
val c = foo3() // Type of c is "String?"

This means a much as, "I don't know what the type is, you may need to check it".

The Kotlin compiler does not enforce null-checking on these types, because it may be unnecessary:

Any reference in Java may be null, which makes Kotlin's requirements of strict null-safety impractical for objects coming from Java. (...) When we call methods on variables of platform types, Kotlin does not issue nullability errors at compile time, but the call may fail at runtime, because of a null-pointer exception or an assertion that Kotlin generates to prevent nulls from propagating:

val item = list[0] // platform type inferred (ordinary Java object)
item.substring(1) // allowed, may throw an exception if item == null
Ostraw answered 14/6, 2017 at 17:34 Comment(0)
S
0

Java code can pass information about nullability using annotations:

  • @Nullable String -> seen as String? in Kotlin

  • @NotNull String -> seen as String in Kotlin

When annotations aren't present, Java type becomes a platform type in Kotlin. A platform type is a type for which Kotlin doesn't have nullability information - you can treat it as nullable or non-null type. This means you carry the full responsibility for the operations you perform with this type (just as in Java). Compiler will allow all operations. Just as in Java, you'll get a NPE if you perform a not null-safe operation on a null value.

We could avoid null checks if Kotlin would treat all incoming values from Java as nullable, but we would end up with large amount of redundant null checks for values that can never be null, hence Kotlin designers came up with platform types.

Note that you can't declare platform types in Kotlin, they can only come from Java. String! notation is how the Kotlin compiler denotes platform types, it emphasises that the nullability of the type is unknown. You can't use this syntax in your Kotlin code.

Depending on how you want to handle the possible null value, you can use the following operators:

  • String -> calling a not null-safe operation with an argument that might be null isn't allowed and will be flagged by compiler

  • String? -> the set of operations you can perform on it is restricted by compiler and in case you wan't to pass a nullable value, you're forced to deal with it (compare it will null value -> compiler will remember that and treat the value as non-null in the scope)

  • String?. -> (Safe-Call Operator) if the value on which you're trying to call the method isn't null, the method is executed normally, else call is skipped and null is returned

  • String?: -> (Elvis Operator, also Non-Coalescing Operator) this operator takes two values, and its result is the first value if it isn't null or second if the first one is null

  • String!! -> (Non-Null Assertion) - for null values an exception is thrown

  • ?.let -> (Let Function together with Safe-Call Operator) - Let Function is called only for non-null values, else nothing happens

Sweatshop answered 25/1, 2020 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.