I deal a lot with Java’s concurrent collections, including their null
-returning methods, like poll
. Consider this (non-concurrent) example:
val q = ju.concurrent.ConcurrentLinkedQueue[Int]()
val a: Int = q.poll() // 0
val b: Any = q.poll() // null
// q.poll() == null // rejected
// null == q.poll() // rejected
// null eq q.poll() // rejected
val c = q.poll() eq null // false
My first question is: Is this the expected behavior? It all seems pretty dangerous. I would have expected the evaluation of a
to cause an NPE
, and the test q.poll() eq null
to be true. As it is, I worry about differentiating an empty queue from a queue that starts with 0.
My second question is: What are the safe usage patterns? Option
and pattern-matching seem to be fine, but I’m still nervous:
val d: Option[Int] = Option(q.poll()) // None
q.add(0)
val e: Option[Int] = Option(q.poll()) // Some(0)
def drain[A](q: ju.Queue[A]): List[A] = q.poll() match
case null => Nil
case value => value :: drain(q)
q.add(0)
val f = drain(q) // List(0)
I wouldn’t want to hit a weird case where, say, Option(q.poll())
evaluates to Some(0)
on an empty queue, or to None
on a queue that starts with 0…
Scastie: https://scastie.scala-lang.org/GWx2h2b7SD67SOL6vM7y7w
1 Like
Yes and yes. The Int
declaration maps to java.lang.Integer
on the Java side - the queue will contain Integer
values. Receiving an Integer
value where an Int
is expected (which will be the case for #poll()
results, unless you explicitly widen the expected type) will trigger an implicit conversion that resolves to a call to #instanceOf[Int]
. For null
values, this will yield the default value which is defined to be 0 for Int
. At least that’s my understanding of the mechanics.
Option#apply()
should be fine, I’d think. Still, I’d probably want to put the queue behind a minimalist, app specific facade, operate the queue in terms of Integer
(or some app specific AnyRef
wrapper around Int
) and explicitly handle conversions to and fro in the facade methods.
2 Likes
My main concern is not so much with a Queue[Int]
, but more with a Queue[A]
when A
(which had been erased) happens to be Int
. Code like null.asInstanceOf[A]
can then become 0, losing the information that Java is conveying through null
.
If q
is empty, this Java code throws an exception:
int a = q.poll();
Maybe I’d be happier if Scala did the same, instead of happily giving me a value out of an empty queue.
Should be fine for A
as for Int
, as long as you ensure the null
case is resolved first (e.g. via Option#apply()
in a facade).
From a quick glance at generated bytecode: Technically, the conversion from null
to 0
is forced by unboxing. #poll()
returns a ref value - if forced to an Int
, an #unboxToInt()
conversion (respecting the default value spec for #asInstanceOf
) is triggered. If you Option#apply()
first, you resolve the null
, but don’t trigger unboxing. (The latter still may happen later, e.g. for Option(ConcurrentLinkedQueue[Int]().poll()).get + 1
.)
I still like the idea, as it expresses what’s really going on, but it doesn’t easily blend in with a generic queue/facade representation in A
, of course, and I haven’t found a way around this when going from Integer
to Option[Int]
:
INVOKEVIRTUAL scala/Predef$.Integer2int (Ljava/lang/Integer;)I
INVOKESTATIC scala/runtime/BoxesRunTime.boxToInteger (I)Ljava/lang/Integer;
INVOKEVIRTUAL scala/Option$.apply (Ljava/lang/Object;)Lscala/Option;
Probably - but it doesn’t, and if it did, somebody else would likely be unhappy.
The best one can do is to encapsulate fragile behavior in a single location at the outermost boundary, as is good practice for anything Java interop, anyway.
2 Likes