Vadym Chekan
Vadym Chekan

Reputation: 5157

What are nullable rules when calling Java from Kotlin

Why does Kotlin in one case infer type returned from Java to be nullable and in another case it is can be either, nullable or non-nullable?

I've checked both HashMap.get and JsonNode.get and I could not identify any @NotNull-like annotations neither in calsses nor anywhere in inheritance chain. What makes Kotlin treating those 2 calls differently?

I have read documentation https://kotlinlang.org/docs/java-interop.html#null-safety-and-platform-types but it explanation use "Platform Types" without explaining what those are and it does not explain differences in behavior anyway.

import com.fasterxml.jackson.databind.JsonNode

private fun docType(node: JsonNode, map: java.util.HashMap<String,String>) {
    val x: JsonNode = node.get("doc_type")  // DOES compile and can throw NPE at runtime
    val y: JsonNode? = node.get("doc_type") // DOES compile and Kotlin's type system will force you to check for null
    val z: String = map.get("a")            // ERROR: Type mismatch: inferred type is String? but String was expected
}

Upvotes: 3

Views: 1222

Answers (2)

aSemy
aSemy

Reputation: 7139

Kotlin provides seamless interoperability with Java, without compromising its own null-safety... almost. One exception is that Kotlin assumes that all types that are defined in Java are not-null.

To understand, let's look at JsonNode.get()

Platform types

public JsonNode get(String fieldName) { return null; }

Note that JsonNode is defined in Java, and is therefore a platform type - and Kotlin does not 'translate' it to JsonNode?, even though that would be technically correct (because in Java all types are nullable).

When calling Java from Kotlin, for convenience it's assumed that platform types are non-nullable. If this wasn't the case, you would always have to check that any instance of any platform type is not null.

So, to answer your question about what a 'platform type' is, it's a term that means

  • some type that is defined in an external target language,
  • you can't mention it explicitly in Kotlin code (but there's probably a synonymous Kotlin equivalent),
  • and we're going to assume that it's non-nullable for convenience.
  • Also the notation is <type>!, for example String! - which we can take to mean String or String?

Nullability annotations

The closest Java equivalent of Kotlin's nullable ? symbol are nullability annotations, which the Kotlin compiler can parse and take into account. However, none are used on JsonNode methods. And so Kotlin will quite happily assume that node.get("") will return JsonNode, not JsonNode?.

As you noted, there are none defined for HashMap.get(...).

So how does Kotlin know that map.get("a") returns a nullable type?

Type inference

Type inference can't help. The (Java) method signature

public V get(Object key) {
  //...
}

indicates that a HashMap<String, String> should return String, not String?. Something else must be going on...

Mapped types

For most Java types, Kotlin will just use the definition as provided. But for some, Kotlin decides to treat them specially, and completely replace the Java definition with its own version.

You can see the list of mapped types in the docs. And while HashMap isn't in there, Map is. And so, when we're writing Kotlin code, HashMap doesn't inherit from java.util.Map - because it's mapped to kotlin.collections.Map

Aside: in fact if you try and use java.util.Map you'll get a warning code that uses java.util.Map with an IntelliJ warning: This class shouldn't be used in Kotlin. Use kotlin.collections.Map or kotlin.collections.MutableMap instead.

So if we look at the code for the get function that kotlin.collections.Map defines, we can see that it returns a nullable value type

/**
* Returns the value corresponding to the given [key], or `null` if such a key is not present in the map.
*/
public operator fun get(key: K): V?

And so the Kotlin compiler can look at HashMap.get(...) and deduce that, because it's implementing kotlin.collections.Map.get(...), the returned value must be a nullable value, which in our case is String?.

Workaround: External annotations

For whatever reason, Jackson doesn't use the nullability annotations that would solve this problem. Fortunately IntelliJ provides a workaround that, while not as strict, will provide helpful warnings: external annotations.

Once I follow the instructions...

  1. Alt+Enter → 'Annotate method...'

    enter image description here

  2. Select 'Nullable' annotation

    enter image description here

  3. Save annotations.xml

Now node.get("") will show an warning.

enter image description here

This annotation isn't visible to the Kotlin compiler, so it can only be a warning - not a compilation error.

Upvotes: 5

Silvio Mayolo
Silvio Mayolo

Reputation: 70287

java.util.HashMap.get implements the interface method java.util.Map.get. Kotlin maps some Java types to its own types internally. The full table of these mappings is available on the website. In our particular case, we see that java.util.Map gets mapped internally to kotlin.collections.Map, whose get function looks like

abstract operator fun get(key: K): V?

So as far as Kotlin is concerned, java.util.Map is just a funny name for kotlin.collections.Map, and all of the methods on java.util.Map actually have the signatures of the corresponding ones from kotlin.collections.Map (which are basically the same except with correct null annotations).

So while the first two node.get calls are Java calls and return platform types, the third one (as far as Kotlin is concerned) is actually calling a method Kotlin understands: namely, get from its own Map type. And that type has an explicit nullability annotation already available, so Kotlin can confidently say that that value can be null and needs to be checked.

Upvotes: 1

Related Questions