同名域覆盖
最近看完CSAPP之后,重新拿着《Java编程思想》这本书复习Java, 发现在年初快速过了一遍Java的时候,还有很多小细节没有注意。这两天看到“第七章:复用类”的时候,书上简单的写了一句话:当这么做(使用extends关键字继承)的时候,(导出类、子类)会自动得到基类中所有的域和方法。在157页上,有这么一段话,Sub实际上包含两个被称为field的域:它自己的和从Super处得到的。然后后边说一般不会发生这种情况,因为一般把域都设置成private的,副作用是只能用方法来访问。
上边这段到底是什么意思,我做了一系列实验终于搞明白了。本质上来说,子类用同名域去覆盖了父类里的域,实际上只是表象。这两个域还是可以使用不同的方法访问到,访问子类的域要用到子类的方法,而访问父类的域要用到父类的方法。来看下边的一系列例子:
基类A
public class A { private String s = "String in A class"; public String getS() { return s; } public void setS(String s) { this.s = s; } }
子类简单继承A
public class B1 extends A { public static void main(String[] args) { B1 b1 = new B1(); System.out.println(b1.getS()); b1.setS("New String set by B1"); System.out.println(b1.getS()); } }
这里就直接继承A,什么也不用做。执行测试的结果是:
String in A class New String set by B1
似乎还看不出来什么,继续向下看。
子类设置同名域
public class B2 extends A { private String s = "String in B2"; public static void main(String[] args) { B2 b2 = new B2(); System.out.println(b2.getS()); b2.setS("New String set by B2"); System.out.println(b2.getS()); System.out.println("-----------------"); } }
执行测试的结果是:
String in A class New String set by B2
这里已经看出一点端倪来了,方法getS和setS似乎与B2类的私有变量String in B2毫无关系。看起来使用父类的方法,操作的是父类里的那个字符串s域。
子类重写getS方法
public class B3 extends A { private String s = "String in B3"; @Override public String getS() { return s; } public static void main(String[] args) { B3 b3 = new B3(); System.out.println(b3.getS()); b3.setS("New String set by B3"); System.out.println(b3.getS()); } }
这次的执行结果是:
String in B3 String in B3
这就很有意思了,子类重写的getS方法返回了子类的s域,父类的那个setS显然设置的不是子类的s域。这说明实际上设置的是父类的同名s域。
子类重写getS和setS方法
public class B4 extends A { String s = "String in B4"; @Override public String getS() { return s; } @Override public void setS(String s) { this.s = s; } public static void main(String[] args) { B4 b4 = new B4(); System.out.println(b4.getS()); b4.setS("New String set by B4"); System.out.println(b4.getS()); } }
这次的执行结果是:
String in B4 New String set by B4
从结果来看,两个覆盖的方法完全都操作了子类的s域,和父类的同名域没有关系了。为了验证文章开始说的子类对象是不是包含两个域,添加两个调用父类的方法来访问看看。
子类添加父类方法调用
public class B5 extends A { String s = "String in B5"; @Override public String getS() { return s; } @Override public void setS(String s) { this.s = s; } public String getAS() { return super.getS(); } public void setAS(String s) { super.setS(s); } public static void main(String[] args) { B5 b5 = new B5(); System.out.println(b5.getS()); System.out.println(b5.getAS()); b5.setS("New String set by B5"); b5.setAS("New String set by B5 to A"); System.out.println(b5.getS()); System.out.println(b5.getAS()); } }
这次的执行结果是:
String in B5 String in A class New String set by B5 New String set by B5 to A
可以看到,确实通过子类和父类的方法,访问了两个同名但是分属于子类和父类的域s。这两个域虽然同名,但是互相独立。
Python中的情况
我记得刚开始学Python中的面向对象时候,简单的理解成同名域会覆盖掉原来的域,其实并不是这样,应该和Java一样两个域都可以访问。为了测试一下Python中是什么情况,也写了简单的Python代码进行测试:
class A: def __init__(self): self.__name = "A-name" def getName(self): return self.__name def setName(self, name): self.__name = name class B1(A): pass class B2(A): def __init__(self): A.__init__(self) self.__name = "B2-name" def getName(self): return self.__name def getAName(self): return super().getName() class B3(A): def __init__(self): A.__init__(self) self.__name = "B3-name" def getName(self): return self.__name def setName(self, name): self.__name = name def getAName(self): return super().getName() print("--------以下是B1--------") b1 = B1() print(b1.getName()) b1.setName("Set Name by b1") print(b1.getName()) print("--------以下是B2--------") b2 = B2() print(b2.getName()) print(b2.getAName()) b2.setName("Set Name by b2") print(b2.getName()) print(b2.getAName()) print("--------以下是B3--------") b3 = B3() print(b3.getName()) print(b3.getAName()) b3.setName("Set Name by b3") print(b3.getName()) print(b3.getAName())
运行结果是:
--------以下是B1-------- A-name Set Name by b1 --------以下是B2-------- B2-name A-name B2-name Set Name by b2 --------以下是B3-------- B3-name A-name Set Name by b3 A-name
这次的同名域是__name。
B1的运行结果完全就和Java中的一样。
B2中只重写了getName方法,添加了调用父类方法的getAName方法,可以看到两个方法返回了不同的结果。直接调用setName方法只修改了父类的同名域。
B3重写了setName方法,很显然这次只修改了子类的域,并没有修改父类的同名域。
因此Python中的继承和Java里是一样的,本该如此,毕竟面向对象的理念是一致的。
不过这里还有个小细节,B1直接继承A,什么都没写,解释器应该是自动调用了父类的初始化函数。而在B2和B3类里,如果去掉:
A.__init__(self)
这一行,getAName() 方法会报错,找不到变量,这是因为Python解释器在子类有初始化方法的时候不会自动去调用父类初始化方法。在Java里,子类的构造器里会默认递归调用完全部的基类构造器,以保证子类对象生成的时候所有基于父类的域都可用。Python这点竟然没有做到,真是意外。
用同名域覆盖基类的域不是一个好的编程实践,不过万一遇到了,也要心里有数,总算搞清楚了这个令人迷惑的地方。