scala雾中风景(23): Nothing类型引发的NullPointerException

这个问题以前遇到过,这次又发生了,记录一下,避免新人再犯类似错误。

一个DAO的方法调用 mybatis 里的 SqlSessionTemplate.selectOne("...") 方法时没有指定类型,大致代码如下:

def check(...): Boolean = {
    ...
    val data = moneySessionTemplate.selectOne("queryXXX", params)
    data != null
}

程序意图是判断数据库里是否有某条记录,存在则返回返回true。 运行的时候上面的代码可能会抛出 NullPointerException 并提示是在 data != null 这一行,让人乍一看感觉很诡异。

这里又是类型系统的一个”陷阱”,因为val data 是一个不存在的值,它是Nothing类型。为何会是Nothing类型,又是因为selectOne方法的泛型参数在运行期类型无法推断所致的。我们模拟一下:

➜  cat -n Test.scala
 1
 2  object Test {
 3
 4    def selectOne[T](): T = { null.asInstanceOf[T] }
 5
 6    def main(args: Array[String]) {
 7      val r = selectOne()
 8      println("ok?")
 9    }
10  } 

上面的代码,编译和执行都没有问题,但当我们增加一行判断r是否为空的语句时:

➜  cat -n Test.scala
 1
 2  object Test {
 3
 4    def selectOne[T](): T = { null.asInstanceOf[T] }
 5
 6    def main(args: Array[String]) {
 7      val r = selectOne()
 8      if ( r != null )  // 运行时异常
 9        println("ok?")
10    }
11  }

运行时在上面的第8行,会抛出空指针异常:

➜  scala Test
java.lang.NullPointerException
    at Test$.main(Test.scala:8)
    at Test.main(Test.scala)
    ...

究其原因是因为r在之前被推导为了Nothing类型,是没有对应任何实例的一个“幽灵”,在访问这种类型的变量时都会抛出NullPointerException。那么问题来了,r的类型是由selectOne方法决定的,在这个方法里我明明是把null造型成结果类型返回的,为啥这里r的类型不是NullAnyRef而是Nothing呢?

因为调用selectOne方法的时候没有显示的声明类型参数T,编译器会对这种情况采用Nothing作为类型参数,比如:

scala> val l = new java.util.ArrayList
        l: java.util.ArrayList[Nothing] = []

所以 val r = selectOne() 这条语句实际被翻译为了(通过-Xprint:jvm)

val r: Nothing = selectOne().asInstanceOf[Nothing]

selectOne()的运行期结果并不是null,而是一个Nothing类型的幽灵,因为没有任何其他类型的值可以在运行期显式造型为Nothing类型,除了它自己:

null.asInstanceOf[Nothing] // 编译通过,运行时抛NPE

"test".asInstanceOf[Nothing]  // 编译通过,运行时抛ClassCastException

因为调用方法时类型参数的缺失,在类型推导时致使val r成了一个“幽灵”: 不可访问的值;后续对它的访问产生了NPE.

scala雾中风景(23): Nothing类型引发的NullPointerException》上有4条评论

  1. YANGL

    这样子就可以了吧:val data:Xxx = moneySessionTemplate.selectOne(“queryXXX”, params)

    回复
    1. hongjiang 文章作者

      对,或者在 selectOne 方法上显示声明类型参数: selectOne[Xxx](…)

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注