从几道面试题看对象的初始化

958 阅读5分钟

这是无意间在网上看到的一道考面向对象的一道题,乍眼一看发现做不出来。好东西就要来分享一下,请看题。

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();
        System.out.println(b);
    }
}

求程序最后的输出。

估摸着这道题考了多态,类的初始化以及成员加载的顺序。

先入为主,以上的代码中有一个main方法,作为一个类的入口,执行main方法触发了类加载的过程。

类初始化

对于什么时候开始类的初始化,以下摘自深入理解java虚拟机第七章? 在类未被初始化过的前提下。

  1. 遇到new getstatic putstatic invokestatic这四个指令时。
  2. 使用java.lang.reflect对类进行反射调用。
  3. 初始化时发现父类未进行初始化,则先触发父类的初始化。
  4. 虚拟机启动时,用户需要制定一个执行主类(main方法的那个类)。

对于一个类的初始化阶段就是执行类构造器方法的过程。方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。

Java虚拟机会对做以下规定。

  1. 虚拟机会保证子类的方法执行之前父类的已经执行。
  2. 接口中不能使用静态语句块,接口初始化的时候不用关心父类接口的初始化。只有父类接口定义的变量使用时才初始化。
  3. 多个线程初始化一个类,虚拟机只允许一个线程执行方法,其他线程都阻塞等待直到方法执行完毕。

对象初始化

初始化后,接着就开始执行类中的方法。在执行new Sub(),它会触发对象的初始化。在java中有多种创建对象的方式,new是最直观最常用的那种。其他还有

  1. 反射机制(Class.newInstance()、 Constructor.newInstance())
  2. Clone方法(obj.clone())
  3. 反序列化

在对象被创建时,虚拟机会为其分配空间来存放对象本身的实例变量还有从父类继承过来的变量,在这个过程中还会为各个变量设置初始默认值。 接着就会进行对象的初始化。一般分为3个步骤,跟类初始化其实差不太多。

  1. 实例变量初始化
  2. 实例代码块初始化
  3. 构造函数初始化

对于1、2并没有严格意义上的执行顺序,谁在前面谁先跑。但是3一定是在1、2之后。为什么?构造方法的目的就是为了在创建对象时给成员变量赋初始值,如果成员都没定义,那构造函数就没有意义了。但是对于1、2点来说,谁在上面谁就先执行。

在new Sub()触发对象的初始化后,会先调用Sub类的无参构造方法,因为Sub类中没有显式声明一个无参的构造方法,那就调用它默认的无参构造。子类的构造方法中都有一个super()去调用父类的无参构造。

在Java中有以下这么几个规则。

  1. 每个Java类中都会有一个构造函数,如果没有显示的定义构造函数,那么就会隐式的给他一个无参构造方法。
  2. 在类实例化之前必须先实例化它的父类以保证所创建实例的完整性。
  3. Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用。其实也就相当于是在构造函数中使用this调用当前类其他的构造函数,或者是使用super调用父类的构造函数。

至于以上3条规则的原因我还没找到官方的解释,找到再补上。

看到这里,或许可以瞄一眼另一道题

class Person {
    String name = "No name";
    public Person(String nm) {
        name = nm;
    }
}
class Employee extends Person {
    String empID = "0000";
    public Employee(String id) {
        empID = id;
    }
}
public class Test {
    public static void main(String args[]) {
        Employee e = new Employee("123");
        System.out.println(e.empID);
    }
}

这题考的是继承关系中,父类子类构造器初始化的问题。这边再拷一下上面的一句话 -> 子类的构造方法中都有一个super()去调用父类的无参构造。再瞄一眼代码,WTF 爸爸的无参构造去哪了?如果父类没有无参的构造函数的情况,子类需要在自己的构造函数中显式调用父类的构造函数。

根据上面的规则,代码就变成了

public Sub() {
    // super(); 隐式的调用父类的无参构造
    callName(); // -> System.out.println(baseName);
    private String baseName = "sub";
    
}

所以结果一目了然了吧,在执行callName()的时候baseName还未初始化,执行到这一步的时候要搞清楚成员变量的初始化顺序。

根据以上一波文档的寻找发现构造器初始化顺序大概就是 父类静态 -> 子类静态 -> 父类成员 -> 父类构造 -> 子类成员 -> 子类构造

最后给一个网上找到的一检测题来练一波手。wota:Java初始化顺序

以后碰上这类面试题往上套就是了。通过理解这个面试题,目的是更深入的了解了类初始化和对象初始化的过程。

学习的最终目的并不是为了面试,面试只是一个激励学习的动机。把握面试题,享受学习新知识的乐趣。

参考:

《深入理解Java虚拟机》