Scala继承 |
摘要:
在本篇中,你将了解到Scala的继承与Java和C++最显著的不同。要点包括:
1. extends、final关键字和Java中相同
2. 重写方法时必须用override
3. 只有主构造器可以调用超类的主构造器
4. 你可以重写字段
在本篇中,我们只探讨类继承自另一个类的情况。继承特质的内容后面会详细介绍
扩展类 |
扩展关键字
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)
这段代码定义了一个子类:
class Employee ( name: String, age: Int, val salary: Double ) extends Person (name, age)
和一个调用超类构造器的主构造器:
class Employee ( name: String, age: Int, val salary: Double ) extends Person (name, age)
将类和构造器交织在一起可以给我们带来更精简的代码。把主构造器的参数当做是类的参数可能更容易理解。本例中的Employee类有三个参数:name、age和salary,
其中的两个被"传递"到了超类。
Scala与Java比较
在Java中,与上述定义等效的代码就要啰嗦得多:
public classEmployee extends Person{ // Java
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, 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通过构造器输入
注意如下限制:
■ def只能重写另一个def
■ val只能重写另一个val或不带参数的def
■ var只能重写另一个抽象的var
在类中用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值在超类的构造器中用到了,而超类的构造器先于子类的构造器运行。确切地说,事情发生的过程是这样的:
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语言的一个设计决定,即允许在超类的构造方法中调用子类的方法。在C++中,对象的虚函数表的指针在超类构造方法执行的时候被设置成指向超类的虚函数表。之后,才指向子类的虚函数表。因此,在c++中,我们没有办法通过重写修改构造方法的行为。Java设计者们觉得这个细微差别是多余的,Java虚拟机因此在构造过程中并不调整虚拟函数表。☆☆☆
Scala继承层级 |
下图展示了Scala类的继承层级:
■ 与Java中基本类型相对应的类,以及Unit类型,都扩展自AnyVal
■ 所有其他类都是AnyRef的子类,AnyRef是Java或.NET虚拟机中Object类的同义词。
■ AnyVal和AnyRef都扩展自Any类,而Any类是整个继承层级的根节点
■ Any类定义了islnstanceOf、aslnstanceOf方法,以及用于相等性判断和哈希码的方法
■ AnyVal并没有追加任何方法,它只是所有值类型的一个标记
■ AnyRef类追加了来自Object类的监视方法wait和notify/notifyAII。同时提供了一个带函数参数的方法synchronized。这个方法等同于Java中的synchronized块。例如:
account.synchronized{ account.balance+=amount }
■ 所有的Scala类都实现ScalaObject这个标记接口,这个接口没有定义任何方法
■ 在继承层级的另一端是Nothing和Null类型。
■ Null类型的唯一实例是null值。可以将null赋值给任何引用,但不能赋值给值类型的变量
■ Nothing类型没有实例。它对于泛型结构时常有用。
■ 空列表Nil的类型是List[Nothing],它是List[T]的子类型,T可以是任何类
注意: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(()),打印出()
对象相等性 |
定义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方法。
定义hashCode
当你定义equals时,记得同时也定义hashCode。在计算哈希码时,只应使用那些你用来做相等性判断的字段。拿Item这个示例来说,可以将两个字段的哈希码结合起来:
final override def hashCode=13*description.hashCode+17*price.hashCode
提示:你并不需要觉得重写equals和hashCode是义务。对很多类而言,将不同的对象看做不相等是很正常的。举例来说,如果你有两个不同的输入流或者单选按钮,则完全不需要考虑他们是否相等的问题。
在应用程序当中,你通常并不直接调用eq或equals,只要用—操作符就好。对于引用类型而言,它会在做完必要的null检查后调用equals方法
如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】。
如果,您希望更容易地发现我的新博客,不妨点击一下左下角的【关注我】。
如果,您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是【Sunddenly】。本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。