java的加载过程之初始化和实例化

想了好久这个题目到底该叫什么,关于加载机制,已经讲的太多,今天说一下加载过程里的初始化。还有初始化之外的实例化。先来一道面试题吧!

public class StaticTest {
    public static int k = 0;
    public static StaticTest t1 = new StaticTest("t1");
    public static StaticTest t2 = new StaticTest("t2");
    public static int i = print("i");
    public static int n = 99;
    
    public int j = print("j");
     
    {
        print("构造快");
    }
    
    static{
        print("静态块");
    }
    
    public StaticTest(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        ++i;
    }
    
    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        return ++n;
    }
    
    public static void main(String[] args) {
        StaticTest t = new StaticTest("init");
    }
 
}

这个结果其实还是挺刺激的,至少我第一次做的时候错了,而且错的还挺离谱……下面我们就好好捋一下,如果你这道题做错了的话,当然后面还会有其他的小例子。

类的加载过程里有两个重要的步骤:连接阶段-准备、初始化。
1、准备阶段:为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内);括号里的内容很重要,因为实际上后面要讲的初始化和实例化的区别也在于此,有关于静态和非静态的!

public static int k = 5; //此时k=0,完成的是赋初值

2、初始化:初始化阶段是执行类构造器()方法的过程,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{} 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。换一句简单的话来说,初始化涉及到的就是类变量和静态语句块static,下面代码如是:

public static int k = 5;  //准备阶段k=0,此时k被第一次修改,k=5
static{
       k=10; //k被第二次修改,顺序就是语句在源文件中出现的顺序,k=10
    }

要画重点了,其实现在很简单,初始化,就是执行类静态变量赋值和静态代码块的过程(这句话严格说有误,后续我会补全) ,那这和实例化又有什么关系呢?首先,要明确的是何时会初始化?答案之一就是实例化的时候,通常也就是new关键字出现的时候。所以实例化的时候会分两步走,第一步:初始化,第二步:实例化(这两步其实也有误,下面单独解释)。下面我们来说一下实例化的过程。

实例化的过程类似于初始化,只不过初始化只能执行一次!这很重要,实例化是和对象相关的过程,所以涉及到的也就是类非静态变量和非静态代码块,以及构造方法,这个涉及到的操作,也称为执行实例构造器()方法的过程。

	public int j = 10;
     
    {
        j=15;
    }
    //此处还有构造方法

需要说明的点如下:
1、我在上面说有一点需要补全,那就是初始化执行类变量和静态代码块之前,会先加载静态方法,要不然是不能执行静态方法的,但是加载不代表执行!对于实例化过程也是这样!
2、其实初始化和实例化是两个过程,初始化只执行一次,而实例化可以多次执行,对于一开始提到的小例子而言,如下:

public static StaticTest t1 = new StaticTest("t1"); 

t1的修饰符是static,是静态变量,所以初始化的时候会执行该行代码,但是同时执行的过程发现又调用了new关键字,所以还要再实例化,之前说到实例化要分两步,先初始化再实例化,但是初始化只有一次,在main方法里new的时候就执行过了,所以此处只执行实例化!

这里对第一个例子进行说明,比较长,但是说的很清楚,比我的语言要简练很多:

/**
 * 加载方法不等于执行方法,初始化变量则会赋值
 *             类加载顺序应为 加载静态方法-静态变量赋值-执行静态代码块
 *             实例化时 先加载非静态方法-非静态变量赋值-执行构造代码块-执行构造函数
 *
 */
public class StaticTest {
    /**第一个加载*/
    public static int k = 0;
    /**第二个加载,因为是new一个实例,
     * 非静态变量赋值 打印出  1:j i=0 n=0
     * 执行构造块     打印出  2:构造快 i=1 n=1
     * 执行构造方法 打印出  3:t1 i=2 n=2
     * 实例化完成
     */
    public static StaticTest t1 = new StaticTest("t1");
    /**第三个加载 过程同上
     * 非静态变量赋值 打印出  4:j i=3 n=3
     * 执行构造块     打印出  5:构造快 i=4 n=4
     * 执行构造方法 打印出  6:t2 i=5 n=5
     */
    public static StaticTest t2 = new StaticTest("t2");
    /**第四个加载
     * 打印出  7:i i=6 n=6
     */
    public static int i = print("i");
    /**
     * 第五个加载
     */
    public static int n = 99;
    /**
     * 此变量在类加载的时候并不初始化,在实例化类的时候初始化
     */
    public int j = print("j");
     
    {
        print("构造快");
    }
    /**
     * 第六个加载 此时,n已经被初始化  所以打印出
     * 8:静态块 i=7 n=99
     */
    static{
        print("静态块");
    }
    //-----------以上属于类加载---------------------
    /**
     * 实例化过程:
     *         首先加载非静态方法集;
     *         初始化非静态变量:9:j i=8 n=100
     *         执行构造块:10:构造快 i=9 n=101
     *         执行构造方法:11:init i=10 n=102
     * 实例化完成
     */
    
    /**
     * 执行构造函数  实例化完成
     * @param str
     */
    public StaticTest(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        ++i;
    }
    /**
     * 这个应该是最先加载 但是,加载不等于执行
     * 因为如果不加载此函数,静态变量是无法初始化的
     * @param str
     * @return
     */
    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        return ++n;
    }
    
    public static void main(String[] args) {
        /**首先加载类,然后实例化:
         * 类加载过程:
         *         首先加载所有的静态方法,但不执行;
         *         然后按照静态变量的顺序开始初始化
         *         静态变量初始化完毕后执行静态构造块(不执行构造块)
         *         此时类加载完毕
         * 实例化过程:
         *         加载非静态方法
         *         初始化非静态变量
         *         执行构造代码块
         *         执行构造函数
         *         此时实例化完毕
         */
        StaticTest t = new StaticTest("init");
    }
 
}

