java 成员变量,静态变量,静态代码块和代码块,构造方法的初始化顺序

胶带一下背景

根据jvm的类加载机制,类的生命周期分为加载,连接,初始化,使用以及卸载,而我们平时所说的赋值这些,是发生在类初始化及使用阶段。

而在类初始化阶段,会对类内部static修饰的成员变量,即静态变量进行赋值,我强调,是赋值,不是分配内存空间,分配内存空间在前面的阶段已经做了。在程序中,静态变量的初始化有两种途径:一是在静态变量的声明处进行初始化;二是在静态代码块中进行初始化。

百度了许多,但是别人的终归是别人的,总是需要自己来一遍,毕竟我头铁还是愣头青,自己老老实实做一下总结吧。

老龟巨!

我们上一波代码!

走!

po一段代码

public class Father {
    public Assets mAssets = new Assets();
    public static LastName sLastName = new LastName();

    public Father() {
        System.out.println(Father.class.getSimpleName()+" invoke construct method");
    }
    {
        System.out.println(Father.class.getSimpleName()+" invoke code block");
    }

    static {
        System.out.println(Father.class.getSimpleName()+" invoke static code block");
    }
}
public class Assets {
    private final double mMoney;

    public Assets() {
        System.out.println(Assets.class.getSimpleName() + " invoke construct method");
        mMoney = 10000D;
    }

    public double getMoney() {
        return mMoney;
    }
}
public class LastName {
    public LastName() {
        System.out.println(LastName.class.getSimpleName() + " invoke construct method");
    }
}

我们创建了一个老爹类,老爹有资产,有姓氏,而且还是静态成员变量,与生俱来的姓氏,在声明的时候就已经初始化,很灵性。还有静态代码块跟代码块以及构造方法。但是资产这个静态成员没有在定义时候初始化,而姓氏相反;貌似也合乎道理,你生下来之前你的姓氏肯定是可以知道的啦,但是资产钱多钱少这个是可以后天来决定的嘛,颇有哲学味道。嗯好有点扯远。

我们运行方法看下,初始化优先级,我们究竟会先初始化哪个。

Father

LastName invoke construct method
Father invoke static code block
Assets invoke construct method
Father invoke code block
Father invoke construct method
SequenceTest invoke main method

以SequenceTest类为例,当我们执行SequenceTest 类的main方法,在上面打印出来的内容我们可以看到,会先初始化静态变量sFather。而sFather的初始化,才是我们要去关注的重点。

来波分析

这里我们要先回顾下类的生命周期,当类被加载器加载,编译生成.class文件,jvm虚拟机通过类全限定名来获取其定义的二进制字节流,获取方式包括但不限于从.class文件读取,字节流所代表的静态存储结构转化为方法区里的运行时数据结构,同时在java堆内生成新的Class对象作为访问方法区内这些数据的入口,完成类验证阶段后,进入类准备阶段。而我们现在要分析的,就是从进入准备阶段开始。

准备阶段

进入准备阶段,jvm虚拟机会正式为类变量,也就是静态变量分配内存并设置其初始化,而在这个时候,sLastName作为Father类内部持有的静态变量会在方法区中分配内存并设置其初始值,上述代码中该变量为一个对象引用,那么其默认值为null,也相当于执行了一次sLastName = null;那这里也作为成员变量Assets类的引用呢,这是个成员变量,这个要等进入类使用阶段才的时候才会有对应操作,这个我们后面会介绍到。

类初始化阶段

这里是类加载流程的最后一步,开始执行类中定义的Java程序代码.将静态变量(类变量)显示赋值,好这里开始将我们的sLastName按照代码里写的new LastName();将java堆内存的地址分配给它。所以这里就使用了LastName类的构造方法。紧接去执行静态代码块中的内容

类使用阶段

完成类初始化阶段后进入类使用阶段,先对成员变量进行默认初始化,也就是前面所说的,分配内存空间,并将成员变量引用,或者称为句柄存储到虚拟栈中,此时该引用不持有任何内存地址,那么这个时候,一样的操作,创建一个Assets类的对象并在堆内存中开辟空间存放该对象,同时将对象内存地址赋给mAssets变量,所以我们可以看到上面打印了Assets类构造方法的调用。

紧接,执行代码块

最后Father类执行本身的构造方法将真正的值赋予实例变量本身,这里的示例代码里指sFather实例,而我们没有在构造方法内再次对成员变量进行赋值,故mAssets变量未受到改变。至此,完成类初始化阶段。

同理,我们可以看到SequenceTest类里的静态变量sFather就会被先初始化,然后再执行入口main方法,所以“SequenceTest invoke main method”这条输出会在sFather变量的后面就不奇怪了。

