@runzhliu
2018-06-13T00:41:22.000000Z
字数 7562
阅读 737
scala
继承
在本篇中,你将了解到 Scala 的继承与 Java 和 C++ 最显著的不同。要点包括:
在本篇中,我们只探讨类继承自另一个类的情况。继承特质的内容后面会详细介绍。
Scala 扩展类的方式和 Java 一样,使用 extends 关键字:
class Employee extends Person {
var salary=0.0
………
}
和 Java 一样,你在定义中给出子类需要而超类没有的字段和方法,或者重写超类的方法
和 Java 一样,你可以将类声明为 final,这样它就不能被扩展。你还可以将单个方法或字段声明为 final,以确保它们不能被重写。注意这和 Java 不同,在 Java 中,final 字段是不可变的,类似 Scala 中的 val。
在 Scala 中重写一个非抽象方法必须使用 override 修饰符。例如:
public class Person {
………
override def toString = getClass.getName+ " [name=" + name+"]"
}
override 修饰符可以在多个常见情况下给出有用的错误提示,包括:
最后一种情况是易违约基类问题的体现,超类的修改无法在不检查所有子类的前提下被验证。假定程序员 Alice 定义了一个 Person 类,在 Alice 完全不知情的情况下,程序员 Bob 定义了一个子类 Student,和一个名为 id 的方法,返回学生 ID。后来,Alice 也定义了一个 id 方法,对应该人员的全国范围的 ID。当 Bob 拿到这个修玫后,Bob 的程序可能会出问题,但在 Alice 的测试案例中不会有问题,因为 Student 对象返回的不再是预期的那个 ID 了。
在 Scala 中调用超类的方法和 Java 完全一样,使用 super 关键字:
public class Employee extends Person {
………
override def toString=super.toString+"[salary="+ salary+"]"
}
super.toString 会调用超类的 toString 方法,亦即 Person.toString
要测试某个对象是否属于某个给定的类,可以用 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 的类型检查和转换的对应关系:
不过,与类型检查和转换相比,模式匹配通常是更好的选择。例如:
p match {
case s : Employee => … //将s作为Employee处理
case _ => // p不是Employee
}
和 Java 或 C++ -样,你可以将字段或方法声明为 protected。这样的成员可以被任何子类访问,但不能从其他位置看到。与 Java 不同,protected 的成员对于类所属的包而言,是不可见的。如果你需要这样一种可见性,则可以用包修饰符。
Scala 还提供了一个 protected[this] 的变体,将访问权限定在当前的对象,类似介绍过的 private[this]。
一个类有一个主构造器和任意数量的辅助构造器,而每个辅助构造器都必须以对先前定义的辅助构造器或主构造器的调用开始。这样做带来的后果是,辅助构造器永远都不可能直接调用超类的构造器。子类的辅助构造器最终都会调用主构造器,只有主构造器可以调用超类的构造器。主构造器是和类定义交织在一起的,调用超类构造器的方式也同样交织在一起。这里有一个示例:
class Employee (name: String, age: Int, val salary: Double) extends Person(name, age)
这段代码定义了一个子类,和一个调用超类构造器的主构造器:
将类和构造器交织在一起可以给我们带来更精简的代码。把主构造器的参数当做是类的参数可能更容易理解。本例中的 Employee 类有三个参数:name、age 和 salary,其中的两个被"传递"到了超类。
在 Java 中,与上述定义等效的代码就要啰嗦得多:
public classEmployee extends Person {
private double salary;
public Employee (String name,int age,double salary) {
super(name, age)
this.salary = salary;
}
}
需要注意的是,在 Scala 的构造器中,你不能调用 super(params),不像 Java,可以用这种方式来调用超类构造器。Scala 类可以扩展 Java类。这种情况下,它的主构造器必须调用 Java 超类的某一个构造方法。例如:
class Square(x: Int, y: Int, width: Int) extends java.awt.Rectangle (x,y,width, width)
Scala 的字段由一个私有字段和取值器/改值器方法构成。你可以用另一个同名的 val 字段重写一个 val 或不带参数的 def。子类有一个私有字段和一个公有的 getter 方法,而这个 getter 方法重写了超类的 getter 方法。例如:
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" // …或类名
}
该示例展示了工作机制,但比较做作。更常见的案例是用 val 重写抽象的 def,就像这样:
abstract class Person {
def id: Int // 每个人都有一个以某种方式计算出来的ID
}
class Student (override val id: Int) extends Person // 学生ID通过构造器输入
注意如下限制:
在类中用 var 没有问题,因为你随时都可以用 getter/setter 对来重新实现。不过,扩展你的类的程序员就没得选了。他们不能用 getter/setter 来重写 var。换句话说,如果你给的是 var,所有的子类都只能被动接受。
和 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)
}
和 Java 一样,你可以用 abstract 关键字来标记不能被实例化的类,通常这是因为它的某个或某几个方法没有被完整定义。例如:
abstract class Person(val name: String) {
def id: Int // 没有方法体,这是一个抽象方法
}
在这里我们说每个人都有一个 ID,不过我们并不知道如何计算它。每个具体的 Person 子类,都需要给出 id 方法。在 Scala 中,不像Java你不需要对抽象方法使用 abstract 关键字,你只是省去其方法体。但和 Java 一样,如果某个类至少存在一个抽象方法,则该类必须声明为 abstract。
在子类中重写超类的抽象方法时,你不需要使用 override 关键字。
class Employee (name: String) extends Person(name) {
def id=name.hashCode // 不需要override关键字
}
除了抽象方法外,类还可以拥有抽象字段。抽象字段就是一个没有初始值的字段。例如:
abstract class Person {
val id : Int // 没有初始化,这是一个带有抽象的getter方法的抽象字段
var name : String // 另一个抽象字段,带有抽象的getter和setter方法
}
该类为 id 和 name 字段定义了抽象的 getter 方法,为 name 字段定义了抽象的 setter 方法。
生成的 Java 类并不带字段,具体的子类必须提供具体的字段,例如:
class Employee ( val id: Int) extends Person { // 子类有具体的id属性
var name=… // 和具体的name属性
}
和方法一样,在子类中重写超类中的抽象字段时,不需要 override 关键字。除此之外,你可以随时用匿名类型来定制抽象字段:
val fred = new Person {
val id=1729
var name="Fred"
}
当你在子类中重写 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 值在超类的构造器中用到了,而超类的构造器先于子类的构造器运行。确切地说,事情发生的过程是这样的:
虽然 range 字段看上去可能是10或者2,但 env 被设成了长度为0的数组。这里的教训是你在构造器内不应该依赖 val 的值。
在 Java 中,当你在超类的构造方法中调用方法时,会遇到相似的问题。被调用的方法可能被子类重写,因此它可能并不会按照你的预期行事。事实上,这就是我们问题的核心所在 range 表达式调用了 getter 方法。有几种解决方式:
所谓的"提前定义"语法,让你可以在超类的构造器执行之前初始化子类的val字段。这个语法简直难看到家了,估计没人会喜欢。你需要将 val 字段放在位于 extends 关键字之后的一个块中,就像这样:
class Ant extends {
override val range=2
} with Creature
注意:超类的类名前的 with 关键字,这个关键字通常用于指定用到的特质。提前定义的等号右侧只能引用之前已有的提前定义,而不能使用类中的其他字段或方法。
提示:可以用 -Xcheckinit
编译器标志来调试构造顺序的问题。这个标志会生成相应的代码,以便在有未初始化的字段被访问的时候抛出异常,而不是输出缺省值。
说明:构造顺序问题的根本原因来自 Java 语言的一个设计决定,即允许在超类的构造方法中调用子类的方法。在 C++ 中,对象的虚函数表的指针在超类构造方法执行的时候被设置成指向超类的虚函数表。之后,才指向子类的虚函数表。因此,在 C++ 中,我们没有办法通过重写修改构造方法的行为。Java 设计者们觉得这个细微差别是多余的,Java虚拟机因此在构造过程中并不调整虚拟函数表。
下图展示了 Scala 类的继承层级:
account.synchronized{account.balance+=amount}
注意:Nothing 类型和 Java 或 C++ 中的 void 完全是两个概念。在 Scala 中,void 由 Unit 类型表示,该类型只有一个值,那就是()。虽然,Unit 并不是任何其他类型的超类型。但是,编译器依然允许任何值被替换成()。考虑如下代码:
def printAny(x: Any) {println(x)}
def printUnit(x: Unit) {println(x)}
printAny("Hello") // 将打印Hello
printUnit("Hello") // 将"Hello"替换成(),然后调用 printUnit(()),打印出()
在 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 方法。
当你定义 equals 时,记得同时也定义 hashCode。在计算哈希码时,只应使用那些你用来做相等性判断的字段。拿 Item 这个示例来说,可以将两个字段的哈希码结合起来:
final override def hashCode = 13*description.hashCode + 17*price.hashCode
提示:你并不需要觉得重写 equals 和 hashCode 是义务。对很多类而言,将不同的对象看做不相等是很正常的。举例来说,如果你有两个不同的输入流或者单选按钮,则完全不需要考虑他们是否相等的问题。
在应用程序当中,你通常并不直接调用 eq 或 equals,只要用操作符就好。对于引用类型而言,它会在做完必要的 null 检查后调用 equals 方法。