到这里我们这篇博客其实才写了一部分,蛋疼呀,谁会读这么长的博客呢……
接下来我们说一下两个类拥有继承关系的情况,当然了,还是实例化的时候,因为实例化总会初始化……
拥有继承关系的两个类,初始化关系如下:
父类的静态变量和静态块)->(子类的静态块和静态变量)->(父类的实例变量和父类的普通块)->父类的构造器->(子类的实例变量和普通块)->子类的构造器

发现什么了没有?即便是拥有继承关系,依旧遵循的是先初始化再实例化!小例子走一波

public class ClassLoaderTest {

    public static void main(String[] args) {
        son sons=new son();
    }
}

class parent{
    private static  int a=1;
    private static  int b;
    private   int c=initc();
    static {
        b=1;
        System.out.println("1.父类静态代码块:赋值b成功");
        System.out.println("1.父类静态代码块:a的值"+a);
    }
    int initc(){
        System.out.println("3.父类成员变量赋值:---> c的值"+c);
        this.c=12;
        System.out.println("3.父类成员变量赋值:---> c的值"+c);
        return c;
    }
    public parent(){
        System.out.println("4.父类构造方式开始执行---> a:"+a+",b:"+b);
        System.out.println("4.父类构造方式开始执行---> c:"+c);
    }
}

class son extends parent{
    private static  int sa=1;
    private static  int sb;
    private   int sc=initc2();
    static {
        sb=1;
        System.out.println("2.子类静态代码块:赋值sb成功");
        System.out.println("2.子类静态代码块:sa的值"+sa);
    }
    int initc2(){
        System.out.println("5.子类成员变量赋值--->:sc的值"+sc);
        this.sc=12;
        return sc;
    }
    public son(){
        System.out.println("6.子类构造方式开始执行---> sa:"+sa+",sb:"+sb);
        System.out.println("6.子类构造方式开始执行---> sc:"+sc);
    }
}

执行结果如下:

 1.父类静态代码块:赋值b成功
 1.父类静态代码块:a的值1
 2.子类静态代码块:赋值sb成功
 2.子类静态代码块:sa的值1
 3.父类成员变量赋值:---> c的值0
 3.父类成员变量赋值:---> c的值12
 4.父类构造方式开始执行---> a:1,b:1
 4.父类构造方式开始执行---> c:12
 5.子类成员变量赋值--->:sc的值0
 6.子类构造方式开始执行---> sa:1,sb:1
 6.子类构造方式开始执行---> sc:12

现在能到一半了吗?我自己写着都绝望了,貌似还有不少东西

现在插播一段广告呀,广告呀,什么情况能触发初始化……
Java虚拟机规范中并没有进行强制约束什么情况下需要开始类加载过程。但是对于初始化阶段,虚拟机规范则是严格规定了有且仅有5种情况必须立即对类进行“初始化”(而加载,验证,准备自然需要在此之前开始):

1.遇到new,getstatic,putstatic,或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令单最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。注意,后面三种都是只初始化!

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先出法其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先出法其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5.当使用JDK1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于这五种会触发类进行初始化的场景,虚拟机规范中是用来一个很强烈的现定于:“有且仅有”,这5种场景中的行为称为一个类进行主动引用。

*** 我们之前说了很久的东西,new关键字-,实例化会触发初始化,所以这本身就是两个不同的过程。
看下面的例子:

public class SuperClass {
    static{
        System.out.println("SuperClass init!");
    }
    public static int value=123;

}
public class SubClass extends SuperClass {
    static{
        System.out.println("SubClass init!");
    }

}   
public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(SubClass.value);

    }

}
SuperClass init!
123

说明:我们提到调用类静态变量的时候会初始化,这没有问题,问题在于,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定。这点取决于虚拟机的具体实现。对于Sun HotSpot来说,可通过-XX:+TraceClassLoading参数观察到此操作会导致子类的加载。

再看下面一个例子,保留上面例子使用的父类和子类,更新一下主方法:

public class NotInitialization {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        SuperClass[]sca = new SuperClass[10];

    }
}

结果是没有任何输出,因为通过数组定义引用类,不会触发此类的初始化!

继续看看下面一个例子

public class ConstClass {
    static{
        System.out.println("ConstClass init!");
    }
    public static final String HelloWorld = "hello world";

	public static void main(String[] args) {
        System.out.println(ConstClass.HelloWorld);
    }
}

结果依旧是没有任何输出,HelloWorld的修饰符是final,已经在编译期把结果放入常量池,所以不会触发初始化!

接下来是我们最后的部分,在上面的各种描述里,我都会提及类,上面是关于类的初始化,那么接口的初始化呢?接口的加载过程与类加载过程稍微有一些不同,针对接口需要做一下特殊说明:接口也有初始化过程,这点与类类是一致的。上面的代码都是用静态语句块“static{}”来输出初始化信息的,而接口中不能使用“static{}”语句块,但是编译器仍然会为接口生成“()”类构造器,用于初始化接口中多定义的成员变量。接口与类真正有所区别的是前面讲述的5种“有且仅有”需要开始初始化场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类接口全部完成初始化,只有在真正使用到父类接口的时候(如引用接口中定义的常量)才会初始化。

参考如下:我就是剪辑手……
http://blog.51cto.com/2839840/1975517
https://blog.csdn.net/peerslee/article/details/79162651
https://www.cnblogs.com/LoganChen/p/6844585.html
https://www.cnblogs.com/tengpan-cn/p/5869099.html
https://www.cnblogs.com/aspirant/p/7200523.html

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值