Reputation: 5157
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
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()
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
<type>!
, for example String!
- which we can take to mean String or String?
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 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...
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
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?
.
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...
Now node.get("")
will show an warning.
This annotation isn't visible to the Kotlin compiler, so it can only be a warning - not a compilation error.
Upvotes: 5
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