天天看点

关于对象创建及初始化的面试向理解

    这是一篇“站在半山腰上”的文章:没有“山脚处”的基础介绍,也达不到“山顶上”一般说一不二的透彻理解,只能说是当前这个编程“历史阶段”下,在重读《java编程思想》初始化部分内容后,结合之前的理解,再加上几个面试向的例子,整理出来,一是达成前文的承诺,二是交流、学习、理解、提高,三是作为爬向“山顶”的一个脚印吧。

关于对象的创建和初始化,我想先把自己总结的关于初始化顺序的经验拎出来说,一是因为这是一个面试的点,多数朋友其实并不想长篇累牍的深入太多,二是因为经验性的总结是普遍适用的,但总有一些概括不到的“特例”,对于这种结论性的东西,我想,去其形,留其神,应当是最好的,就像张三丰教张无忌武功哈哈。

    这里讨论的初始化顺序,要涉及到静态内容的初始化,所以不完全是止于对象这一层的,会涉及到类加载和类的初始化。结论有两点:静态优先于非静态;属性(及代码块)优先于方法。 看到这儿可以先停一下,想一想。

    然后,援引书中关于对象创建的步骤,摘出来(【注1】这里不涉及反射、clone()或反序列化等方式创建对象):

假设有一个Dog类。

1.当首次创建Dog类的对象,或首次调用Dog类的静态方法/静态成员(书中称静态域)时,java解释器将查找类路径,以定位Dog.class文件。(【注2】另外书中提到:构造器虽然没有显式使用static,但实际上是静态方法。以我目前的水平,并不能很好地理解这句话,我的实践是:构造方法能够调用非静态成员,这一现象与静态方法的特征冲突。)

2. 载入 Dog.class文件,执行所有静态初始化操作。静态初始化只在Class对象首次加载时执行一次。

3. 在使用new Dog()来创建对象时,首先将在堆上为该对象分配足够的内存。(我将书中第4点合并到此处)这块存储空间将被清零,这自动的将Dog对象中的所有基本类型数据都设置为默认值(0),引用被置为null。(【注3】不知道我理解的对不对,但我倾向于把此处的初始化称为默认初始化或隐式初始化)

4. 执行所有出现于字段定义处的初始化操作。(【注4】类似前述,我倾向于将此处的初始化操作称为显式初始化或书中提到的指定初始化,这个问题不大。最主要的问题是“所有”,必须强调,这个“所有”出现的并不严谨:应该是:执行除静态成员外,其他字段的显式初始化操作!这里解释一下:按照书中的理想情况,步骤2已经完整的将全部静态初始化操作执行完毕,但是实际上(或者说特别是在面试题中…),处在类开始处的静态对象可能先要执行new Object()的操作来完成初始化,此时,就像插曲一样,你要先执行3-5,然后返回头来,继续执行步骤2。当然,可能这样说不直观,没关系,看了后例,再返回头来看就明白了。)

5. 执行构造器。(【注5】如书中叙,如果涉及继承,会比较麻烦,同见后例)

    至此,堆中的对象就创建并初始化完成了。我没有补充太多类加载的内容(比如说通过符号引用判断类是否被加载过等等),一个是JVM虚拟机和内存机制我懂的不多,二是不想再引入其他复杂度来干扰这部分内容的理解了,以后系统学习JVM时候,会继续整理相关内容。

同时,看到这里,之前的两条结论,应该已经忘得差不多了。下面通过几个实例,具体说明。

例:
public class StaticTest {
        public static void main(String[] args) {
            System.out.println("main方法");
            st.function();
        }
        static StaticTest st = new StaticTest();

        static {
            System.out.println("静态代码块");
        }

        {
            System.out.println("构造代码块");
        }
        public StaticTest() {
            System.out.println("构造方法");
            System.out.println("构造方法中 a="+a+", b="+b);
        }
        public void function() {
            System.out.println("main方法中 a="+a+", b="+b);
            System.out.println("main方法执行完毕");
        }
        int a =;
        static int b=;
}
           

