这个问题以前遇到过,这次又发生了,记录一下,避免新人再犯类似错误。
一个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
的类型不是Null
或AnyRef
而是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.