Playing with null-safety: Is this an error?


#1

Hello,

I’m playing a bit with Hazelcast at the moment using Kotlin 8.11:

val cfg = Config(); val instance = Hazelcast.newHazelcastInstance(cfg); val mapCustomers: IMap<Int, String>? = instance?.getMap("customers"); val myMap = mapCustomers as java.util.Map<Int, String>

if(myMap != null) {
  myMap.put(1, “Joe”);
  myMap.put(2, “Ali”);
  myMap.put(3, “Avi”);

  println("Customer with key 1: "+ myMap.get(“1”));
  println(“Map Size:” + myMap.size());
}


As the code shows in line 3 the message instance?.getMap(“customers”) returns an Hazelcast type named IMap which implements java.util.Map. To get things to compile I introduced the helper variable myMap. My first question is whether there is a more elegant way to get around having to declare that helper map myMap. Everything I tried didn’t work, f.ex.

val mapCustomers = (instance?.getMap("customers")) as HashMap<Int, String>;

But my main question concerns this piece of code:

var mapCustomers: IMap<Int, String>? = instance?.getMap("customers"); mapCustomers = null; val myMap = mapCustomers as java.util.Map<Int, String>

The code above compiles and runs and of course creates a kotlin.TypeCastException in the 3rd line. My question is whether the compiler shouldn't have complained that it must be

?

instead of

mapCustomers as java.util.Map<Int, String>  // no question mark

because it knows that mapCustomers in the 1st line was declared with an ?

Regards, Oliver


#2

To the bulk of the post:

Casting to java.util.Map is very strongly discouraged. You could probably go for kotlin.MutableMap, couldn’t you?

And why would you cast at all? What exactly doesn’t work with IMap as it is?

To the second question: since “as” means a downcast, it’s perfectly legitimate to narrow down a nullable type to a non-null one, i.e. in many case that may be your actual intention, so a warning would be incorrect.


#3

But my main question concerns this piece of code:

var mapCustomers: IMap<Int, String>? = instance?.getMap("customers"); mapCustomers = null; val myMap = mapCustomers as java.util.Map<Int, String>

The code above compiles and runs and of course creates a kotlin.TypeCastException in the 3rd line. My question is whether the compiler shouldn't have complained that it must be

?

instead of

mapCustomers as java.util.Map<Int, String>  // no question mark

because it knows that mapCustomers in the 1st line was declared with an ?

I guess, that's exactly like in Java: Any cast is legal as long as the compiler can't proof that it must always fail. In this case it may succeed. It could fail for two reasons:

  • mapCustomers being an incompatible instance
  • mapCustomers being null
Given the declaration, none of the cases is sure. This brings me to two things:
  1. Given the statement

mapCustomers = null;

Kotlin could prove that the cast must fail. It does similar things in if-blocks

  1. Unfortunately, casting allows changing multiple things at once: the exact type, nullability, generics(?), whatever. This may lead to casting more than expected (as you did). There’s already a specialized “null cast”, namely the !! operator. Maybe there could be a cast allowing nothing but changing the erased type itself. I mean something like

var mapCustomers: IMap<Int, String>? = instance?.getMap(“customers”);
val myMap = mapCustomers restricted_as java.util.Map<…>?

You wouldn’t have to repeat all the generics and you couldn’t cast away nullability by mistake.


#4

To the second question: since "as" means a downcast, it's perfectly legitimate to narrow down a nullable type to a non-null one, i.e. in many case that may be your actual intention, so a warning would be incorrect.

I see. Thanks for the information.

And why would you cast at all? What exactly doesn't work with IMap as it is?

The problem is that this here does not work:

val mapCustomers  = instance?.getMap("customers");  // type inference failed

I need to mention that the hazelcast type IMap is a proxy to a remote map. So I can't copy its entries into some local map. I tried various things that all result in compiler errors:

// uses kotlin.Map val mapCustomers: kotlin.Map<Int, String>? = instance?.getMap("customers"); val myMap = mapCustomers as java.util.Map<Int, String>

if(mapCustomers != null) {  // compiler error: resolution ambiguity
  myMap.put(1, “Joe”);
}

val instance = Hazelcast.newHazelcastInstance(cfg);

// uses java.util.Map
val mapCustomers: java.util.Map<Int, String>? = instance?.getMap(“customers”); // compiler error type mismatch

val instance = Hazelcast.newHazelcastInstance(cfg);

// uses com.hazelcast.core.IMap
val mapCustomers: IMap<Int, String>? = instance?.getMap(“customers”); // compiler error type mismatch

Error:Kotlin: The following Java entities have annotations with wrong Kotlin signatures:
com.hazelcast.core.IMap V put(K, V):
Incompatible types in superclasses: [K?, K]
Incompatible types in superclasses: [V?, V]

val mapCustomers: MutableMap<Int, String>? = instance?.getMap(“customers”); // compiler error type mismatch

  &nbsp;&nbsp;if(mapCustomers != null) {

           mapCustomers.put(1, “Joe”);
           System.out.println("Customer with key 1: "+ mapCustomers.get(1));
  }