希望你先手写出答案,再继续向下看。

// Output:
构造代码块
构造方法
构造方法中 a=, b=
静态代码块
main方法
main方法中 a=, b=
main方法执行完毕
           

    结果是否和你的答案有出入? 如是,推荐你打断点debug,那样会很清楚。下面我描述一下程序的执行流程:

1、要执行main方法,需要定位并加载 StaticTest.class

2、类加载时,开始顺序执行静态初始化(包括静态成员、静态代码块)

3、首先执行

static StaticTest st = new StaticTest();
           

    ( 这里就是前文【注4】提到的情况:虽然类中还有其他静态成员需要初始化,但是想要完成 st 的初始化,必须要先去 new StaticTest(),执行一些看似与静态初始化无关的“支线任务”。这就是我希望你把结论中提到的 “静态优先于非静态”去其形,留其神的原因。)

3-1、执行 new StaticTest(),在堆中为这个对象分配空间,并将此空间清0,执行默认初始化(特别注意:默认初始化之后, a=0;b=0 ,很重要!)

3-2、顺序执行非静态成员(含构造块)的显式初始化。本例中,先执行

{
    System.out.println("构造代码块");
}
           

    再执行

    此时 a 被赋值为 1 . 再次强调: 静态成员 b 此时只执行了默认初始化,赋予了初值 0 ,在 new 的过程中,不会执行显式初始化而被赋值为1!

    至此,执行构造方法前的“前置任务”完成了。

3-3、执行构造方法

    至此,new 操作完成, 静态成员 st 完成初始化。

4、继续进行静态成员的初始化,执行

static {
    System.out.println("静态代码块");
}
           

5、完成最后一个静态成员的初始化:

static int b=;
           

    此时,b 被赋值为 2 .

    至此,StaticTest类中全部静态成员已完成初始化,执行main方法的“前置任务”已经完成。(希望你认识到:main方法是静态方法,这也是结论中“属性及代码块优先于方法”的一处佐证)

6、执行 main 方法。

    以上就是例1 。

    例1 是没有继承关系的例子,如果加上继承关系,那么情况会稍稍复杂。结论是:1. 先加载父类(先执行父类静态初始化),再加载子类(后执行子类静态初始化) 2. 先实例化父类,再实例化子类。同样,实例说明:

例2:

public class StaticTest extends Father{
        public static void main(String[] args) {
            System.out.println("main方法");
            st.function();
        }

        //便于理解子类静态初始化和父类对象实例化时机
        static {
            System.out.println("子类加载,子类静态初始化开始");
        }

        static StaticTest st = new StaticTest();

        static {
            System.out.println("静态代码块");
        }

        {
            System.out.println("构造代码块");
        }
        public StaticTest() {
            System.out.println("构造方法");
            System.out.println("构造方法中 a="+a+", b="+b);
        }
        public void function() {
            System.out.println("main方法中 a="+a+", b="+b);
            System.out.println("main方法执行完毕");
        }
        int a =;
        static int b=;
}
class Father {

    public Father() {
        System.out.println("执行了父类构造");
    }

    {
        System.out.println("父类构造块");
    }

    static {
        System.out.println("父类加载,父类静态初始化开始");
    }
}
           

    可以看到,我建立了新的继承体系,并在子类初始化静态对象 st 前,添加了一个静态块,目的是便于理解子类静态初始化和父类对象实例化时机。

    输出如下:

//Output:
父类加载,父类静态初始化开始
子类加载,子类静态初始化开始
父类构造块
执行了父类构造
构造代码块
构造方法
构造方法中 a=, b=
静态代码块
main方法
main方法中 a=, b=
main方法执行完毕
           

    这个例子就没有什么费解的地方了,记住结论就可以了,不再详述。

    如果想检验自己理解的如何,请看这篇文章,

http://www.cnblogs.com/maowh/p/3729971.html

看起来复杂,但只要你真正做到了“留其神”,那就都是纸老虎!

继续阅读