本文主要内容:
1、继承
2、特质
1、继承
我们一步一步来完善定义一个父类和子类。
第一步:定义一个抽象类
abstract class Element{
def contents:Array[String]
}
抽象类的方法没有实现,抽象类的类本身必须被abstract
修饰。而方法只要没有实现,它就是抽象的,不需要加abstract
。
第二步:定义无参方法
abstract class Element{
def contents:Array[String]
def height:Int = contents.lenght
def width:Int = if(height==0) 0 else contents(0).length
}
无参数方法在Scala里十分常见。相对的如果带有空括号的方法,比如def height():Int
,被称为空括号方法。推荐使用的方式是:只要方法中没有参数且方法仅能够通过读取所包含对象的属性去访问可变状态(特指方法不能改变可变状态),就使用无参数方法。这个惯例支持统一访问原则。简单来说,就是说客户代码不应该由属性是通过字段实现还是方法实现而受到影响。例如我们可以把上述代码中的def
换成 val
,这种转换会使运行更快。在Scala中调用空括号方法,省略括号是合法的,在无参数方法和空括号方法之间有着极大的自由程度。
abstract class Element{
def contents:Array[String]
val height:Int = contents.lenght
val width:Int = if(height==0) 0 else contents(0).length
}
总之,Scala鼓励定义将不带参数且没有副作用的方法定义成为无参数方法的风格。
第三步:子类
class ArrayElement(conts:Array[String]) extends Elements{
def contents:Array[String] = conts
}
ArrayElement
类用extends
继承Elements
,ArrayElement
获得了Elements
的所有非私有属性成员,ArrayElement
是Elements
的子类,Elements
是ArrayElement
的超类。如果一个类没有extends
,编译器会将该类隐式地继承scala.AnyRef
,如同Java的Object
类一样。
Scala中,子类可以重写父类属性,可以实现父类抽象属性,继承父类非私有属性。这几点都和Java相同。
第四步:重写方法和字段
Scala中的字段和方法属于相同的命名空间,这让字段可以重写无参数方法。比如下面:
class ArrayElement(conts:Array[String]) extends Elements{
//实现父类的无参数方法(def contents:Array[String])
val contents:Array[String] = conts
}
当然,优点有时候也是缺点,即Scala在同一个类里,不能用同样的名称去定义方法和字段。
Java的命名空间:字段\类型\方法\包
Scala的命名空间:值(字段、方法、包还有单例对象)\类型(类和特质)
第五步:调用超类构造器和override关键词
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中的相同。
第六步:定义final成员
final def a : Int = {...}
final class b(a:Int){
...
}
修饰类,类不能被继承,修饰方法和字段,不能被重写。这点也同Java中的一样。
第七步:定义工厂方法
定义一个工厂方法来生产Element的各个子类实例,隐藏了实现细节。
object Element {
/**
* 私有的类,隐藏实现细节
*
* @param cons
*/
private class ArrayElment(cons: Array[String]) extends Elment {
val contents = cons
}
private class UniformElement(ch: Char, override val height: Int, override val width: Int) extends Elment {
private val line = ch.toString * width
def contents = new Array[String](3);
}
private class LineElment(s: String) extends ArrayElment(Array(s)) {
override val height: Int = 1
override val width: Int = s.length
}
/**
* 通过公有的方法,提供私有类的实例
*
* @param contents
* @return
*/
def elem(contents: Array[String]): Elment = new ArrayElment(contents)
def elem(chr: Char, width: Int, height: Int): Elment = new UniformElement(chr, width, height)
def elem(line: String): Elment = new LineElment(line)
}
2、特质
特质是Scala中代码复用的基本单元。特质中封装了方法和字段的定义。特质功能非常强大。类可以混入任意多个特质,其主要的运用方式是:拓宽瘦接口为胖接口和实现可堆叠的改变。
trait A{
val tyl : String
def method : Int {
...
}
}
trait C{
def method : Int {
...
}
}
class D {
...
}
class B with A {
override def toString = "B with A"
}
class B extends D with A with C{
...
}
一旦特质被定义,就可以用extends
或with
关键字,把它混入类中。特质就像带有具体方法的Java接口,它可以有正常的变量、方法,即类可以做到的事情,特质都可以除了没有构造器。还有一点类的super调用是静态绑定的,而特质则是动态绑定的,这也决定了特质可以实现堆叠式改变。
拓宽瘦接口为胖接口
瘦接口和胖接口的对阵体现了面向对象设计常会面临的实现者和接口用户之间的权衡。胖接口有更多方法,对于调用者来说更为方便,客户可以选择一个完全符合他们功能需要的方法。瘦接口有较少的方法,对于实现者来说简单,但是调用者却要为此写更多的代码,由于没有更多可选的方法,他们或许不得不选一个不太完美匹配的或是重写额外代码。
Java中的接口常常是过瘦而非过胖。Java中的CharSequence
接口定义了4个方法。如果CharSequence
包含全部的String
接口,那实现者将为CharSequence
定义一堆可能自己不需要定义的方法。
试想一下,一个特质拥有几个抽象方法,剩余的是大量具有具体方法的方法。接口用户只需要混入该特质,简单地实现抽象方法,就可以拥有所有的具体方法。这是Java中不能做到的。
堆叠式改变
我们考虑一个这样的例子:
一个整数队列有两种操作,put
:把整数放入队列,get
:从尾部取出整数。队列先进先出。假如一个类实现了这样的队列,你可以定义特质执行如下改动:
Doubling
:把所有放入到队列的数字加倍Incrementing
:把所有放入到队列的数字增值Filtering
:过滤调负整数
//抽象整数队列
abstract class IntQueue{
def put(x:Int)
def get : Int
}
//队列实现
class BasicIntQueue extends IntQueue{
private val buf = new ArrayBuffer[Int]
def put(x:Int) {buf +=x}
def get = buf.remove(0)
}
//继承了IntQueue,表明Doubling只能混入扩展了IntQueue的类中
trait Doubling extends IntQueue{
abstract override def put(x:Int) {super.put(2*x)}
}
class MyQueue extends BasicIntQueue with Doubling{
}
val MyQueue = new MyQueue
//等价于
val MyQueueC = new BasicIntQueue with Doubling
接下来,我们再定义2个特质,来看一下堆叠改变。
//增值特质
trait Increamenting extends IntQueue{
abstract override def put(x:Int) {super.put(x+1)}
}
//过滤特质
trait Filtering extends IntQueue{
abstract override def put(x:Int){
if(x>=0) super.put(x)
}
}
//粗略地说,越靠近右侧的特质越先起作用,当我们调用的最右侧的特质调用了super,它会先调用其
//左侧特质的方法,再以此类推。这里先过滤再增量
val queue = new BasicIntQueue with Incrementing with Filtering
特质和多重继承
特质和多重继承一个尤为重要的区别在于super
。特质是动态绑定的,super
调用是由类和被混入到类的特质的线性化决定的,而继承是静态绑定的。
举个例子来展示线性化的顺序:
假设我们有一个类Cat
,继承自超类Animal
以及两个特质Furry
和FourLegged
。FourLegged
又扩展了另一个特质HasLegs
。
class Animal
trait Furry extends Animal
trait HasLegs extends Animal
trait FourLegged extends HasLegs
class Cat extends Animal with Furry with FourLegged
Cat线性次序为:
Cat——>FourLegged——>HasLegs——>Furry——>Animal——>AnyRef——>Any
总结:
- 如果行为不会被重用,做成类。如果行为要在多个不相关的类中重用,做成特质。
- 如果希望从Java代码里继承它,使用抽象类。因为在Java里继承特质是很笨拙的,而继承Scala的类和继承Java的类完全一样。特殊情况是,若特质只有抽象属性将被直接翻译成Java接口,则可以在Java代码中继承该特质。
- 如果我们计划以编译后的方式发布它,并且希望外部组织能够写一些继承它的类,那我们应该更倾向于使用抽象类。因为当特质获得或者失去成员,所有继承自它的类就算没有改变也要被重新编译。如果客户仅需要调用行为,而不是继承自它,则特质没有问题。
- 如果效率非常重要,则应该更倾向于使用类。大多数Java运行时都能让类成员的虚方法调用快于接口方法调用。特质被翻译成接口,因此会付出微小的性能代价。