class Aclass A
trait B {
def say = " B_say "
}
trait C
trait D extends B {
override def say = " D_say "
}
trait E extends B {
override def say = " E_say "
}
trait F extends B {
override def say = super.say + " F_say "
}
然后,在scala的REPL里执行:
scala> val z:B = new A with B with D with E with C with F
z: B = $anon$1@1b664bb
scala> z.say
res6: String = " E_say F_say "
scala> val w:B = new A with B with E with D with C with F
w: B = $anon$1@14153c4
scala> w.say
res7: String = " D_say F_say "
先定义几个词语,譬如说,val a:Person = new Student(), 那么,对象a的实际类型是 Student,引用类型是Person。
在java和scala里面,通过this来调用的方法是动态绑定的,打个比喻就是,就好像有“一只手”,从对象的最外面,往对象的最里面深入,首先找到的那个方法就是要被实际执行的方法。这只神奇的手就是所谓的多态的“幕后黑手”。
但是,super关键字,在java和scala里面,内涵有很大的不一样。
在java,使用super.method(...)调用的时候,“一只手”从super语句所在的类(不包括该类本身)开始往里面逐层深入,首先找到的那个方法就是要被实际执行的方法。听上去,这个好像也是一个需要动态绑定的过程,其实不然。原因是,java的继承总是是线性的单一继承,所以,不管当前对象的实际类型是什么,super语句所在的类,它的父类,它的父类的父类,依次往下类推,都是在编译时期就可以确定下来的,也就是说,这只手会首先找到哪个方法,在编译时期,就可以确定下来的。所以,java里面,使用super的方法调用是静态绑定的,编译器有足够的信息能够知道“这只手”最终会找到哪个方法。
而在scala里面,trait实际上是多重继承的。不过,通过scala独有的线性化,一个trait或者class的父亲们,就被排成了一个线性的链式结构,这个链决定了使用this和super调用方法时的优先级。
在scala里,使用super.method(...)时,这只手从super所在的trait或者class(不包括它本身),沿着线性化得到的链往左边逐个查找,首先找到的那个方法就是实际要被执行的方法。这个过程是需要动态绑定的,因为编译器是没有办法知道该对象的线性化结果的。
用上面的例子说明,
scala> val z:B = new A with B with D with E with C with F
z: B = $anon$1@1b664bb
这是一个对象,叫z,它的实际类型是 A with B with D with E with C with F , 引用类型是B
线性化得到的链是: A B D E C F
scala> z.say
res2: String = " E_say F_say "
执行z.say的时候,实际上执行的是F里面的say,然后,F里面的say又调用了super.say,这时候,“那只手”从F开始,沿着链【A B D E C F】往左边找,第一个找到的say是E里面的say,所以得到了这样的输出:" E_say F_say "
scala> val w:B = new A with B with E with D with C with F
w: B = $anon$1@14153c4
这是一个对象w,实际类型是 A with B with E with D with C with F, 引用类型是B,它的线性化结构是 A B E D C F
在执行z.say,同样的事情发生了,“那只手”从F开始往左找,这一次,首先找到的是D里面的say,所以,调用的是D里面的say,输出自然就是 " D_say F_say "
关于单一继承与多重继承带来的意义:
在java(单一继承)里面,假设有一个对象a,它既是类型X,又是类型Y,那么X和Y必定具有“父子关系”,也就是说,其中一个是另一个的父类。
在scala(多重继承)里面,假设有一个对象a,它既是trait X,又是trait Y, X和Y可能具有父子关系,也可能是共享同一个祖先的“兄弟”,反正,它们的关系不再限定在“父子”上。
最后,总结一下:
java的super是静态绑定的。因为java的继承是单一继承,不管实际类型是什么,一个对象的“继承链”,从super所在类开始往左的部分,都是在编译时期就可以确定下来的。
scala的suoer是动态绑定的。因为scala允许多重继承,父亲类和trait们的优先顺序,是由对象的实际类型的线性化结果决定的,所以需要动态绑定。
最后的最后,再来一个总结= =
调用方法,有三种情况,如下
(1)当你看到 obj.method 的时候,你想知道实际被执行的是哪个类或者trait里的method:
你首先要知道obj所指向的对象的“实际类型”,然后做线性化,然后,从线性化得到的“链”的最右边的类或者trait开始,往左边查找,首先找到的那个method就是实际被执行的方法。
(2)当你看到 this.method 的时候(this可能被省略),你想知道实际被执行的是哪个类或者trait里的method:
你首先要知道this所指向的对象的“实际类型”,然后做线性化,然后,从线性化得到的“链”的最右边的类或者trait开始,往左边查找,首先找到的那个method就是实际被执行的方法。
(3)当你在某个类或者trait X 里面看到super.method的时候,你想知道实际被执行的是哪个类或者trait里的method:
你首先要知道这个super所指向的对象的“实际类型”,然后做线性化,然后,从线性化得到的“链”里,从X开始往左边找(不包括X本身),首先找到的那个method就是实际被执行的方法。
从上面的描述可以看出,这三种情况,都涉及到“你首先要知道这个xxx所指向的对象的实际类型,然后做线性化”,显然,在编译时期,编译器并不总是能知道xxx的实际类型,所以,这三种情况,编译器都必须做动态绑定。
======================================
线性化看起来好像很”学术”,很“严谨”
其实实际应用里,只要记住这两点
(1)右边的可以覆盖左边的
(2)同时混进多个trait,等价于分步逐个地混进多个trait,这样子,super关键字的含义就明显了