第十二讲|第十二讲 继承

在本章中,你将了解到Scala的继承与Java和C++最显著的不同。
本章要点:

  • extends、final关键字
  • 重写时必须用override
  • 类型检验与转换
  • 匿名类
  • scala继承层级
  • 对象相等性
    在本章中,我们只探讨类继承自另一个类的情况。继承特质的内容后面会详细介绍。
12.1 extends、final关键字的简单介绍
  • extends:之前提到过Scala扩展类的方式和Java一样,使用extends关键字;
  • final:将类声明为final,类就不能被扩展。将单个方法或字段声明为final,它们就不能被重写。注意这和Java不同,在Java中,final字段是不可变的,类似Scala中的val。
12.2 重写 12.2.1方法重写 在Scala中重写一个非抽象方法必须使用override修饰符。例如:
abstract class UndoableAction(val description: String) { def undo(): Unit def redo(): Unit } class DoNothingAction extends UndoableAction("Do nothing") { override def undo() { print("****undo******") } override def redo() { print("*****redo*****") } }

在Scala中调用超类的方法和Java完全一样,也使用super关键字
12.2.2 字段重写
class Person ( val name: String ) { override def toString=getClass.getName+"name="+ name+ "]" }class SecretAgent (codename: String) extends Person (codename) { override val name = "secret" // 不想暴露真名… override val toString = "secret" // …或类名 }

重写注意如下限制:
  • def只能重写另一个def
  • val只能重写另一个val或不带参数的def
  • var只能重写另一个抽象的var
12.3 类型检查和转换 要测试某个对象是否属于某个给定的类,可以用 islnstanceOf 方法。如果测试成功,你就可以用 aslnstanceOf 方法将引用转换为子类的引用。
如下:
if ( p.islnstanceOf[Employee]) { val s : p.asInstanceOf[Employee] // s的类型为Employee }

如果p指向的是Employee类及其子类的对象,则p.islnstanceOf[Employee]将会成功。如果p是null,则p.islnstanceOf[Employee]将返回false,且p.aslnstanceOf[Employee]将返回null。如果p不是一个Employee,则p.aslnstanceOf[Employee]将抛出异常
如果你想要测p指向的是一个Employee对象但又不是其子类的话,可以用:
if ( p.getClass==classOf[Employee] )

classOf方法定义在scala.Predef对象中,因此会被自动引入。
*下表显示了Scala和Java的类型检查和转换的对应关系
scala java
obj.isInstanceOf[Class] obj instanceof Class
obj.asInstanceOf[Class] (Class)obj
classOf[Class] Class.class
不过,与类型检查和转换相比,模式匹配通常是更好的选择。例如:
p match { case s : Employee => … //将s作为Employee处理 case _ => // p不是Employee }

关于模式匹配,以后有机会会详细介绍。
12.4 匿名类 和Java一样,你可以通过包含带有定义或重写的代码块的方式创建一个匿名类,比如:
val alien = new Person ("Fred") { def greeting = "Greet4ings, Earthling! My name is Fred. " }

从技术上讲,这将会创建出一个结构类型的对象。该类型标记为Person{ def greeting: String }。你可以用这个类型作为参数类型的定义:
def meet (p: Person { def greeting: String}) { println(p.name + "says: " + p.greeting) }

12.5 抽象类 和Java一样,你可以用abstract关键字来标记不能被实例化的类,通常这是因为它的某个或某几个方法没有被完整定义。例如:
abstract class Person(val name: String) { def id: Int // 没有方法体,这是一个抽象方法 }

12.5 构造顺序和提前定义 12.5.1 构造顺序 当你在子类中重写val并且在超类的构造器中使用该值的话,其行为并不那么显而易见。有这样一个示例:动物可以感知其周围的环境。简单起见,我们假定动物生活在一维的世界里,而感知数据以整数表示。动物在默认情况下可以看到前方10个单位:
class Creature { val range: Int = 10 val env: Array[Int] = new Array[Int](range) }

不过蚂蚁是近视的:
class Ant extends Creature { override val range = 2 }

面临问题:
我们现在面临一个问题:range值在超类的构造器中用到了,而超类的构造器先于子类的构造器运行。确切地说,事情发生的过程是这样的:
  1. Ant的构造器在做它自己的构造之前,调用Creature的构造器
  2. Creature的构造器将它的range字段设为10
  3. Creature的构造器为了初始化env数组,调用range()取值器
  4. 该方法被重写以输出(还未初始化的)Ant类的range字段值
  5. range方法返回0。这是对象被分配空间时所有整型字段的初始值
  6. env被设为长度为0的数组
  7. Ant构造器继续执行,将其range字段设为2
虽然range字段看上去可能是10或者2,但env被设成了长度为0的数组。这里的教训是你在构造器内不应该依赖val的值。
解决方案
在Java中,当你在超类的构造方法中调用方法时,会遇到相似的问题。被调用的方法可能被子类重写,因此它可能并不会按照你的预期行事。事实上,这就是我们问题的核心所在range表达式调用了getter方法。有几种解决方式:
  1. 将val声明为final。这样很安全但并不灵活
  2. 在超类中将val声明为lazy。这样很安全但并不高效
  3. 在子类中使用提前定义语法
提前定义语句
所谓的"提前定义"语法,让你可以在超类的构造器执行之前初始化子类的val字段。这个语法简直难看到家了,估计没人会喜欢。你需要将val字段放在位于extends关键字之后的一个块中,就像这样:
class Ant extends { override val range=2 } with Creature

注意:超类的类名前的with关键字,这个关键字通常用于指定用到的特质。提前定义的等号右侧只能引用之前已有的提前定义,而不能使用类中的其他字段或方法。
提示:可以用-Xcheckinit编译器标志来调试构造顺序的问题。这个标志会生成相应的代码,以便在有未初始化的字段被访问的时候抛出异常,而不是输出缺省值。
说明:构造顺序问题的根本原因来自Java语言的一个设计决定,即允许在超类的构造方法中调用子类的方法。
12.6 Scala继承层级 【第十二讲|第十二讲 继承】下图展示了Scala类的继承层级:
  • 与Java中基本类型相对应的类,以及Unit类型,都扩展自AnyVal
  • 所有其他类都是AnyRef的子类,AnyRef是Java或.NET虚拟机中Object类的同义词。
  • AnyVal和AnyRef都扩展自Any类,而Any类是整个继承层级的根节点
  • Any类定义了islnstanceOf、aslnstanceOf方法,以及用于相等性判断和哈希码的方法
  • AnyVal并没有追加任何方法,它只是所有值类型的一个标记
    -n AnyRef类追加了来自Object类的监视方法wait和notify/notifyAII。同时提供了一个带函数参数的方法synchronized。这个方法等同于Java中的synchronized块。例如:
account.synchronized{ account.balance+=amount }

第十二讲|第十二讲 继承
文章图片
继承层级.png
  • 所有的Scala类都实现ScalaObject这个标记接口,这个接口没有定义任何方法
  • 在继承层级的另一端是Nothing和Null类型。
  • Null类型的唯一实例是null值。可以将null赋值给任何引用,但不能赋值给值类型的变量
  • Nothing类型没有实例。它对于泛型结构时常有用。
  • 空列表Nil的类型是List[Nothing],它是List[T]的子类型,T可以是任何类
注意: Nothing类型和Java或C++中的void完全是两个概念。在Scala中,void由Unit类型表示,该类型只有一个值,那就是()。虽然,Unit并不是任何其他类型的超类型。但是,编译器依然允许任何值被替换成()。
考虑如下代码:
class Hello { def printAny(x: Any) { println(x) } def printUnit(x: Unit) { println(x) } } object Hello { def main(args: Array[String]): Unit = { val hello = new Hello hello.printAny("Hello") // 将打印Hello hello.printUnit("Hello") // 将"Hello"替换成(),然后调用printUnit(()),打印出() } }

12.7 对象相等性 12.7.1 equals方法 在Scala中,AnyRef的eq方法检查两个引用是否指向同一个对象。AnyRef的equals方法调用eq。当你实现类的时候,应该考虑重写equals方法,以提供一个自然的、与你的实际情况相称的相等性判断。举例来说,如果你定义
class Item (val description : String,val price : Double)

你可能会认为当两个物件有着相同描述和价格的时候它们就是相等的。以下是相应的equals方法定义:
final override def equals(other: Any) = { val that = other.aslnstanceOf[Item] if (that == null){ false }else{ description == that.description && price == that.price } }

我们将方法定义为final,是因为通常而言在子类中正确地扩展相等性判断非常困难。问题出在对称性上。你想让a.equals(b)和b.equals(a)的结果相同,尽管b属于a的子类。与此同时还需注意的是,请确保定义的equals方法参数类型为Any。以下代码是错误的:
final def equals (other: Item) = { … }

这是一个不相关的方法,并不会重写AnyRef的equals方法。
17.7.2 定义hashCode 当你定义equals时,记得同时也定义hashCode。在计算哈希码时,只应使用那些你用来做相等性判断的字段。拿Item这个示例来说,可以将两个字段的哈希码结合起来:
final override def hashCode=13*description.hashCode+17*price.hashCode

    推荐阅读