1.扩展类(继承)
Scala扩展类的方式和Java一样,使用extends关键字:
class Employee extends Person{ var salary = 0.0 ...}
基本上,Scala中的继承和Java没有什么区别,唯一的区别就是:
和Java一样,你可以将类声明为final,这样它就不能被继承。你还可以将单个方法或字段声明为final,以确保他们不能被重写。注意这里和Java不同,在Java中final字段是不可变的,在Scala中时val。
2.重写方法
在Scala中重写一个非抽象的方法必须使用override修饰符。例如:
public class Person{ ... override def toString = getClass.getName + "[name=" + name + "]"}
在Scala中调用父类的方法和Java完全一样,使用super关键字:
public class Employee extends Person{ ... override def toString = super.toString + "[salary=" + salary + "]"}
3.类型检查和转换
要测试某个对象是否属于某个给定的类,可以用isInstanceOf方法。如果测试成功,你可以用asInstanceOf方法将引用转换为子类引用:
if(p.isInstanceOf[Employee]){ val s = p.asInstanceOf[Employee] //s的类型为Employee}
------------------------------------------------
如果你想要测试p指向的是一个Employee对象但又不是其子类的话,可以用:
//这种判断是精确判断,==号两边必须完全一样。isInstanceOf方法是模糊判断只要两边存在继承关系就可以返回true
if(p.getClass == classOf[Employee])
说明:getClass方法会返回引用对象的类型及其子类类型Class[_ <:A]。classOf方法得到的是指定对象类型Class[A]。
说明:这里总结下Scala中的类型界定:
- 协变 [+T], covariant (or “flexible”) in its type parameter T,类似Java中的(? extends T), 即可以用T和T的子类来替换T,里氏替换原则。
- 不变 不支持T的子类或者父类,只知支持T本身。
- 逆变 [-T], contravariant, 类似(? supers T) 只能用T的父类来替换T。是逆里氏替换原则。
- 上界: 只允许T的超类U来替换T。 [U >: T]
- 下界: 只允许T的子类U来替代T。 [U <: T]
-----------------------------------------------
Scala和Java中类型检查和转换
Scala | Java |
---|---|
obj.isInstanceOf[Cl] | obj instanceof Cl |
obj.asInstanceOf[Cl] | (Cl) obj |
classOf[Cl] | Cl.class |
4.受保护的字段和方法
和java或C++一样,你可以将字段或方法声明为protected。这样的成员可以被任何子类访问,但不能从其他位置看到。
与Java不同,protected的成员对于类所属的包而言,是不可见的。
Scala还提供了一个protected[this]的变体,将访问权限定在当前的对象,类似private[this]。
5.超类的构造
之前的学习中我们提到过,Scala类有一个主构造器和任意数量的辅助构造器,而每个辅助构造器都必须对先前定义的辅助构造器或主构造器的调用开始。
这样做的结果是,复制构造器永远都不可能直接调用超类的构造器。
子类的复制构造器最终都会调用主构造器。只有主构造器可以调用超累的构造器。
主构造器是和类定义交织在一起的。调用超类构造器的方式也同样交织在一起。例如:
class Person(val name: String, val age: Int){}class Employee(name: String, age: Int, val salary: Double) extends Person(name, age){}
注:在父类中定义了的参数,在子类中不必重新定义,只要名称相同即可,参考上面的name和age。
将类和构造器交织在一起可以给我们带来更精简的代码。把主构造器的参数当做是类参数可能更容易理解。本例中Employee类有三个参数:name、age和salary其中两个被“传递”到了超类。
------------------------------------------------
Scala可以扩展Java 类。这种情况写,他的主构造器必须调用java 超类的某一个构造方法:
class Square(x: Int, y:Int, width: Int) extends java.awt.Rectangle(x, y, width, width)
6.重写字段
Scala 中重写字段由如下限制(同时参考下表):
- def只能重写def。
- val只能重写另一个val或不带参数的def。
- var只能重写另一个抽象的var
用val | 用def | 用var | |
---|---|---|---|
重写val |
| 错误 | 错误 |
重写def |
| 和Java一样 | var可以重写getter/setter对。只重写getter会报错 |
重写var | 错误 | 错误 | 仅当超类的var是抽象的才可以 |
例如:
abstract class Person{ def id: Int }class Student(override val id: Int) extends Person
7.匿名子类
和Java一样,你可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类,比如:
val alien = new Person("Fred"){ def greeting = "Greetings,Earthling! My name is Fred."}
从技术上讲,这将会创建出一个结构类型的对象。该类型标记为Person{def greeting: String}。你可以用这个作为参数类型的定义:
def meet(p: Person{def greeting: String}){ println(p.name + "says: " + p.greeting)}
8.抽象类
和Java一样,你可以用abstract关键字类标记不能被实例化的类,通常因为某个或某几个方法没有被完整定义。例如:
abstract class Person(val name: String){ def id: Int //没有方法体——这是一个抽象方法}
在子类中重写超类的抽象方法时,你不需要使用override关键字。
class Employ(name: String) extends Person(name){ def id = name.hashCode //不需要override关键字}
9.抽象字段
除了抽象方法外,类还可以拥有抽象字段。抽象字段就是一个没有初始值的字段。例如:
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" }
10.构造顺序和提前定义
当你在子类中重写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值在超类的构造器中用到了,而超类的构造器先于子类的构造器运行。确切地说,事情发生的过程是这样的:
- Ant的构造器在做它自己的构造之前,调用Creature的构造器
- Creature的构造器将它的range字段设为10
- Creature的构造器为了初始化env数组,调用range()取值器
- 该方法被重写以输出(还未初始化的)Ant类的range字段值
- range方法返回0。这是对象被分配空间时所有整型字段的初始值
- env被设为长度为0的数组
- Ant构造器继续执行,将其range字段设为2
虽然range字段看上去可能是10或者2,但env被设成了长度为0的数组。这里的教训是你在构造器内不应该依赖val的值。
解决方案
在Java中,当你在超类的构造方法中调用方法时,会遇到相似的问题。被调用的方法可能被子类重写,因此它可能并不会按照你的预期行事。事实上,这就是我们问题的核心所在range表达式调用了getter方法。有几种解决方式:
- 将val声明为final。这样很安全但并不灵活
- 在超类中将val声明为lazy。这样很安全但并不高效
- 在子类中使用提前定义语法
提前定义语句
所谓的"提前定义"语法,让你可以在超类的构造器执行之前初始化子类的val字段。这个语法简直难看到家了,估计没人会喜欢。你需要将val字段放在位于extends关
键字之后的一个块中,就像这样:
class Ant extends { override val range=2} with Creature
注意:超类的类名前的with关键字,这个关键字通常用于指定用到的特质。提前定义的等号右侧只能引用之前已有的提前定义,而不能使用类中的其他字段或方法。
11.Scala继承层级
在Scala中,与Java基本类型相对应的类,以及Unit类型,都扩展自AnyVal。
所有其他类都是AnyRef的子类,AnyRef相当于Java中的Object
而AnyVal与AnyRef都扩展自Any,Any类是整个继承层级的根节点。
Any类定义了isInstanceOf、asInstanceOf方法,以及用于相等性的判断和哈希码的方法。
AnyVal并没有追加任何方法。他只是所有值类型的标记。
AnyRef类追加了来自Object类的监事方法wait和notify/notifyAll。同时提供了一个带函数参数的方法synchronized。这个方法等同于Java中的synchronized块。例如:
account.synchronized{ account.balance += amount}
说明:和Java一样,我建议你远离wait、notify和synchronized——除非你有充分的理由使用这些关键字而不是更高层次的并发结构。
所有的Scala类都实现ScalaObject这个标记接口,这个借口没有定义任何方法。
Scala继承层级的另一端是Nothing和Null类型。
Null类型的唯一实例时null值,你可以将null值赋给任何引用,但不能赋值给类型变量。
Nothing类型没有实例,它对于泛型结构时非常有用。举例来说,空列表Nil的类型是List[Nothing],它是List[T]的子类型,T可以试任何类。
注:Nothing类型和Java中的void完全是两个概念。在Scala中void用Unit表示,该类型只有一个值,那就是()。注意Unit并不是任何类型的超类型。但是依然允许任何值被替换成()。例如:
def printAny(x: Any){ println(x) }
def printUnit(x: Unit){ println(x) }
printAny("Hello") //将打印Hello
printUnit("Hello") //将“Hello”替换成(),然后调用printUnit(()),打印出()
12.对象相等性
在Scala中,AnyRef的eq方法检查了两个引用是否指向同一个对象。AnyRef的equals方法调用eq。当你实现类的时候应该考虑重写equals方法,以提供一个自然地与你的实际情况相称的相等性判断。
以下是相应的equals方法定义:
final override def equals(other: Any) = { ....}
注意: 请确保定义的equals方法参数类型为Any。这样并不会重写AnyRef的equals方法。
当你定义equals时,记得同时定义hashCode:
final override def hashCode = { ... }
在应用程序当中,你通常并不直接调用eq或equals,只要用==操作符就好。对于引用类型而言,他会在做完必要的null检查后调用equals方法。