按照我们的操作思路,哈哈我知道了,我马上写出:

类初始化过程中,顺序是:

静态变量-->静态代码块-->成员变量-->代码块-->构造方法

好,一键发表,注明原创,心里美滋滋,睡觉幻想大家评论的画面,如果真的是这样,那肯定要被人骂死.为什么,我们现在来看下。不是已经完事了么?开玩笑

作为一个脑洞比较大的人,我还会纠结别的情况,再确定是不是真的这样。上面说静态变量的初始化有两种途径:一是在静态变量的声明处进行初始化;二是在静态代码块中进行初始化。我们的示例代码里值验证了第一种,但是没有反过来证明,其次,示例代码里也没有对静态代码块中进行初始化的情况进行验证,这样直接得出结果,不严谨。

这里我们开始进行情况的补充

===================================分割线==================================

好,第一种,如果我类里面那个Assets类型的成员变量,我改成static修饰,但是我不实现它,会怎样?如果我换成在构造方法里实现,会怎样?

第二种,我上面的例子没试过静态变量在静态代码块里初始化,如果静态变量sFather声明却放在静态代码块里初始化,会影响什么?

最后一种,现在是单个类,如果我考虑继承,那么会是怎样的情况?
来,我们现在一个一个情况来尝试

当静态变量声明却不在声明处初始化,而是放在构造方法里

LastName invoke construct method
Father invoke static code block
LastName invoke construct method
Assets invoke construct method
Father invoke code block
Father invoke construct method
SequenceTest invoke main method

 按照我们上面的介绍一推就可以知道,就算是静态变量,在声明处不初始化只是让类初始化阶段不进行真正赋值,而写在构造方法,只是在对象初始化的时候进行赋值,所以进入Father类的构造方法后再进行Assets类型变量的初始化,换而言之,如果我不在构造方法里对Assets类型变量的初始化,那么压根就不会有这个静态成员的初始化,“Assets invoke construct method”这条输出就不会被打印。

静态变量放在静态代码块里初始化,会不会影响初始化的优先级

Father invoke static code block
LastName invoke construct method
Assets invoke construct method
Father invoke code block
Father invoke construct method
SequenceTest invoke main method

那很明显,如果静态变量放在静态代码块里,肯定走到了静态代码块里面再进行静态变量的初始化呀,这个情况考虑得没有意义

 继承情况下的优先级顺序

在这种情况下,我们引入一个Son类,同时main方法内声明一个静态的Son变量并初始化

public class Son extends Father{
    public static Disposition sDisposition = new Disposition();
    public Game mGame = new Game();
    public Son() {
        System.out.println(Son.class.getSimpleName()+" invoke construct method");
    }

    {
        System.out.println(Son.class.getSimpleName()+" invoke code block");
    }

    static {
        System.out.println(Son.class.getSimpleName()+" invoke static code block");
    }
}
public class Disposition {
    public Disposition() {
        System.out.println(Disposition.class.getSimpleName()+" invoke construct method");
    }
}
public class Game {
    public Game() {
        System.out.println(Game.class.getSimpleName()+" invoke construct method");
    }
}
public class SequenceTest {
    public static void main(String[] args) {
        System.out.println(SequenceTest.class.getSimpleName() + " invoke main method");
    }
    //    static Father sFather = new Father();
    static Son son = new Son();
}

运行结果如下

LastName invoke construct method
Father invoke static code block
Disposition invoke construct method
Son invoke static code block

Assets invoke construct method
Father invoke code block
Father invoke construct method
Game invoke construct method
Son invoke code block
Son invoke construct method

SequenceTest invoke main method

到这里,我们可以有一个结论了,那就是先父类再子类的过程。

再次分析

首先明确,Son类继承自Father类,继承其非私有成员。

在类初始化阶段,Son类初始化,会去为之前已经分配好的从父类继承的非私有成员进行对应初始化,此时会执行父类静态成员变量初始化,再执行父类静态代码块。然后再是子类的静态成员变量和静态代码块

进入类使用阶段,开始进行Son对象初始化,这里对应执行父类成员变量初始化,代码块和构造方法。同时对应子类成员变量,代码块和构造方法

最后才是main方法的执行

到这里,关于继承情况下的分析就到此。而我们前面所说的初始化顺序,应该是针对声明处初始化而言,否则,静态变量和静态代码块之间的优先级顺序是可以改变的,成员变量和构造方法的优先级顺序同理。

最后

谢谢大家的耐心,有错误的地方尽管提出来,毕竟这种基础的东西,有时候才是最折磨人的

猜你喜欢

转载自blog.csdn.net/arthurs_l/article/details/81263098