val instance = Hazelcast.newHazelcastInstance(cfg);

// uses com.hazelcast.core.IMap
val mapCustomers: IMap<Int, String>? = instance?.getMap(“customers”); // compiler error type mismatch

if(mapCustomers != null) {
  mapCustomers.put(1, “Joe”);
}


So as I was at my wits’ end resorted to casting. I’ve attached the IntelliJ Kotlin project files. Maybe this helps to better understand the compiler errors.
Any hint to find a clean solution appreciated :-).

Regards, Oliver



Hazelcast.zip (15.6 KB)

#5

I should have mentioned that com.hazelcast.core.IMap implements java.util.Map. In that way it shouldn't be hard, but I can't get things to compile without the bloody cast ... ;-)


#6

The respective Java code is this:

Config cfg = new Config(); HazelcastInstance instance = Hazelcast.newHazelcastInstance(cfg); Map<Integer, String> mapCustomers = instance.getMap("customers"); mapCustomers.put(1, "Joe");

The corresponding Scala code compiles and runs:

val cfg = new Config(); val instance = Hazelcast.newHazelcastInstance(cfg) val mapCustomers: IMap[Integer, String] = instance.getMap("customers") mapCustomers.put(1, "Joe") println("Customer with key 1: "+ mapCustomers.get(1))

This is the Kotlin code in the same way

val cfg = Config(); val instance = Hazelcast.newHazelcastInstance(cfg); val mapCustomers: IMap<Int, String>? = instance?.getMap("customers");

if(mapCustomers != null) {
  mapCustomers.put(1, “Joe”);
  println("Customer with key 1: "+ mapCustomers.get(1));
}


The IDE in the Kotlin code above shows no error (IntelliJ 13.1.3 UE). When I run the compiler I get this error, though:

Error:Kotlin: The following Java entities have annotations with wrong Kotlin signatures:
com.hazelcast.core.IMap V put(K, V):
Incompatible types in superclasses: [K?, K]
Incompatible types in superclasses: [V?, V]


Maybe this is now the essence of the problem … ;-).

In both, Scala and Kotlin this compiles:

val mapCustomers: IMap[Integer, String] = instance.getMap("customers") // Scala val mapCustomers: IMap<Int, String>? = instance?.getMap("customers"); // Kotlin

But again for both, Scala and Kotlin, declaring java.util.Map instead of IMap does not compile, although it works in Java. Strange.


#7

This works fine for me:

 
val mapCustomers = instance?.getMap<Int, String>("customers");

if(mapCustomers != null) {  // compiler error: resolution ambiguity
  mapCustomers.put(1, “Joe”);
}

You don't have to rely on type inference, it's OK to pass type parameters explicitly.

Now, regarding the kotlin signatures error: looks like a bug on our side :frowning:
Compiles fine in the super-bleeding edge version 0.8.1527 (using platform types instead of annotations)


#8

 

val mapCustomers = instance?.getMap<Int, String>(“customers”);


I should have checked out the method signature … Thanks for the hint.

Now, regarding the kotlin signatures error: looks like a bug on our side Compiles fine in the super-bleeding edge version 0.8.1527 (using platform types instead of annotations)

I see, I will wait till M9 has arrived.


#9

I forgot to drop a note that the following assignments don't compile with 8.11:

val map1 = kotlin.MutableMap<Int, String>() // Error:(11, 28) Kotlin: Unresolved reference: MutableMap

val map2: java.util.HashMap<Int, String> = hashMapOf() // compiles

// Error:(15, 48) Kotlin: Type mismatch: inferred type is java.util.HashMap<kotlin.Int, kotlin.String> but java.util.Map<kotlin.Int, kotlin.String> was expected
val map4: java.util.Map<Int, String> = hashMapOf<Int, String>()

// Error:(18, 48) Kotlin: Type mismatch: inferred type is java.util.HashMap<kotlin.Int, kotlin.String> but java.util.Map<kotlin.Int, kotlin.String> was expected
val map5: java.util.Map<Int, String> = HashMap<Int, String>()


I don’t understand whether I’m just missing something or whether I should create a bug entry.


#10

None of these should compile, although the first error message is misleading: it is complaining about the constructor (that is, of course, not present for the MutableMap trait), not the trait MutableMap itself.

The compiler warns you against using java.util.Map in your Kotlin code, because Kotlin’s type mapping makes it incompatible with everything else, which you rightfully observe in the examples. Do not use this type, kotlin.Map/MutableMap should be enough.


#11

Think I got it now. This here compiles and runs:

val map: kotlin.MutableMap<Int, String> = HashMap() map.put(1, "foo")

I'm not sure it is immediately obvious to everyone that this doesn't compile:

val map: java.util.Map<Int, String> = java.util.HashMap()

Well, for me the matter is closed. Thanks for the help :-).