Java初始化基础知识

前言

Java中的初始化包括默认初始化,静态初始化块,普通初始化块和构造方法中的初始化,这些初始化代码的执行最终的结果究竟是怎么样的呢,对很多Java开发者来说还是比较困惑的,这里通过学习Java类的加载基础知识来理清它们的调用过程。

类的加载和链接

阶段 注释
加载 查找并加载类的二进制数据
链接 验证:确保被加载的类的正确性;准备:为类的静态变量分配内存,并将其初始化为默认值;解析:把类中的符号引用转换成直接引用
初始化 为类的静态变量赋予正确的初始值
public static int x; // 默认值是0
public static int num = 10; // 默认的值是0, 初始化值是10

类的加载指的时将类的class文件中的二进制文件读入到内存中,将其放在运行时数据区的方法区内,然后再堆区内创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

Java虚拟机自带的加载器

加载器名称 注释
根类加载器 Bootstrap,C++实现
扩展加载器 系统属性java.ext.dirs指定的目录由ExtClassLoader加载器加载,如果没有指定该系统属性加载器默认加载$JAVA_HOME/lib/ext目录下class文件
系统类加载器 加载处于-classpath目录里的类

用户自定义的类加载器都是java.lang.ClassLoader类的子类,任何类都是通过类加载器加载的,它的Class对象里面都会包含加载它的ClassLoader实例。如果类是由根类加载器加载的调用它的getClassLoader就会返回空。

System.out.println(String.class.getClassLoader()); // null 根类加载器
System.out.println(JFrame.class.getClassLoader()); // null 根类加载器
System.out.println(ClassLoaderTest.class.getClassLoader()); // 系统类加载器

JVM允许类加载器在预料到某个类将要使用的时候预先加载它,如果在预先加载时遇到class文件缺失或者存在错误,需要到程序首次主动使用它的时候才报错误,如果程序始终没有主动使用这个类,类加载器就不会报告错误。

连接阶段,连接就是将已经读入内存的类二进制数据合并到虚拟机的运行时环境中。
类的验证:主要包含类文件的结构检查;语义检查,确保代码符合Java语法;字节码验证;二进制兼容性的验证
类的准备:在准备阶段Java虚拟机为静态变量分配内存,并且设置默认的初始值
类的解析:JVM会把类的二进制数据中的符号引用替换为直接引用,代码中变量名、方法名类名等都是符号引用,直接引用就是变量方法类在方法区内存中的位置,也就是它们的指针。

类的初始化

假如类还没有被加载和链接,先加载和链接;如果类存在直接父类并且父类没有初始化,先初始化直接父类;如果类中又初始化语句,先执行类的初始化语句,为类的静态变量赋予初始值。JVM初始化一个类的时候会要求它的父类都已经初始化,但是这条规则并不适用于接口。初始化一个类并不会初始化它实现的接口,初始化一个接口并不会初始化它的父接口,只有当访问的变量或者方法确实定义在类中才算做对类的主动使用。

Java程序对类的使用方式分为主动使用和被动使用,所有的Java虚拟机实现必须在每个类或借口被Java程序“首次主动使用”的时候才初始化它们。主动使用分为6中情况:

  1. 创建类的实例
  2. 访问某个类或者接口的静态变量
  3. 调用类的静态方法
  4. 使用反射获取类
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类,例如执行java Hello,这个Hello.class就是启动类

除了上面的6中情况外的使用都属于被动使用,被动使用都不会执行类的初始化操作。总之类的初始化会导致类的静态代码块被执行,如果有多个静态代码块,这些代码块按照在类中定义位置先后执行。需要注意直接写在类变量后面的初始化表达式和写在static块里的初始化是等价的。

public class StaticTest {

    private static int a;
    private static boolean b;
    private static float f;

    public static void main(String[] args) {
        System.out.println(StaticTest.a); // 0
        System.out.println(StaticTest.b); // false
        System.out.println(StaticTest.f); // 0.0
    }
}

在上面的例子中a,b,f静态变量由于在连接的准备阶段被设置为默认值,后面初始化阶段没有设置新值,它们的值就是默认值。

public class StaticTest {

    private static int a = 10;

    static {
        f = 30.0f;
    }

    private static float f = 20.1f;
    // 等同于 
    // static {
    //     f = 20.1f;
    // }

