Java 补强 03 继承中出现同名域的情况

作者 柚爸

同名域覆盖

  1. 基类A
  2. 子类简单继承A
  3. 子类设置同名域
  4. 子类重写getS方法
  5. 子类重写getS和setS方法
  6. 子类添加父类方法调用
  7. Python中的情况

最近看完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这点竟然没有做到,真是意外。
用同名域覆盖基类的域不是一个好的编程实践,不过万一遇到了,也要心里有数,总算搞清楚了这个令人迷惑的地方。