Dealing with Java nulls and value classes

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

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)

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…


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.


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. :slight_smile: 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.