类的初始化

类的使用顺序:

类的装载 -> 链接(验证 -> 准备 -> 解析) -> 初始化 -> 对象实例化

 https://github.com/Moriadry/useful-things/tree/master/src/Class/T.java

class T  implements Cloneable{
    //static,类T的静态成员变量,需要在类加载的过程中被执行初始化。
    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("静态块");
    }

    //构造方法是个实例化方法,在 T 被实例化时调用
    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");
    }
}

  

  • 在类链接之后,类初始化之前,实际上类已经可以被实例化了。

    就如此题代码中所述,在众多静态成员变量被初始化完成之前,已经有两个实例的初始化了。实际上,此时对类的实例化,除了无法正常使用类的静态承运变量以外(还没有保证完全被初始化),JVM 中已经加载了类的内存结构布局,只是没有执行初始化的过程。比如第 3 行public static T t1 = new T("t1");,在链接过程中,JVM 中已经存在了一个 t1,它的值为 null,还没有执行new T("t1")。又比如第 5 行的public static int i = print("i");,在没有执行初始化时,i 的值为 0.

  • 先执行成员变量自身初始化,后执行static {…}{…}代码块中的内容。

    如此策略的意义在于让代码块能处理成员变量相关的逻辑。如果不使用这种策略,而是相反先执行代码块,那么在执行代码块的过程中,成员变量并没有意义,代码块的执行也是多余。

  • 类实例化的过程中,先执行隐式的构造代码,再执行构造方法中的代码 这里隐式的构造代码包括了{}代码块中的代码,以及实例成员变量声明中的初始化代码,以及父类的对应的代码(还好本题中没有考察到父类这一继承关系,否则更复杂;))。为何不是先执行显示的构造方法中的代码,再执行隐式的代码呢?这也很容易解释:构造方法中可能就需要使用到实例成员变量,而这时候,我们是期待实例变量能正常使用的。

一、加载(Loading)

按如下三步执行

1.通过类的全名产生对应类的二进制数据流。(注意,如果没找到对应类文件,只有在类实际使用时才抛出错误。)
2.分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
3.创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。

二、链接(Linking)

链接的过程比加载过成复杂不少,这是实现 Java 的动态性的重要一步。分为三部分:验证,准备和解析。

1.验证(verification)

链接的第三部解析会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是 public ,private 的的问题),诸如检查 final class 又没有被继承,检查静态变量的正确性等等。(注意到实际上有一部分验证过程已经在加载的过程中执行了。)

2.准备(preparation)

对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:

所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。比如方发表(类似与 C++中的虚函数表,参见另一篇博文《Java:方法的虚分派和方法表》)。

3.解析(Resolution)

为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。

这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!

另外,这一步与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。详情见另一篇博文《Java 类加载的延迟初始化》。

三、初始化类(Initialization)

开发 Java 时,接触最多的是对象的初始化。实际上类也是有初始化的。相比对象初始化(参见博文 Java 类的实例化),类的初始化机制要简单不少。

类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。(参见博文《Java 类加载的延迟初始化》)

类的初始化分两步:

1.如果基类没有被初始化,初始化基类。
2.有类构造函数,则执行类构造函数。

类构造函数是由 Java 编译器完成的。它把类成员变量的初始化和 static 区间的代码提取出,放到一个的方法中。这个方法不能被一般的方法访问(注意,static final 成员变量不会在此执行初始化,它一般被编译器生成 constant 值)。同时,2中是不会显示的调用基类的,因为 1 中已经执行了基类的初始化。类的初始化还必须注意线程安全的问题。

https://github.com/Moriadry/useful-things/tree/master/src/LoadClass

总结

一个类的初始化准备工作如下:

1.加载:这个由类加载器完成,他去查找字节码,并且创建一个Class对象

2.连接:验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的引用

3.初始化:如果这个类有超类,则对其进行初始化,执行静态初始化器和静态初始化代码块

初始化被延迟到了对静态方法(构造器是隐式的静态方法)或

非静态方法的首次引用才执行


由上面可以知道,接口不能被初始化,如果想定义常量,必须是在接口的字节码被装载到虚拟机的时候他的常量就得被访问,所以他必须是static final的

为什么接口不能定义成员变量,而只能定义 final static 变量。

  • 1.接口是不可实例化,它的所有元素都不必是实例(对象)层面的。static 满足了这一点。
  • 2.如果接口的变量能被修改,那么一旦一个子类实现了这个接口,并修改了接口中的非 final 变量,而该子类的子类再次修改这个非 final 的变量后,造成的结果就是虽然实现了相同的接口,但接口中的变量值是不一样的。

综上述,static final 更适合于接口。

static final叫编译期常量,不需要初始化就能读取。

原文地址:https://www.cnblogs.com/mywy/p/5157070.html