今天和朋友聊到scala继承的时候,发现有一些问题,我也没有想清楚。无论从那个角度想,都觉得有点怪怪的,帮贴出来,跟大家分享一下,帮忙指出理解不到位的地方。
先来说一下问题代码,比较简单,就是一个简单的继承:
class SuperClass(var name:String){
}
class SubClass(name :String) extends SuperClass(name){
def updateName(newName:String)={
name = newName //编译报错
}
}
这个updateName方法,肯定是编译不过去。 在给name赋值那一行会报错:Reassigment to val. 问题就这样出来了!
根据《快学scala》上的说的,“统一访问原则”,当直接调用一个属性时,会默认调用他的get方法,当给一个属性赋值是,会调用他的set方法,即_=()。可是这里为什么会报错?
反编译一下代码,如下:
public class SuperClass
{
private String name;
public String name()
{
return this.name; }
public void name_$eq(String x$1) { this.name = x$1; }
public SuperClass(String name)
{
}
}
public class SubClass extends SuperClass
{
public void updateName(String newName)
{
}
public SubClass(String name)
{
super(name);
}
}
其实这样的Java类,在子类我们肯定是访问不到父类的属性name,但是可以访问他的getter setter方法。但是你访问他的属性的时候,报的是:未定义的属性!这好像和我们scala中提示的错误不是一回事。
有一位朋友说:SubClass中的name重写了SuperClass中的name。那么就有以下几点疑虑:
1. 为何反编译出来的SubClass,没有看到重写迹象?
2.scala明确规定,只要出现override情况,一定要显式申明override,这里并没有
3.假如这种说法成立,那么SubClass中的name的作用域相当于变成了private[this] val,看上去好像是符合逻辑,只能取值,不能赋值。但是接着写了一个测试,发现这种假设也不成立:
object extendsTest extends App{
val c = new SubClass("aaa")
println(c.name)
}
看,我们仍然还是可以在外部对象中访问到SubClass的name属性! 那这种说法好像也不成立!
这时候,另外一位朋友,提出这么情况:将构造器的参数换个名字,就OK了,代码如下:
class SuperClass(var name:String){
}
class SubClass(name1 :String) extends SuperClass(name1){
def updateName(newName:String)={
name = newName
}
}
注意SubClass的参数名叫:name1。发现,现在居然可给name赋值,我们看一下反编译的源代码:
public class SubClass extends SuperClass
{
public void updateName(String newName)
{
name_$eq(newName);
}
public SubClass(String name1)
{
super(name1);
}
}
好像确实是符合我们预期了,确实是最能解释我们情况的问题了,事实上问题也这么解决。
----------------------------------------------------------------------------------分割线----------------------------------------------------------------------------------------------
但是,故事还没有就此结束,因为对于我们这些追根溯源的码农来说,怎么会就此罢手列,我们不光要知道How,还要知道Why。
那我们再来一点改造:在updateName里面,打印一下name1。
class SubClass(name1 :String) extends SuperClass(name1){
def updateName(newName:String)={
name = newName
println(name1)
}
}
再反编译一下:
public class SubClass extends SuperClass
{
private final String name1;
public void updateName(String newName)
{
name_$eq(newName);
Predef..MODULE$.println(this.name1);
}
public SubClass(String name1)
{
super(name1);
}
}
这说明了一个情况:我们在构造器里面使用的参数,当有一个以上方法使用到时,将会提升为类成员属性。事实上scala,也确实是这么约定的。
那么想到这,我又跑出了一个想法:如果变量名,仍然还是name,我在update方法里面打印一下name,即println(name),也就是说只调get方法。反编译出来代码会是啥样的列?拭目以待。
public class SubClass extends SuperClass
{
private final String name;
public void updateName(String newName)
{
Predef..MODULE$.println(this.name);
}
public SubClass(String name)
{
super(name);
}
}
果然,跟前面一样,只要参数被一个以上方法使用,就会提升为属性。 这时候的确实是SubClass的name覆盖了SuperClass的name属性。
那么我们是不是可以这样理解: 之所以报错,是因为我们试图在一个方法里面给SubClass赋值时,scala会把name属性提升为SubClass的成员属性。而name的访问域是 private[this] val的,所以赋值会编译不过去!而当我们把name更名为name1时,name便是从SuperClass继承过来的name.因为是var声明,所以赋值操作是没有问题的。罪魁祸首就是:当构造器的参数在一个以上方法中使用时,会自动提升为
所以最终的解决办法就是我们前面标红的:在发生继承时,子类构造器参数的名子不要与父类的属性名同名。
-------------------------------------------------------------------------------------------------再次分割-------------------------------------------------------------------------------------------------------------------------
故事还没有结束,我们还再举个容易出问题的例子来让你看到这个坑:
试着跑下下面这段scala代码:
class SuperClass(var name:String){
def sayName()={
println(name + " in SuperClass.")
}
}
class SubClass(name:String) extends SuperClass(name){
def sayNameOther()={
println(name + " in SubClass.")
}
}
object TestRunner extends App{
val s1 = new SubClass("aaa")
s1.name = "bb"
s1.sayName()
s1.sayNameOther()
}
运行一下,看看是不是很容易掉进这个坑里面。