最近真的发现自己越来越懒了,虽然现在有点晚了,可是内疚完之后,还是得更新每个星期一篇的面试题,找了很多面试题,发现有些很基础的就没必要分享出来,最后找了这么一篇经典的面试题,一开始我也还真做错了,话不多说。来看下 2015 携程的 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 。为什么呢?
我们在仔细的观察一下题目,可以知道,这道题无非就是考察我们三个知识点,第一,类的加载机制以及类的初始化过程;第二,继承的相关知识,其中这里涉及到子类继承父类的时候,同名的属性不会覆盖父类,只是会将父类的同名属性隐藏;第三,多态性,多态性就是让实现与接口进行分离,在这道题目中,在父类的构造方法中调用了虚函数造成多态
竟然我们上面就提到这个题目就是考察我们三个知识点,那么我们就根据题目对这三个知识点进行逐一击破
1.类加载的机制和程序运行的顺序
我们通过 Debug 能很好的了解程序的运行顺序,因为 new 了一个 Sub 对象,且 Sub 类中没有重写构造函数,因此会调用父类的构造函数,父类 Base 的构造函数中调用了 callName 方法,因此就在父类的 callName 方法中的输出语句打一个断点,最后因为子类的 Sub 重写了 callName 方法, 因此也在子类中重写的 callName 方法中打一个断点。最后通过 debug 我们可以看出程序的运行顺序
知道了程序的运行顺序之后,我们还需知道一个知识点,那就是类的实例变量的初始化过程,也就是题目中成员变量 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 。
时间也很晚了(2016年12月27日01:35:54),最后给出一个知识点就睡了
构造器的初始化顺序大概是:父类静态块 ->子类静态块 ->父类初始化语句 ->父类构造函器 ->子类初始化语句 子类构造器。