问题
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?
回答1:
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) { ... }
}
回答2:
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
回答3:
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
来源:https://stackoverflow.com/questions/44551177/null-safety-in-legacy-java-libraries-used-in-kotlin-projects