值类型与数组

在之前的值类型的一些细节 里说过“值类型在赋值给数组时会被装箱”,这个不严谨,当时上下文是自定义的值类型会被装箱,而系统原生的几个值类型不会,因为它们有默认值。

比如原生的Int,Unit,Double 等,创建好数组后,都以它们的默认值来填充的:

// Int的默认值是0
scala> val arr = new Array[Int](1)
arr: Array[Int] = Array(0)

// Unit的默认值是()
scala> val arr = new Array[Unit](1)
arr: Array[Unit] = Array(())

// Double的默认值是0.0
scala> val arr = new Array[Double](1)
arr: Array[Double] = Array(0.0)

而自定义的值类型,在创建一个数组后,则是用null填充的:

// 定义值类型A
scala> class A(val str:String) extends AnyVal
defined class A

// 创建A类型的数组,默认用null填充
scala> val arr = new Array[A](1)
arr: Array[A] = Array(null)

或许会怀疑上面的例子里,A内部包的是一个String类型,这是个引用类型,null是String的默认值,所以用null填充

// 把A内部换成Int
scala> class A(val i:Int) extends AnyVal
defined class A

// 默认还是用null填充,而不是Int的默认值0
scala> val arr = new Array[A](1)
arr: Array[A] = Array(null)

所以,自定义的值类型数组初始值都是null,与值类型的内部数据无关。
因为自定义的值类型赋给数组时会装箱,它们都是被当作引用来对待的。

但对值类型赋值时是不能用null来赋值的,不管是系统原生的值类型还是自定义的值类型。

// 对刚才的值类型数组的元素赋值
scala> arr(0) = null
<console>:10: error: type mismatch;
 found   : Null(null)
 required: A
          arr(0) = null
                   ^

这点初看有些矛盾,自定义值类型数组,既然都被装箱对待,且初始值为null,却又不允许用null来赋值?

展开看,在数组初始化时它被装箱为引用类型,访问arr(0)时,它是引用类型,值为null;而修改arr(0)时编译器又严格的按照值类型对待,不管它其实已经被装箱为引用类型,不能把null赋给它。 虽然别扭,但对于赋值操作编译器还是保持简单一致的原则。

赋值时把null赋给值类型的变量,编译器无法把Null类型造型为值类型A,这点参考:“Null与Nothing,造型问题” 一文

数组中对值类型元素初始化都做装箱处理,但对var变量默认赋值时却不是这样

scala> class A(val i:Int) extends AnyVal

scala> var a:A = _
a: A = A@0 //默认赋值是 A(0)

scala> a == 0
res0: Boolean = false

scala> a == new A(0)
res1: Boolean = true

默认值是值类型内部数据的默认值, 内部数据如果是引用类型,初始值为A(null),注意在验证时容易碰到scala的一个bug

scala> class A(val s:String) extends AnyVal

// repl下触发scala的一个bug,实际这条语句是没有问题的
scala> var a:A = _  
java.lang.NullPointerException
    at A$.hashCode$extension(<console>:7)
    at A.hashCode(<console>:7)

可以放到文件里编译,运行来看:

class A(val s:String) extends AnyVal

object Main extends App{
  var a:A = _
  println(a == null)
  println(a.s == null)
}

Null与Nothing,造型问题

Null与Nothing

scala类型系统以Any为根,分为AnyRefAnyVal 两个分支体系,在AnyRef分支的最底层,有个Null类型的特殊类型,它被当作是所有AnyRef类型的子类型。
更进一步在两个分支共同的最底层类型是Nothing类型,它被当作所有AnyRefAnyVal类型的子类型。

Null类型只有一个唯一的值:null,可以被赋给所有的AnyRef类型变量

scala> val s:String = null
s: String = null

但null不可以赋值给AnyVal类型:

scala> val i:Int = null
<console>:7: error: type mismatch;
 found   : Null(null)
 required: Int

注意,不要被Unit值类型在赋值时的障眼法给骗了,以为null可以被赋给Unit

scala> val u:Unit = null
u: Unit = ()

实际上把任何表达式结果不是Unit类型的表达式赋给Unit类型变量,都被转为 { expr; () },所以上面的等同于{null; ()} 把最终得到的()赋给了u变量。

Null在类型推导时只能被造型为AnyRef分支下的类型,不能造型为AnyVal分支下的类型,不过我们显式的通过asInstanceOf操作却又是可以把null造型为AnyVal类型的:

scala> null.asInstanceOf[Int]
res0: Int = 0

这是因为asInstanceOf操作导致了装箱拆箱的行为,在值类型的一些细节里有提到过;

val i = null.asInstanceOf[Int]

// 类似于java里的
int i = (int)((Integer)null);

先装箱Int为引用类型,null被造型成了引用类型的Int(java.lang.Integer),然后又做了一次拆箱,把一个为null的Integer类型变量造型成Int值类型,但在拆箱这一点处理上,体现了scala与java的不同

