最近在看代码的时候遇到一个天坑,由于习惯性思维,可能大部分人都会掉近这个坑,所以拿出来记录一下

子类使用super调用的父类方法里,再调用父类的方法

先来看一段代码(该段代码只是作为测试用,正常编码时类名一定要规范编写)

package supertetst;

/**
 * @Author: x1aolone
 * @Date: 2019/11/8 22:11
 */

class father {
    public void invoke () {
        say();
    }

    public void say () {
        System.out.println("father say");
    }
}

class child extends father{
    @Override
    public void invoke () { 
    	super.invoke(); 
    }

    @Override
    public void say () { 
    	System.out.println("child say"); 
    }
}


public class test {
    public static void main(String[] args) {
        child c = new child();
        c.invoke();
    }
}

猜猜上面的代码运行结果是什么?
“child say”

你没有看错,答案不是想象之中的"father say",实际上调用的是子类中的say方法。

在刚看到代码时,你可能想当然地以为代码的执行顺序是:子类invoke() => 父类invoke() => 父类say()

但是,通过debug断点发现,上面这段代码的执行顺序是:子类invoke() => 父类invoke() => 子类say()

OK,虽然上面的例子有点“毁三观”,但是你可能以为你已经明白super的使用方法了,那么再看看下面这个粒子

子类使用super调用的父类方法里,再调用父类的变量

package supertetst;

/**
 * @Author: x1aolone
 * @Date: 2019/11/8 22:11
 */

class father {
    public String a = "father";

    public void invoke () {
        System.out.println(a);
    }
}

class child extends father{

    public String a = "child";

    @Override
    public void invoke () { super.invoke(); }
}


public class test {
    public static void main(String[] args) {
        child c = new child();
        c.invoke();
    }
}

在这一段例子中,变量a是公共变量,可以由子类继承,于是父类的变量a会被子类的变量a隐藏,类似@Override

按照我们上一个例子得出的教训,子类通过super调用的父类invoke方法,在父类Invoke方法里再调用say方法,实际上调用的是子类的invoke方法,那么此时父类的invoke方法使用的变量a应该也是子类覆盖过的变量a,也就是输出"child"

很遗憾!结果是输出“father”,也就是父类自己的变量a

那么经过以上两个例子,我们可以发现:

子类通过super调用父类的invoke方法,在父类的invoke方法中,如果调用方法,则优先选择子类重写的方法,如果使用变量,则选择父类自己的变量(这里可没有优先二字)

对于不关心原理的人来说,你只需要看到这个坑,记住上面那句话,不要再想当然采坑就好了,对于喜欢问“为什么”的同学,我们下面就来聊聊它到底是为什么

成员变量的隐藏和方法重写

如果父类中存在一个可继承的变量,并且子类中存在同类型、同名的变量,那么父类的变量会被隐藏,称为成员变量的隐藏
如果父类中存在一个可继承的方法,并且子类中存在同名、同参数类型、同参数个数、同返回值的方法,那么父类的方法会被隐藏,成为方法重写

无论是变量还是方法,其实都只是被隐藏了而已,在《JAVA程序设计精编教程》中是这么说的:“如果子类想使用被隐藏的方法或成员变量,必须使用关键字super”

super关键字

在JAVA中,super关键字用来操作被隐藏的成员变量和方法,对于变量,使用方式如super.x,对于方法,使用方式如super.x()

到这里,JAVA的基础知识就复习得差不多了,需要注意的是,例子1和例子中在父类和子类中同时出现的变量、方法,其实只是在子类中被隐藏了而已,在访问时是需要通过super关键字才能访问的。

回到我们的例子,先看例子1,为了方便阅读,再贴一遍代码

package supertetst;

/**
 * @Author: x1aolone
 * @Date: 2019/11/8 22:11
 */

class father {
    public void invoke () {
        say();
    }

    public void say () {
        System.out.println("father say");
    }
}

class child extends father{
    @Override
    public void invoke () { 
    	super.invoke(); 
    }

    @Override
    public void say () { 
    	System.out.println("child say"); 
    }
}


public class test {
    public static void main(String[] args) {
        child c = new child();
        c.invoke();
    }
}

在例子1中,主函数调用child.invoke(),代码继续往下执行,super.invoke(),这个时候就要注意了,子类通过super关键字访问子类中被隐藏invoke方法,说到底还是在子类中,此时执行被隐藏的invoke()方法,内容为say(),自然就会调用子类中重写的say(),因为子类中被隐藏的say()是需要使用super才能调用的

再来看例子2,为什么变量就不会用子类的变量了呢?

package supertetst;

/**
* @Author: x1aolone
* @Date: 2019/11/8 22:11
*/

class father {
   public String a = "father";

   public void invoke () {
       System.out.println(a);
   }
}

class child extends father{

   public String a = "child";

   @Override
   public void invoke () { super.invoke(); }
}


public class test {
   public static void main(String[] args) {
       child c = new child();
       c.invoke();
   }
}

在《JAVA程序设计精编教程》中讲述成员变量的隐藏时,有这么一句话:“需要注意的是,子类对象仍然可以调用从父类继承的方法操作隐藏的成员变量”

子类隐藏掉的invoke()方法,是从父类继承的,从上面这句话可以看出,从父类继承的方法所操作的变量仍然是从父类的继承变量。

于是乎,子类调用super.invoke()来使用那个从父类中继承的、被隐藏的invoke()方法,这个方法中操作的变量仍然是从父类中继承的、被隐藏的变量,所以输出的答案是"father"

当然了,这个机制看起来跟我上面例子1提到的原理有点冲突,既然是调用隐藏的方法,访问变量就应该访问子类的变量不是?

所以个人觉得例子2出现的问题可能是个机制问题,由于面向对象编程时常常需要封装,变量设为private后子类无法继承,于是使用继承getter、setter的方法来访问父类私有变量,由于这种情况遇见得多,我们会下意识地认为父类方法调用某个方法时调用的是自己的方法,但实际上无论例子1的子类invoke()是否存在,输出都是"child",通过子类对象调用的父类方法invoke()在实际执行时调用的是子类已经重写的say()方法,即使把子类对象上转型成父类对象以后,再调用invoke(),结果仍然是调用子类的say(),父类的say()根本不会被调用

上面这个问题可能跟jvm有关,先埋个坑,等以后了解到jvm的相关知识,再回来填

====================================

对这个问题有疑惑的同学可以看一下《深入理解Java虚拟机》第三版的 8.3.2 分派 里面的 2.动态分派 ,在那一小节的结尾有一段代码,讲的就是这个问题,与虚拟机的静态分派和动态分派有关(其实严格来说静态分派发生在编译阶段,不是由虚拟机执行)

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