类的继承与层级
组合与继承
定制一个二维布局库
作为本章运行的例子,我们将创造一个制造和渲染二维布局元素的库。每个元素将代表一个
填充字符的长方形。方便起见,库将提供名为elem
的工厂方法来通过传入的数据构造新的
元素。例如,你将能通过工厂方法采用下面的写法创建带有字串的元素:
elem(s: String): Element
元素将以名为Element
的类型为模型。你将能在元素上调用above
或beside
,把另一个
元素放在当前元素的右边或是上边:
val column1 = elem("hello") above elem("***") val column2 = elem("***") above elem("world") column1 beside column2
打印这个表达式的结果将是:
hello *** *** world
抽象类
abstract声明抽象类
布局元素名为Element
,存放的文本内容类型为Array[String]
。提供方法contents
取得存放的文本内容,但没有定义实现方式,所以这个类是抽象类,要加上abstract
关键字:
abstract class Element { def contents: Array[String] }
抽象方法
注意:类Element
的contents
方法并没带有abstract
修饰符。不像Java,方法的声明
中不需要(也不允许)抽象修饰符。如果方法没有实现,它就是抽象的。
另一个术语用法需要分辨声明(declaration)和定义(definition)。类Element
声明了
抽象方法contents
,但当前没有定义具体方法。
抽象字段
abstract class Person { val id: Int val Name: String
这两个字段并没有生成在对应的Java类中,产生的只有对应的方法:
-
val
只有抽象getter
方法。 -
var
有抽象的getter
与setter
方法。
实现类要提供具体的字段,对于抽象的字段不用加abstract
:
class Employee(val: id: Int) extends Person { var name = "" }
实现类可以是一个匿名类:
val fred = new Person { val id = 1729 var name = "Fred" }
定义无参数方法
添加显示宽度和高度的方法:height
方法返回contents
里的行数。width
方法返回
第一行的长度,或如果元素没有行记录,返回零。(也就是说你不能定义一个高度为零但
宽度不为零的元素。)
abstract class Element { def contents: Array[String] def height: Int = contents.length def width: Int = if (height == 0) 0 else contents(0).length }
三个方法没一个有参数列表,甚至连个空列表都没有。如:
def width(): Int // 省略括号 def width: Int
推荐的惯例是在没有参数并且方法仅通过读含有对象的方式访问可变状态(专指其不改变
可变状态)时使用无参数方法。这样感觉上就和只读字段一样,其实也可以选择把width
和height
作为字段而不是方法来实现,只要简单地在每个实现里把def
修改成val
即可:
abstract class Element { def contents: Array[String] val height = contents.length val width = if (height == 0) 0 else contents(0).length }
两组定义从客户的观点来看是完全相同的。唯一的差别是字段的访问或许稍微比方法调用要 快,因为字段值在类被初始化的时候被预计算,而方法调用在每次调用的时候都要计算。
换句话说,字段在每个Element
实例上需要更多的内存空间。因此类的使用概况,属性
表达成字段还是方法更好,决定了其实现,并且这个概况还可以随时改变。
重点是Element
类的客户不应在其内部实现改变的时候受影响。
特别是如果类的字段变成了访问函数,且访问函数是纯的,就是说它没有副作用并且
不依赖于可变状态,那么类Element
的客户不需要被重写。客户都不应该需要关心这些。
目前为止一切良好。但仍然有些琐碎的复杂的东西要去做以协同Java处理事情的方式。问题
在于Java没有实现统一访问原则。因此Java里是string.length()
,不是string.length
(尽管是array.length
,不是array.length()
)。不用说,这让人很困惑。
为了在这道缺口上架一座桥梁,Scala在遇到混合了无参数和空括号方法的情况时很大度。 特别是,你可以用空括号方法重载无参数方法,并且反之亦可。你还可以在调用任何不带 参数的方法时省略空的括号。例如,下面两行在Scala里都是合法的:
Array(1, 2, 3).toString "abc".length
原则上Scala的函数调用中可以省略所有的空括号。然而,在调用的方法表达的超过其接收 调用者实例的属性时,推荐仍然写一对空的括号。例如,如果方法执行了I/O,或写入 可重新赋值的变量(var),或读出不是接受调用者的字段的var,无论是直接的还是非直接 的通过使用可变实例,那么空括号是合适的。这种方式是让参数列表扮演一个可见的线索 说明某些有趣的计算正通过调用被触发。例如:
"hello".length // no () because no side-effect println() // better to not drop the ()
总结起来,Scala里定义不带参数也没有副作用的方法为无参数方法,也就是说,省略空的 括号,是鼓励的风格。另一方面,永远不要定义没有括号的带副作用的方法,因为那样的话 方法调用看上去会像选择一个字段。这样你的客户看到了副作用会很奇怪。相同地,当你 调用带副作用的函数,请确信写这个调用的时候包括了空的括号。另一种考虑这个问题的 方式是,如果你调用的函数执行了操作,使用括号,但如果仅提供了对某个属性的访问, 省略括号。
扩展类
实例化一个元素,我们需要创建扩展了Element
并实现抽象的contents
方法的子类。
class ArrayElement(conts: Array[String]) extends Element { def contents: Array[String] = conts }
这种extends
子句有两个效果:使类ArrayElement
从类Element
继承所有非私有的成员
,并且使ArrayElement
成为Element
的子类型。由于ArrayElement
扩展了Element
,
类ArrayElement
被称为类Element
的子类。反过来,Element
是ArrayElement
的超类
。
如果你省略extends
子句,Scala编译器隐式地假设你的类扩展自scala.AnyRef
,在Java
平台上与java.lang.Object
一致。因此,类Element
隐式地扩展了类AnyRef
。
ArrayElement
的contents
方法实现了类Element
的抽象方法contents
:
scala> val ae = new ArrayElement(Array("hello", "world")) ae: ArrayElement = ArrayElement@d94e60 scala> ae.width res1: Int = 5
子类型化(subtyping)是指子类的值可以被用在需要其超类的值的任何地方。例如:
val e: Element = new ArrayElement(Array("hello"))
如果子类中的字段与超类同名,或是子类的中的方法名称和参数与超类类完全一样,就会
覆盖(override)超类中的版本。而且Scala里强制如果覆盖了就一定要加上override
修饰符。
重写方法和字段
命名空间
Java为定义准备了四个命名空间:字段,方法,类型和包。
而Scala仅有两个,与Java的四个命名空间相对:
- 值(字段,方法,包还有单例对象)
- 类型(类和特质名)
Scala把字段和方法放进同一个命名空间的理由很清楚,因为这样你就可以使用val
重重写
无参数的方法,这种你在Java里做不到的事情。
字段和方法属于相同的命名空间。这使得字段重写无参数方法成为可能。比如说,你可以
改变类ArrayElement
中contents
的实现,从一个方法变为一个字段,而无需修改类
Element
中contents
的抽象方法定义:
class ArrayElement(conts: Array[String]) extends Element { val contents: Array[String] = conts }
这个ArrayElement
的版本里,字段contents
(用val
定义)完美地实现了类Element
里的无参数方法contents
(用def
定义)。
另一方面,Scala里禁止在同一个类里用同样的名称定义字段和方法,而在Java里这样做 被允许。例如,下面的Java类能够很好地编译:
// This is Java class CompilesFine { private int f = 0; public int f() { return 1; } }
但是相应的Scala类将不能编译:
class WontCompile { private var f = 0 // Won't compile, because a field def f = 1 // and method have the same name }
如果要调试实例的构造顺序,可在编译加上参数-Xcheckinit
。这样在方法末初始化字段
时会抛出异常。
使用override修饰符
考虑一下这样的场景:
基类和子类是不同的人维护的。原来基类里没有add
方法,所以子类里加上了。后来
基类里也加上了add
方法,但维护子类的人不知道。这样的规定是为了防止「脆基类」问题
。
所以Scala里override
有强制的规定:
- 如果实现了抽象成员,加不加随便。
- 如果重载了具体实现,就一定要加。
- 没有重载就绝不能加。
这样起码保证了维护子类的人知道自己会覆盖超类的方法。
不可重写 final
Scala中字段与方法都可以用final
修饰为不可重写(因为Scala的字段也是可以重写为
方法的)。注意这与Java不一样,Java里final
字段表示不可改变,而Scala里已经有
val
表示不可改变了。
重写限制
概括:
-
def
只能重写另一个def
-
val
只能重写另一个val
与无参def
-
var
只能重写另一个抽象的var
详述:
def | val | var | |
---|---|---|---|
用val重写 | 子类有一个私有字段。<br/>重写超类的getter方法 | 超类同名的私有字段。<br/>重写超类的getter方法 | 错误 |
用def重写 | 同Java | 错误 | 错误 |
用var重写 | 同时重写getter/setter。<br/>只重写getter会报错 | 错误 | 重写超类的抽象var |
在当前类中,随时可以对getter
与setter
重新实现var
,但在子类中不能通过
getter
与setter
重新实现var
,只能接受现有的实现。
结构类型
结构类型(structural type)只给出类必须拥有的方法,而不是类的名称。
如:我不知道反射出来的是什么类型,但我知道应该要有hello(String): String
方法:
class Foo { def hello(name: String): String = "Hello there, %s".format(name) } object FooMain { def main(args: Array[String]) { val foo = Class.forName("Foo").newInstance .asInstanceOf[{ def hello(name: String): String }] println(foo.hello("Walter")) // prints "Hello there, Walter" } }
定义参数化字段
ArrayElement
类的定义。它有一个参数conts
,其唯一目的是被复制到contents
字段
。选择conts
这个参数的名称只是为了让它看上去更像字段名contents
而又不会因为
名字一样而发生实际冲突。这是一种「代码异味」,一个表明或许某些不必须的累赘和重复。
可以通过在单一的参数化字段(parametric field)定义中组合参数和字段避免:
class ArrayElement(val contents: Array[String]) extends Element
注意用的是val
,所以现在拥有一个可以从类外部访问的,(不能重新赋值的)字段
contents
。字段使用参数值初始化。等同于:
class ArrayElement(x123: Array[String]) extends Element { val contents: Array[String] = x123 }
同样也可以使用var
前缀类参数,这种情况下相应的字段将能重新被赋值。还有可能添加
如private
、protected
或override
这类的修饰符到这些参数化字段上,就好象
你可以在其他类成员上做的事情:
class Cat { val dangerous = false } class Tiger( override val dangerous: Boolean, private var age: Int ) extends Cat
Tiger的定义是以下包括重写成员dangerous
和private
成员age
的类定义替代写法的
简写:
class Tiger(param1: Boolean, param2: Int) extends Cat { override val dangerous = param1 private var age = param2 }
调用超类构造器
如果再要新的子类:
class LineElement(s: String) extends ArrayElement(Array(s)) { override def width = s.length override def height = 1 }
由于LineElement
扩展了ArrayElement
,并且ArrayElement
的构造器带一个参数
(Array[String]),LineElement需要传递一个参数到它的超类的主构造器。要调用超类
构造器,只要把你要传递的参数或参数列表放在超类名之后的括号里)即可。
只有主构造器可以调用超类构造器
Scala中辅助构造器不能调用超类构造器,只有主构造器可以调用超类构造器。
构造顺序
Scala与Java有一个共同的问题:超类的构造器会调用被子类覆盖的方法。
用动物的视力(或感知以距离)来作为例子:
- 默认动物的视力为10
- 蚂蚁的视力只有2
class Creature { val range: Int = 10 val env: Array[Int] = new array[Int](range) } class Ant extends Creature { override val range = 2 }
如果初始化Ant
实例,过程比较复杂:
-
调用超类构造器,设置
range
为10。 -
超类构造器初始化
env
长度时要用到range
,发现range
被子类重写。 -
调用子类的
range
,但是子类还没有初始化,所以range
值为0
。 -
env
被初始化为长度为0的数组。超类构造器执行完毕。 -
子类构造器开始执行,把
range
设置为2.
解决的方案:
-
final val
声明不能覆盖,这样安全但是不灵活。 -
lazy
懒加载。安全但是影响性能。 - 预初始化。接下来就讲。
预初始化
用with
代替extends
,并给字段定义加上花括号,放在超类的构造器之前:
class Ant { override val range = 2 } with Creature
由于预初始化的字段的超类构造器调用前被初始化,所以不能引用正在被构造的实例。所以
对于this
实际指向的是正被构造的类或对象的实例,而来是被构造的实例本身。
多态和动态绑定
创建一个新的子类,它可以按给出的长度宽度,用指定的字符填充:
class UniformElement( ch: Char, override val width: Int, override val height: Int ) extends Element { private val line = ch.toString * width def contents = Array.make(height, line) }
父类的变量可以存放子类的实例,就是多态的一种体现。这么多子类都可以用父类的变量来 存放:
val e1: Element = new ArrayElement(Array("hello", "world")) val ae: ArrayElement = new LineElement("hello") val e2: Element = ae val e3: Element = new UniformElement('x', 2, 3)
变量和表达式上的方法调用是动态绑定(dynamically bound)的。这意味着被调用的 实际方法实现取决于运行期实例实际的类,而不是变量或表达式的类型。
为了演示这种行为,我们会从我们的Element
类中临时移除所有存在的成员并添加一个名
为demo
的方法。我们会在ArrayElement
和LineElement
中重写demo
,但
UniformElement
除外:
abstract class Element { def demo() { println("Element's implementation invoked") } } class ArrayElement extends Element { override def demo() { println("ArrayElement's implementation invoked") } } class LineElement extends ArrayElement { override def demo() { println("LineElement's implementation invoked") } } // UniformElement inherits Element's demo class UniformElement extends Element
如果你把这些代码输入到了解释器中,那么你就能定义这个带了一个Element
并调用
demo
的方法:
def invokeDemo(e: Element) { e.demo() }
如果你传给invokeDemo
一个ArrayElement
,你会看到一条消息指明ArrayElement
的
demo
实现被调用,尽管被调用demo
的变量e
的类型是Element
:
scala> invokeDemo(new ArrayElement) ArrayElement's implementation invoked
相同的,如果你传递LineElement
给invokeDemo
,你会看到一条指明LineElement
的
demo
实现被调用的消息:
scala> invokeDemo(new LineElement) LineElement's implementation invoked
传递UniformElement
时的行为一眼看上去会有些可以,但是正确:
scala> invokeDemo(new UniformElement) Element's implementation invoked
因为UniformElement
没有重写demo
,它从它的超类Element
继承了demo
的实现。
因此,当实例的类是UniformElement
时,Element
的实现就是要调用的demo
的正确
实现。
使用组合与继承
组合与继承是利用其它现存类定义新类的两个方法。
如果你接下来的工作主要是代码重用,通常你应采用组合而不是继承。只有继承受脆基类 问题困扰,这种情况你可能会无意中通过改变超类而破坏了子类。
关于继承关系你可以问自己一个问题,是否它建模了一个is-a关系。你能问的另一个问题是 ,是否客户想要把子类类型当作超类类型来用。
实现示例中的功能
把一个元素放在另一个上面是指串连这两个元素的contents
值。
def above(that: Element): Element = new ArrayElement(this.contents ++ that.contents)
操作符++
把两个元素靠在一起,我们将创造一个新的元素,其中的每一行都来自于两个
元素的相应行的串连。
def beside(that: Element): Element = { val contents = new Array[String](this.contents.length) for (i <- 0 until this.contents.length) contents(i) = this.contents(i) + that.contents(i) new ArrayElement(contents) }
索引数组的循环是指令式风格。这个方法可以替代缩减成一个表达式:
new ArrayElement( for ( (line1, line2) <- this.contents zip that.contents ) yield line1 + line2 )
zip
操作符转换为一个对子的数组(可以称为Tupele2
)。zip
方法从它的两个参数中
拣出相应的元素并组织成对子数组。
例如,表达式:
Array(1, 2, 3) zip Array("a", "b")
将生成:
Array((1, "a"), (2, "b"))
如果两个操作数组的其中一个比另一个长,zip将舍弃余下的元素。
定义toString
方法返回元素格式化成的字串:
override def toString = contents mkString "\n"
最后是这个样子:
abstract class Element { def contents: Array[String] def width: Int = if (height == 0) 0 else contents(0).length def height: Int = contents.length def above(that: Element): Element = new ArrayElement(this.contents ++ that.contents) def beside(that: Element): Element = new ArrayElement( for ( (line1, line2) <- this.contents zip that.contents ) yield line1 + line2 ) override def toString = contents mkString "\n" }
定义工厂对象
最直接的方案是创建类Element
的伴生对象并把它做成布局元素的工厂方法。这种方式
唯一要暴露给客户的就是Element
的类/实例组合,隐藏它的三个实现类ArrayElement
,
LineElement
和UniformElement
。
object Element { def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(chr: Char, width: Int, height: Int): Element = new UniformElement(chr, width, height) def elem(line: String): Element = new LineElement(line) }
这些工厂方法使得改变类Element
的实现通过使用elem
工厂方法实现而不用new
操作
产新的ArrayElement
实例成为可能。
为了不使用单例对象的名称Element
从而化调用工厂方法,我们将在源文件引入
Element.elem
。
换句话说,代之以在Element
类内部使用Element.elem
调用工厂方法,我们将引用
Element.elem
,这样我们只要使用它们的简化名,elem
,就可以调用工厂方法。
import Element.elem abstract class Element { def contents: Array[String] def width: Int = if (height == 0) 0 else contents(0).length def height: Int = contents.length def above(that: Element): Element = elem(this.contents ++ that.contents) def beside(that: Element): Element = elem( for ( (line1, line2) <- this.contents zip that.contents ) yield line1 + line2 ) override def toString = contents mkString "\n" }
有了工厂方法之后,子类ArrayElement
,LineElement
和UniformElement
不再需要
直接被客户访问,所以可以改成是私有的。
Scala里,你可以在类和单例对象中定义其它的类和单例对象。因此一种让Element
的子类
私有化的方式就是把它们放在Element
单例对象中并在那里声明它们为私有。需要的时候
,这些类将仍然能被三个elem
工厂方法访问。
private class ArrayElement( val contents: Array[String] ) extends Element private class LineElement(s: String) extends Element { val contents = Array(s) override def width = s.length override def height = 1 } private class UniformElement( ch: Char, override val width: Int, override val height: Int ) extends Element { private val line = ch.toString * width def contents = Array.make(height, line) } def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(chr: Char, width: Int, height: Int): Element = new UniformElement(chr, width, height) def elem(line: String): Element = new LineElement(line) }
变高变宽
Element的版本并不完全,因为他不允许客户把不同宽度的元素堆叠在一起,或者不同高度 的元素靠在一起。比方说,下面的表达式将不能正常工作,因为组合元素的第二行比第一行 要长:
new ArrayElement(Array("hello")) above new ArrayElement(Array("world!"))
与之相似的,下面的表达式也不能正常工作:
new ArrayElement(Array("one", "two")) beside new ArrayElement(Array("one"))
添加私有帮助方法widen
通过带个宽度做参数并返回那个宽度的Element
。heighten
,
能在竖直方向执行同样的功能。
import Element.elem abstract class Element { def contents: Array[String] def width: Int = contents(0).length def height: Int = contents.length def above(that: Element): Element = { val this1 = this widen that.width val that1 = that widen this.width elem(this1.contents ++ that1.contents) } def beside(that: Element): Element = { val this1 = this heighten that.height val that1 = that heighten this.height elem( for ((line1, line2) <- this1.contents zip that1.contents) yield line1 + line2) } def widen(w: Int): Element = if (w <= width) this else { val left = elem(' ', (w - width) / 2, height) var right = elem(' ', w - width - left.width, height) left beside this beside right } def heighten(h: Int): Element = if (h <= height) this else { val top = elem(' ', width, (h - height) / 2) var bot = elem(' ', width, h - height - top.height) top above this above bot } override def toString = contents mkString "\n" }
完整的示例代码
写一个画给定数量边界的螺旋的程序。
// In file compo-inherit/Spiral.scala import Element.elem object Spiral { val space = elem(" ") val corner = elem("+") def spiral(nEdges: Int, direction: Int): Element = { if (nEdges == 1) elem("+") else { val sp = spiral(nEdges - 1, (direction + 3) % 4) def verticalBar = elem('|', 1, sp.height) def horizontalBar = elem('-', sp.width, 1) if (direction == 0) (corner beside horizontalBar) above (sp beside space) else if (direction == 1) (sp above space) beside (corner above verticalBar) else if (direction == 2) (space beside sp) above (horizontalBar beside corner) else (verticalBar above corner) beside (space above sp) } } def main(args: Array[String]) { val nSides = args(0).toInt println(spiral(nSides, 0)) } }
$ scala Spiral 6 $ scala Spiral 11 $ scala Spiral 17 +----- +---------- +---------------- | | | | +-+ | +------+ | +------------+ | + | | | | | | | | | | | +--+ | | | +--------+ | +---+ | | | | | | | | | | | | ++ | | | | | +----+ | | | | | | | | | | | | | | +----+ | | | | | ++ | | | | | | | | | | | | | +--------+ | | | +--+ | | | | | | | | | | | +------+ | | | | | | | +----------+ | | | +--------------+
Scala类的层级
Scala里,每个类都继承自通用的名为Any
的超类。因为所有的类都是Any
的子类,那么
定义在Any
中的方法就是「普遍」方法:它们可以被任何实例调用。
Scala还在层级的底端定义了Null
和Nothing
,主要都扮演通用的子类。例如,就像说
Any
是所有其它类的超类,Nothing
是所有其它类的子类。
Scala类的概览
层级的顶端是类Any
,定义了包含下列的方法:
final def ==(that: Any): Boolean final def !=(that: Any): Boolean def equals(that: Any): Boolean def hashCode: Int def toString: String
类Any里的=
和!=
,被声明为final
,因此它们不能在子类里面重载。实际上,==
总是与equals
相同,!=
总是与equals
相反。因此独立的类可以通过重载equals
方法
修改==
或!=
的意义。
根类Any
有两个子类:AnyVal
和AnyRef
。
值类型(AnyVal)
AnyVal
是Scala里每个内建值类型的父类。有九个这样的值类型:Byte
,Short
,
Char
,Int
,Long
,Float
,Double
,Boolean
和Unit
。其中的前八个对应到
Java的原始类型,它们的值在运行时表示成Java的原始值。
Scala里这些类的实例都写成字面量,不能使用new
创造这些类的实例。值类都被定义为
即是抽象的又是final
的,强制贯彻。因此如果你写了new
就会出错:
scala> new Int <console>:5: error: class Int is abstract; cannot be instantiated new Int ^
另一个值类型Unit
大约对应于Java的void
类型;被用作不返回任何有趣结果的方法的
结果类型。Unit
只有一个实例值,被写作()
。
值类型支持作为方法的通用的数学和布尔操作符。例如,Int
有名为+
和*
的方法,
Boolean
有名为||
和&&
的方法。值类型也从类Any
继承所有的方法:
scala> 42 max 43 res4: Int = 43 scala> 42 min 43 res5: Int = 42 scala> 1 until 5 res6: Range = Range(1, 2, 3, 4) scala> 1 to 5 res7: Range.Inclusive = Range(1, 2, 3, 4, 5) scala> 3.abs res8: Int = 3 scala> (-3).abs res9: Int = 3
值类型的空间是扁平的;所有的值类型都是scala.AnyVal
的子类型,但是它们不是互相的
子类。代之以它们不同的值类型之间可以隐式地互相转换。例如,需要的时候,类
scala.Int
的实例可以自动放宽(通过隐式转换)到类scala.Long
的实例。
隐式转换还用来为值类型添加更多的功能。例如,类型Int
支持以下所有的操作:
scala> 42 max 43 res4: Int = 43 scala> 42 min 43 res5: Int = 42 scala> 1 until 5 res6: Range = Range(1, 2, 3, 4) scala> 1 to 5 res7: Range.Inclusive = Range(1, 2, 3, 4, 5) scala> 3.abs res8: Int = 3 scala> (-3).abs res9: Int = 3
工作原理:
方法min
,max
,until
,to
和abs
都定义在类scala.runtime.RichInt
里,并且
有一个从类Int
到RichInt
的隐式转换。当你在Int上调用没有定义在Int
上但定义在
RichInt
上的方法时,这个转换就被应用了:
引用类型(AnyRef)
类Any
的另一个子类是类AnyRef
。这个是Scala
里所有引用类的基类。正如前面提到的
,在Java平台上AnyRef
实际就是类java.lang.Object
的别名。因此Java里写的类和
Scala
里写的都继承自AnyRef
。
存在AnyRef
别名代替使用java.lang.Object
名称的理由是,Scala被设计成可以同时
工作在Java和.Net平台。在.NET平台上,AnyRef
是System.Object
的别名。
可以认为java.lang.Object
是Java平台上实现AnyRef
的方式。因此,尽管你可以在Java
平台上的Scala程序里交换使用Object
和AnyRef
,推荐的风格是在任何地方都只使用
AnyRef
。
Scala类与Java类不同在于它们还继承自一个名为ScalaObject
的特别的记号特质。理念是
ScalaObject
包含了Scala编译器定义和实现的方法,目的是让Scala程序的执行更有效。
到现在为止,Scala实例包含了单个方法,名为$tag
,用于内部以提速模式匹配。
原始类型是如何实现的
Scala以与Java同样的方式存储整数:把它当作32位的字。这对在JVM上的效率以及与Java库
的互操作性方面来说都很重要。标准的操作如加法或乘法都被实现为原始操作。然而,当
整数需要被当作(Java)对象看待的时候,Scala使用了「备份」类java.lang.Integer
。
如在整数上调用toString
方法或者把整数赋值给Any类型的变量时,就会这么做。
所有这些听上去都近似Java5里的自动装箱并且它们的确很像。不过有一个关键差异,Scala 里的装箱比Java里的更少看见。尝试下面的Java代码:
// This is Java boolean isEqual(int x, int y) { return x == y; } System.out.println(isEqual(421, 421));
当然会得到true
。现在,把isEqual
的参数类型变为java.lang.Integer
(或Object
,结果都一样):
// This is Java boolean isEqual(Integer x, Integer y) { return x == y; } System.out.println(isEqual(421, 421));
却得到了false
!原因是数421
被装箱了两次,因此参数x
和y
是两个不同的实例。
因为在引用类型上==
表示引用相等,而Integer
是引用类型,所以结果是false
。这是
展示了Java不是纯面向对象语言的一个方面。我们能清楚观察到原始类型和引用类型之间的
差别。
现在在Scala里尝试同样的实验:
scala> def isEqual(x: Int, y: Int) = x == y isEqual: (Int,Int)Boolean scala> isEqual(421, 421) res10: Boolean = true scala> def isEqual(x: Any, y: Any) = x == y isEqual: (Any,Any)Boolean scala> isEqual(421, 421) res11: Boolean = true
实际上Scala里的相等操作==
被设计为透明的参考类型代表的东西。对值类型来说,就是
自然的(数学或布尔)相等。对于引用类型,==
被视为继承自Object
的equals
方法的
别名。这个方法被初始地定义为引用相等,但被许多子类重载实现它们种族的相等概念。
这也意味着Scala里你永远也不会落入Java知名的关于字串比较的陷阱。Scala里,字串比较
以其应有的方式工作:
scala> val x = "abcd".substring(2) x: java.lang.String = cd scala> val y = "abcd".substring(2) y: java.lang.String = cd scala> x == y res12: Boolean = true
Java里,x
与y
的比较结果将是false
。程序员在这种情况应该用equals
,不过它
容易被忘记。
然而,有些情况你需要使用引用相等代替用户定义的相等。
例如,某些时候效率是首要因素,你想要把某些类哈希合并(hash cons)然后通过引用
相等比较它们的实例(类实例的哈希合并是指把创建的所有实例缓存在弱集合中。然后,
一旦需要类的新实例,首先检查缓存。如果缓存中已经有一个元素等于你打算创建的,你
可以重用存在的实例。这样安排的结果是,任何以equals()
判断相等的两个实例同样在
引用相等上判断一致。)。
为这种情况,类AnyRef定义了附加的eq
方法,它不能被重载并且实现为引用相等(也就
是说,它表现得就像Java里对于引用类型的==
那样)。同样也有一个eq
的反义词,被
称为ne
。例如:
scala> val x = new String("abc") x: java.lang.String = abc scala> val y = new String("abc") y: java.lang.String = abc scala> x == y res13: Boolean = true scala> x eq y res14: Boolean = false scala> x ne y res15: Boolean = true
底层类型
层级的底部你看到了两个类scala.Null
和Scala.Nothing
。它们是用统一的方式处理
某些Scala的面向对象类型系统的「边界情况」的特殊类型。
Null
类Null
唯一的实例是null
值;它是每个引用类(就是说,每个继承自AnyRef
的类)的
子类。Null
不兼容值类型。比方说,不可把null
值赋给整数变量:
scala> val i: Int = null <console>:4: error: type mismatch; found : Null(null) required: Int
Nothing
类型Nothing
在Scala
的类层级的最底端;它是任何其它类型的子类型。然而,根本没有
这个类型的任何值。要一个没有值的类型有什么意思呢?在控制结构的try-catch中讨论过
,Nothing
的一个用处是它标明了不正常的终止。例如Scala的标准库中的Predef
单例
对象有一个error
方法,如下定义:
def error(message: String): Nothing = throw new RuntimeException(message)
error
的返回类型是Nothing
,告诉用户方法不是正常返回的(代之以抛出了异常)。
因为Nothing
是任何其它类型的子类,你可以非常灵活的使用像error
这样的方法。
例如:
def divide(x: Int, y: Int): Int = if (y != 0) x / y else error("can't divide by zero")
if
状态分支,x / y
,类型为Int,而else
分支,调用了error
,类型为Nothing
。
因为Nothing
是Int
的子类型,整个状态语句的类型是Int
,正如需要的那样。
Unit
Unit
类似于Java中的void
。Unit
只有一个实例()
。