// java里,编译通过,运行失败,空指针异常
int i = (int)((Integer)null);

// scala里,把值为null的Integer拆箱为值类型Int是ok的,得到Int的默认值0
val i = null.asInstanceOf[java.lang.Integer].asInstanceOf[Int]

在java里基本类型(primitive type) 与引用类型是有明确差异的,虽然提供了自动装箱拆箱的便捷,但在类型上两者是不统一的;而scala里修正这一点,Int类型不再区分int/Integer,类型一致,所以值为null的Integer在通过asInstanceOf[Int]时被当作一个未初始化的Int对待,返回了一个默认值Int(注:asInstanceOf不改变原值,返回一个新值)

这一点对自定义的值类型也一样

scala> class A(val i:Int) extends AnyVal
defined class A

scala> null.asInstanceOf[A]
res18: A = A@0

Nothing类型的特殊之处在于它没有对应的值,这个类型存在的意义或许只是为了类型推导:

scala> def foo = throw new RuntimeException
foo: Nothing

因为scala里每个表达式都有结果,也包括throw语句,它的返回类型是Nothing,而实际运行中会导致线程终止,所以只是编译时用于类型推导

scala> def foo(i:Int) = if (i==100) "OK" else throw new RuntimeException
foo: (i: Int)String

上面if分支返回String类型,而else分支是Nothing,返回值类型是String与Nothing在类型树上的最小公共父类型,因为Nothing是String的子类型,所以直接返回了 String类型。

另外Nothing可用在泛型情况下:

scala> val l:List[Int] = List[Nothing]()
l: List[Int] = List()

因为List[+A]定义是协变的,所以List[Nothing]是List[Int]的子类,但List[Null]不是List[Int]的子类

scala> val l:List[Int] = List[Null]()
<console>:7: error: type mismatch;
 found   : List[Null]
 required: List[Int]

lazy变量与双重检测锁(DCL)

scala里的lazy变量,背后的实现是个典型的双重检测锁(DCL)模式。比如:

class A{
    lazy val a=2
}

对a的访问,先通过状态标记变量(做一次位与操作)判断是否已经初始化过。通过scalac -Xprint:jvm来看对a的访问:

lazy private[this] var a: Int = _;

// 通过标记变量的与1做位与操作,判断是否已初始化
lazy def a(): Int = if (A.this.bitmap$0.&(1).$asInstanceOf[Byte]().==(0))
  A.this.a$lzycompute()
else
  A.this.a;

看看状态变量(需要用volatile修饰) 和延迟计算的逻辑:

// 定义一个可变的Byte类型的变量 bitmap$0
@volatile private[this] var bitmap$0: Byte = 0.$asInstanceOf[Byte]();

// 延迟计算的函数
private def a$lzycompute(): Int = {
  {
    // 线程安全
    A.this.synchronized({
      // 按位与操作,
      if (A.this.bitmap$0.&(1).$asInstanceOf[Byte]().==(0))
        {
          A.this.a = 2;
          // 改变 bitmap$0 的值,标记a已经赋值过了
          A.this.bitmap$0 = A.this.bitmap$0.|(1).$asInstanceOf[Byte]();
          ()
        };
      scala.runtime.BoxedUnit.UNIT
    });
    ()
  };
  A.this.a
};

为什么用 Byte 变量而非用Boolean,是当有多个lazy成员时,可以用同一个状态变量,按”位”标记,而不必用多个Boolean变量来标识各个lazy成员的状态。

比如有a,b,c 三个lazy成员,则 bitmap$0 第一位表示a,第二位表示b,第三位表示c,以此类推;判断时只要分别与1,2,4,8… 做位与操作就知道了。

不过Byte类型最大只有8位,一个类中如果超过8个lazy成员,编译器就把 bitmap$0 状态变量改用Int类型了。

String当作集合处理时的方法

String 会被隐式转换为 StringOps

StringOps extends StringLike

StringLike是个支持协变类型的trait,混入了 IndexedSeqOptimized[Char, Repr]Ordered[String]

StringLike[+Repr] extends collection.IndexedSeqOptimized[Char, Repr] with Ordered[String]

scala> val str = "hello"
str: java.lang.String = hello

scala> str.reverse   // GenSeqLike 中的方法
res6: String = olleh

scala> str.map(_.toUpper)  // map方法
res7: String = HELLO

scala> str(0)       //apply(i:Int) 是在GenSeqLike特质里定义的
res5: Char = h

scala> str drop 3  // TraversableLike 里的方法
res8: String = lo

scala> str slice (1, 4)  // GenTraversableLike 里的方法
res9: String = ell

scala> val s: Seq[Char] = str
s: Seq[Char] = WrappedString(h, e, l, l, o)