    static {
        a = 20;
    }

    public static void main(String[] args) {
        System.out.println(StaticTest.a); // 20
        System.out.println(StaticTest.f); // 20.1f
    }
}

在上面的例子中由于f=20.1f这句赋值语句其实相当于执行 static { f = 20.1f };由于前面赋值为30.0f的静态初始化代码先执行,最终导致后面的20.1f后执行的值覆盖最开始的值。前面的这两个例子符合主动使用类的第6种情况,也就是类被标记为启动类,因而它的静态初始化块被执行了。

扫描二维码关注公众号,回复: 2250681 查看本文章
public class StaticTest {

    public static void main(String[] args) {
        Hello hello; // 什么都不会打印
        System.out.println(Hello.x); // 打印80,但不会打印Hello static块里的语句
    }
}

class Hello {
    static {
        System.out.println("Hello class inited");
    }

    public static final int x = 40 * 2; // 编译期常亮
}

上面的例子第一个声明一个hello变量并不属于主动使用,因而不会做类初始化操作,调用Hello.x是查看编译器常亮,不属于访问静态变量,因而两个都不属于主动调用,不会执行类内部的静态初始化块。

public class StaticTest {

    public static void main(String[] args) {
        new Hello(); // 会执行static块中代码
        System.out.println(Hello.x); // 会执行static块常亮
    }
}

class Hello {
    static {
        System.out.println("Hello class inited");
    }

    public static final int x = new Random().nextInt();
}

上面的例子由于创建了类的实例和访问了运行期的常亮,它们都属于主动使用类,这时就会执行类的初始化操作。

public class StaticTest {

    public static void main(String[] args) {
        System.out.println(MyHello.x); // 只会打印Hello 静态初始化块,不打印MyHello静态初始化块
    }
}

class Hello {
    static {
        System.out.println("Hello class inited");
    }

    public static final int x = new Random().nextInt();
}


class MyHello extends Hello {
    static {
        System.out.println("My Hello class inited");
    }
}

上面的例子虽然使用子类的常量但常量其实是定义在父类中,这样的化就不是主动使用子类,而是主动使用父类,所以子类没有执行初始化操作。

public class StaticTest {

    public static void main(String[] args) {
        System.out.println(MyHello.y); // 调用父类和子类的静态初始化块
    }
}

class Hello {
    static {
        System.out.println("Hello class inited");
    }

    public static final int x = new Random().nextInt();
}

class MyHello extends Hello {
    static {
        System.out.println("My Hello class inited");
    }

    public static final int y = new Random().nextInt();
}

上面的例子由于使用了子类的运行时常量主动使用了子类,这时也就主动使用了父类,父类先初始化,完成之后子类在做初始化操作。

public class Test {
    static {
        System.out.println("Test inited");
    }
}

    public static void main(String[] args) throws ClassNotFoundException {
//        try {
//            Class.forName("myjvm.Test"); // 会执行静态初始化块
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        }
        // 不会执行静态初始化块
        StaticTest.class.getClassLoader().loadClass("myjvm.Test");
    }

上面的事例证明使用反射创建类的时候属于主动使用会执行初始化操作,而类加载器在加载类时属于被动使用不会执行初始化操作。

普通初始化块

前面讨论的类加载里面提到的初始化都是类变量的初始化,也就是静态类变量的初始化。现在来看一下对象初始化的顺序,普通初始化块就是前面不带static的大括号。

public class StaticTest {

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(new Test().x); // 30
    }
}

public class Test {

    {
        x = 20;
        System.out.println("Test object inited");
    }

    public int x = 10;

    static {
        System.out.println("Test inited");
    }

    public Test() {
        x = 30;
    }
}

// Test inited
// Test object inited
// 30

由此可见对象初始化块在类初始化块执行完成之后才会执行,而构造方法则在对象初始化块之后才会执行。

总结

通过上面的测试可以得出如下结论:JVM在加载类执行链接的时候会为静态变量准备内存并赋予默认值,在程序主动使用类的时候会执行初始化操作,按照类定义里地静态初始化先后顺序执行静态变量的初始值赋值,在用户定义类的实例的时候会按照类中定义的对象初始化块执行,最后再执行构造函数内的代码。

猜你喜欢

转载自blog.csdn.net/xingzhong128/article/details/80462486