一道题引起的惨案(加深理解)

题目:代码的输出?java

public class Base
{
    private String baseName = "base";
    public Base()
    {
        callName();
    }
 
    public void callName()
    {
        System. out. println(baseName);
    }
 
    static class Sub extends Base
    {
        private String baseName = "sub";
        public void callName()
        {
            System. out. println (baseName) ;
        }
    }
    public static void main(String[] args)
    {
        Base b = new Sub();
    }
}

答案:null函数

解答:this


咱们在仔细的观察一下题目,能够知道,这道题无非就是考察咱们三个知识点,第一,类的加载机制以及类的初始化过程;第二,继承的相关知识,其中这里涉及到子类继承父类的时候,同名的属性不会覆盖父类,只是会将父类的同名属性隐藏;第三,多态性,多态性就是让实现与接口进行分离,在这道题目中,在父类的构造方法中调用了虚函数形成多态debug

居然咱们上面就提到这个题目就是考察咱们三个知识点,那么咱们就根据题目对这三个知识点进行逐一击破code

1.类加载的机制和程序运行的顺序对象

咱们经过 Debug 能很好的了解程序的运行顺序,由于 new 了一个 Sub 对象,且 Sub 类中没有重写构造函数,所以会调用父类的构造函数,父类 Base 的构造函数中调用了 callName 方法,所以就在父类的 callName 方法中的输出语句打一个断点,最后由于子类的 Sub 重写了 callName 方法, 所以也在子类中重写的 callName 方法中打一个断点。最后经过 debug 咱们能够看出程序的运行顺序blog


 

知道了程序的运行顺序以后,咱们还需知道一个知识点,那就是类的实例变量的初始化过程,也就是题目中成员变量 baseName 的初始化过程。继承

咱们都知道,一个类一旦被加载链接初始化,它就能够随时被使用了,程序能够访问它的静态字段,调用静态方法,或者建立它的实例。在 JAVA 程序中类能够被明确或者隐含地实例化有四种途径:(1)明确使用 new 操做符;(2)调用 Class 或者 Constructor 对象的 newInstance() 方法;(3)调用任何现有对象的 clone() 方法;(4)或者经过 objectInputStream 类的 getObject() 方法反序列化。虚拟机建立一个新的实例时,都须要在堆中为保存对象的实例分配内存。全部在对象的类中和它的父类中声明的变量(包括隐藏的实例变量)都要分配内存。一旦虚拟机为新的对象准备好堆内存,它当即把实例变量初始化为默认的初始值。接口

2.继承内存

题目中 Sub 类继承了 Base 类,关于继承,一个基本全部人都知道的知识点,不过这里仍是贴出来

Java保证了一个对象被初始化前其父类也必须被初始化。有下面机制来保证:Java强制要求任何类的构造函数中的第一句必须是调用父类构造函数或者是类中定义的其余构造函数。若是没有构造函数,系统添加默认的无参构造函数,若是咱们的构造函数中没有显示的调用父类的构造函数,那么编译器自动生成一个父类的无参构造函数

3.多态

父类中的构造函数调用了 callName 方法,在题目中是经过 new Sub() 对象,所以调用的是子类 Sub 类中的 callName 方法,所以当前的 this 是指 Sub 类中的。

好了,最后咱们根据运行顺序分析整个过程
1.Base b = new Sub();

在 main 方法中声明父类变量b对子类的引用,JAVA类加载器将Base,Sub类加载到JVM;也就是完成了 Base 类和 Sub 类的初始化

2.JVM 为 Base,Sub 的的成员开辟内存空间且值均为 null

在初始化 Sub 对象前,首先 JAVA 虚拟机就在堆区开辟内存并将子类 Sub 中的 baseName 和父类 Base 中的 baseName(已被隐藏)均赋为 null ,至于为何 Base 类中的 baseName 为何会被隐藏,上面的知识点也已经说明,就是子类继承父类的时候,同名的属性不会覆盖父类,只是会将父类的同名属性隐藏

3.调用父类的无参构造

调用 Sub 的构造函数,由于子类没有重写构造函数,默认调用无参的构造函数,调用了 super() 。

4.callName 在子类中被重写,所以调用子类的 callName();

调用了父类的构造函数,父类的构造函数中调用了 callName 方法,此时父类中的 baseName 的值为 base,但是子类重写了 callName 方法,且 调用父类 Base 中的 callName 是在子类 Sub 中调用的,所以当前的 this 指向的是子类,也就是说是实现子类的 callName 方法

5.调用子类的callName,打印baseName

实际上在new Sub()时,实际执行过程为:

public Sub(){
    super();
    baseName = "sub"; 
}

可见,在 baseName = "sub" 执行前,子类的 callName() 已经执行,因此子类的 baseName 为默认值状态 null 。

main 方法调用b.callName(),输出的是base  由于java中,向上造型呈现的多态性仅仅针对成员函数,成员属性不具备多态性.
b.callName(); //调用的是子类的函数(动态绑定)
b.baseName; // 调用的是父类的属性

构造器的初始化顺序大概是:父类静态块 ->子类静态块 ->父类初始化语句 ->父类构造函器 ->子类初始化语句 ->子类构造器。

做者:fuck两点水 连接:https://www.jianshu.com/p/39f91f3fba32

相关文章
相关标签/搜索