Scalaにおけるunboxing

今更な話題なのですが、解説したくなったので解説を書いておきます。

ここでいうunboxingとは「以下のような変換」のこと、とします。 Scalaのversionは2.13.8としておきますが、2.12でも3でもおそらく変わりません。

scala.Int, scala.Boolean などは、実態はJVM上でのprimitive型なので、Javaにおけるunboxingと同じと思ってもらって構いません。

そもそも、普通にScalaを書くと、あまりunboxingを意識することはありません。 Scala自体が、Javaと異なり型引数にJavaでいうprimitiveを含めた任意の型を当てはめることが可能になっているからです。

意識するとしたら、Javaのライブラリ側がboxingされたjava.lang.Integerなどを返す場合です。 Javaにおいても、java.util.Optionalなどが後から追加されたため、それらを使ってくれていれば、以下の問題は気にする必要が薄れるのですが。

それらは、以下のPredefの定義で勝手にimplicitに変換されるため、unboxingされるからといって普段は気にする必要がありません。

https://github.com/scala/scala/blob/v2.13.8/src/library/scala/Predef.scala#L487-L502

問題は

Javaのライブラリが、あえてnullを返すためにboxingされた型を使用している場合」

です。 例えば天下のgoogleのライブラリなどでも「値があったら返し、なかったらnull返す」と、堂々とjavadocに書かれたIntegerを返すメソッドがライブラリ内部に存在していたりします。 (googleは一例であって、Javaではよくある・・・?)

実態の値がnullの場合にunboxingが発生すると、ScalaJavaではどうなるでしょうか?

$ jshell
|  Welcome to JShell -- Version 11.0.15
|  For an introduction type: /help intro

jshell> Integer a = null
a ==> null

jshell> int b = a;
|  Exception java.lang.NullPointerException
|        at (#2:1)

jshell> java.util.Optional<Integer> c = java.util.Optional.ofNullable(a)
c ==> Optional.empty
Welcome to Scala 2.13.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_332).
Type in expressions for evaluation. Or try :help.

scala> def a: Integer = null
def a: Integer

scala> val b: Int = a
val b: Int = 0

scala> val c: Option[Int] = Option(a)
val c: Option[Int] = Some(0)

scala> val d: Option[Integer] = Option(a)
val d: Option[Integer] = None

微妙にnullの扱いが異なるのがわかると思います。

また、nullをOption.applyによってNoneに変換しようとしても、implicit conversionによるunboxingは、あくまで IntegerからInt へ関するだけで Option[Integer] から Option[Int] へ変換してくれるわけではないので、上記の val c: Option[Int] = Option(nullかもしれないIntegerの値) というコードを書いた場合は、おそらく意図しない結果になっているパターンがあり得るかもしれません。

しかし、これ検知するの結構面倒というか難しいですね???

というわけでwartremoverで検知する君を作って本体に入れてリリースしました。

https://github.com/wartremover/wartremover/commit/ae298b2b771db12ad867eb01ff1f33f9bb9d044c

以上、解説含めた「新しいwartremoverのrule作ったよ」という宣伝でした。