• 从阿里巴巴笔试题看Java加载顺序


    一、阿里巴巴笔试题:

    public class T implements Cloneable {
        public static int k = 0;
        public static T t1 = new T("t1");
        public static T t2 = new T("t2");
        public static int i = print("i");
        public static int n = 99;
    
        public int j = print("j");
    
        {
            print("构造快");
        }
    
        static {
            print("静态块");
        }
    
        public T(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);
            ++n;
            return ++i;
        }
    
        public static void main(String[] args) {
            T t = new T("init");
        }
    
    }

    当.java源代码转换成一个.class文件后,其转换成类似下面的等价代码:

    public class T implements Cloneable {
        public static int k;
        public static T t1;
        public static T t2;
        public static int i;
        public static int n;
    
        static {
            k = 0;
            t1 = new T("t1");
            t2 = new T("t2");
            i = print("i");
            n = 99;
            print("静态块");
        }
    
        public int j;
    
        public T(String str) {
            j = print("j");
            print("构造快");  // 最终的构造方法是这个样子
    
            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);
            ++n;
            return ++i;
        }
    
        public static void main(String[] args) {
            T t = new T("init");
        }
    
    }

    二、运行结果

    1:j   i=0   n=0
    2:构造快   i=1   n=1
    3:t1    i=2  n=2
    4:j   i=3   n=3
    5:构造快   i=4   n=4
    6:t2    i=5  n=5
    7:i   i=6   n=6
    8:静态块   i=7   n=99
    9:j   i=8   n=100
    10:构造快   i=9   n=101
    11:init    i=10  n=102

    三、加载过程分析

    一、执行main()时,由于使用new语句创建实例,属于首次主动使用类T,JVM加载类T,

    声明静态变量k、t1、t2、i、n(为静态变量分配内存),并设置变量初始化的值(0,null,null,0,0)  --- 类生命周期的准备阶段

    在static代码块中对k进行第二次赋值,k=0;

    在static代码块中对t1进行第二次赋值,触发new T(“t1”)的实例化过程                                           --- 静态变量的初始化可以在声明处进行,也可以在静态代码块进行

    1. 声明实例变量j,并设置变量初始化的值0,然后进入构造方法public T (String str)构造方法执行完毕,将heap区中创建的T的实例对象,并赋值给t1
      1. 执行静态方法print("j"),并将返回值赋值给j,print()方法输出“1:j   i=0   n=0”,返回值为1;并完成j=1的赋值
      2. 执行静态方法print("构造块"),print()方法输出“2:构造快   i=1   n=1”,返回值为2
      3. 执行构造方法的剩下三行,即
         System.out.println((++k) + ":" + str + "    i=" + i + "  n=" + n);
                ++n;
                ++i;
        # 此时str的值是构造方法的传参“t1”,最终输出“
        3:t1 i=2 n=2”
    2. 在static代码块中对t2进行第二次赋值,触发new T(“t2”)的实例化过程,由于k,i,n是静态变量,自增操作的值都被保留了下来。输出: 
    4:j   i=3   n=3
    5:构造快   i=4   n=4
    6:t2    i=5  n=5

    执行语句 i = print(“i”),输出

    7:i   i=6   n=6
    
    # print(“i”)返回值为6,赋值给i

    执行n=99;print(“静态块”),输出

    8:静态块   i=7   n=99
    
    #因为n=99的赋值语句,输出成为了n=99

     

    至此,类初始化完毕。

    二、完成类T的加载后,然后进行new T("init")的创建类T的实例,

    过程和前几次new T("t1")、new T("t2")基本相同,输出

    9:j   i=8   n=100
    10:构造快   i=9   n=101
    11:init    i=10  n=102

    实例化操作3次 * 3 + print(“i”) + print(“静态块”),共计输出11行内容

    四、涉及知识点

    主动使用:

    1):最为常用的new一个类的实例对象,或者通过反射/克隆/反序列化手段来创建实例。
    2):直接调用类的静态方法(也包括main方法)。
    3):操作(访问或改变)该类或接口中声明的非编译期常量静态字段(对于final类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看做编译时常量,不视作主动使用
    4):调用Java API的某些反射方法,比如调用Class.forName("ClassName")
    5):初始化一个类的子类的时候,父类也相当于被程序主动调用了(如果调用子类的静态变量是从父类继承过来并没有覆写的,那么也就相当于只用到了父类的静态变量,和子类无关,所以这个时候子类不需要进行类初始化)。
    6):JVM虚拟机启动时被标为启动类的类.

    所有的JVM实现,在首次主动使用某类的时候才会加载该类。

    被动使用:

    1. 父接口并不会因为它的子接口或者实现类的初始化而初始化. 只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化.
    2. 通过数组定义来引用类,不会触发类的初始化,如SubClass[] sca = new SubClass[10];
    3. Java程序中对类的编译时常量的使用,被看做是对类的被动使用,不会导致类的初始化.(对于final类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看做编译时常量.)
    4. 如果调用子类的静态变量是从父类继承过来并没有覆写的,那么也就相当于只用到了父类的静态变量,和子类无关,所以这个时候子类不需要进行类初始化)。

    自增运算:

    int i = 0; System.out.println(i++); System.out.println(i)    # 输出0和1

    int i = 0; System.out.println(i++); System.out.println(i) # 输出1和1

    其他:

    对于类的成员变量,不管程序有没有显式地进行初始化,JVM都会先自动给它初始化为默认值。
    局部变量声明以后,JVM不会自动地为它初始化为默认值,必须先经过显式的初始化才能使用它。如果编译器确认一个局部变量在使用之前可能没有被初始化,编译器将报错。
    数组和String字符串都不是基本数据类型,它们被当作类来处理,是引用数据类型。引用数据类型的默认初始值都是null

    参考:

    Java类、实例的初始化顺序

    Java Tutor - Visualize Java code execution to learn Java online (also visualize PythonJavaJavaScriptTypeScriptRubyC, and C++ code)

  • 相关阅读:
    Sequelize框架:
    sequelize 测试
    sequelize 用于PostgreSQL,MySQL,SQLite和MSSQL的Node.js / io.js ORM
    node Util 模块
    bluebird的安装配置
    bluebird 开发文档链接
    Node.js的__dirname,__filename,process.cwd(),./的含义
    editplus
    luogu3377 【模板】左偏树(可并堆)
    cf936c Lock Puzzle
  • 原文地址:https://www.cnblogs.com/echo1937/p/6242578.html
Copyright © 2020-2023  润新知