A major reason for the popularity of JVM languages is the ability to interoperate with Java. This enables you to use the vast ecosystem of Java libraries and frameworks in the language of your choice, such as Kotlin. However, one of the challenges of using Kotlin in a Java environment is dealing with nullability. In contrast to Java, Kotlin has a strong type system that distinguishes between nullable and non-nullable types. This is a huge boon for Kotlin developers, as it helps to already prevent null pointer exceptions at compile time.
Let us assume we have a simple Java class (e.g. one from a Java library):
// (Java)
record SomeJavaClass(String property) {
}
and another one with SomeJavaClass
as a property:
// (Java)
public class OuterJavaClass {
private SomeJavaClass inner;
public OuterJavaClass(SomeJavaClass someClass) {
this.inner = someClass;
}
public SomeJavaClass getInner() {
return inner;
}
}
This code in Java potentially leads to a runtime exception:
// (Java)
public class Main {
public static void main(String[] args) {
OuterJavaClass outerClass = new OuterJavaClass(null);
// This throws a NullPointerException, because the result of getInner() is null
System.out.println(outerClass.getInner().property());
}
}
In Kotlin, the equivalent code would look like this:
// (Kotlin)
data class SomeKotlinClass(val property: String)
// Let's assume that inner may be null (same as in Java)
data class OuterKotlinClass(val inner: SomeKotlinClass?)
fun main() {
val outerKotlinClass = OuterKotlinClass(null)
// This would not compile, because "inner" has the nullable type 'SomeKotlinClass?'
println(outerKotlinClass.inner.property)
}
In this case, the Kotlin compiler will prevent you from calling property
on a nullable type, which is great. What would happen if we were to call the Java code from Kotlin, though?
// (Kotlin)
fun main() {
val outerJavaClass = OuterJavaClass(null)
// This will compile, and then throw a NullPointerException at runtime
println(outerJavaClass.inner.property())
}
This is because the Kotlin compiler does not know that inner
can be null, as it is a Java reference. The Kotlin documentation states:
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 in Kotlin in a specific manner and called platform types. Null-checks are relaxed for such types, so that safety guarantees for them are the same as in Java.
However, not all hope is lost: the Kotlin compiler is aware of various Java annotations that can help it infer nullability.
As per the documentation, these include (among others):
JetBrains (
@Nullable
and@NotNull
from theorg.jetbrains.annotations
package)JSR-305 (
javax.annotation
)FindBugs (
edu.umd.cs.findbugs.annotations
)Lombok (
lombok.NonNull
)
If the implicit contract in our Java code were that the property
in SomeJavaClass
is never null, and that the inner
property in OuterJavaClass
might be null, we could annotate the Java code with the appropriate annotations:
// (Java)
import javax.annotation.Nonnull;
record SomeJavaClass(@Nonnull String property) {}
and
// (Java)
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class OuterJavaClass {
@Nullable
private SomeJavaClass inner;
public OuterJavaClass(@Nullable SomeJavaClass someClass) {
this.inner = someClass;
}
@Nullable
public SomeJavaClass getInner() {
return inner;
}
}
This way, the Kotlin compiler will be able to infer the nullability of the properties correctly:
// (Kotlin)
fun main() {
val outerJavaClass = OuterJavaClass(null)
// This will now fail to compile, instead of throwing a NullPointerException at runtime
println(outerJavaClass.inner.property())
}
This way, we have two advantages:
- We can use the Java code in Kotlin without worrying about nullability issues.
- We have additional information that is useful even if accessing the Java code from Java.
Especially if you provide a Java library, you can drastically improve the quality of life for anyone accessing it from Kotlin, if you annotate your Java code properly.
We could be happy now, but there is one final catch (which I recently encountered in a project): the Kotlin compiler only infers nullability for the Java property from the getter method. Even though Kotlin provides syntactic sugar for accessing Java properties via their getter methods, it does not directly access the property itself:
// (Kotlin)
val outerJavaClass = OuterJavaClass(SomeJavaClass("foo"))
// This looks like a property access:
println(outerJavaClass.inner.property)
// But it is actually a method call:
println(outerJavaClass.getInner().property())
This means that Kotlin ignores the annotation on the property itself. Therefore, if you have a Java property that is annotated with @Nullable
, but the getter method is not, the Kotlin compiler will treat it as a platform type, and you won’t get the advantages of nullability checking. So always ensure that the getter method is annotated properly, as well.
Photo credit: Andrew Kenney
Kommentare