Java代码块及代码加载顺序

Java代码块加载顺序

1. 局部代码块

  • 形式:用用 “ { } ” 包围的代码

  • 位置:局部位置(方法内部)

  • 作用:限定变量的生命周期,代码块运行完毕立即释放块内变量,节约内存

  • 调用:调用其所在的方法时执行,并且是按照其在方法中的具体位置来执行

实例代码如下:

class A {
    
    
    A(){
    
    }
    public void showInfo() {
    
    
        System.out.println("hello");
       
        //局部代码块
        {
    
    
            String msg = "A局部代码块运行";
            System.out.println(msg);
        }

        //局部代码块的变量在局部代码块结束即释放,再次引用会报错
//        System.out.println(msg);
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
       A a = new A();
       a.showInfo();
    }
}

测试结果为:

hello
A局部代码块运行

注意:方法中的局部代码块,一般进行一次性地调用,调用完立刻释放空间,避免在接下来的调用过程中占用栈空间,因为栈空间内存是有限的,方法调用可能会会生成很多局部变量导致栈内存不足,使用局部代码块可以避免这样的情况发生。

2. 构造代码块

  • 形式:用 “ { } ” 包围的代码

  • 位置:类成员的位置,即类中方法之外的位置

  • 作用:把多个构造方法共同的部分提取出来,共用构造代码块

  • 调用:在每次new一个对象时自动调用,优先于构造方法执行,实现对对象的初始化

实例代码如下:

class A {
    
    
    int value; //成员变量的初始化交给代码块来完成
    
    A(){
    
    
        System.out.println("A构造方法运行");
    }
    
    //构造代码块
    {
    
    
        System.out.println("A构造代码块运行");
        value = 123; //初始化value值
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
       A a = new A();
       System.out.println("value = "+a.value);
    }
}

测试结果:

A构造代码块运行
A构造方法运行
value = 123

3. 静态代码块

  • 形式:用static关键字修饰并用 “ { } ” 包围的代码

  • 位置:类成员位置,即类中方法之外的位置

  • 作用:对类进行一些初始化 只加载一次,当new多个对象时,只有第一次会调用静态代码块,因为静态代码块是属于类的,所有对象共享一份

  • 调用:加载类并初始化类时调用。

实例代码如下:

class A {
    
    
    //构造方法
    A(){
    
    
        System.out.println("A构造方法运行");
    }
    //构造代码块
    {
    
    
        System.out.println("A构造代码块运行");
    }
    //静态代码块
    static {
    
    
        System.out.println("A静态代码块运行");
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
       A a1 = new A();
       A a2 = new A();
    }
}

测试结果:

A静态代码块运行 
A构造代码块运行
A构造方法运行
A构造代码块运行
A构造方法运行

注意:静态代码只加载一次,代码块执行顺序:静态代码块 → 构造代码块 → 构造方法

3.1 误区

简单地认为Java静态代码块在类被加载时就会自动执行。证错如下:

class A {
    
    
    //构造方法
    A(){
    
    
        System.out.println("A构造方法运行");
    }
    //构造代码块
    {
    
    
        System.out.println("A构造代码块运行");
    }
    //静态代码块
    static {
    
    
        System.out.println("A静态代码块运行");
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        System.out.println(A.class);
    }
}

测试结果:

class com.exercise.A      //A类被定义在com.exercise包中

并没有输出A类中的静态代码块内容,由此可见,静态代码块不是在类被仅仅加载时调用的。

3.2 正解

如果了解JVM原理,我们知道,一个类的运行分为以下步骤:

  1. 装载
  2. 连接
  3. 初始化
  4. 使用
  5. 卸载

3.2.1 装载

装载阶段由三个基本动作组成:

  1. 通过类型的完全限定名,产生一个代表该类型的二进制数据流
  2. 解析这个二进制数据流为方法区内的内部数据结构,创建一个表示该类型的java.lang.Class类的实例
  3. 如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。

3.2.2 连接

连接阶段也分为三部分:

  1. 验证。确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),还需要进行符号引用的验证。
  2. 准备。Java虚拟机为类变量分配内存,设置默认初始值。
  3. 解析。在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。

3.2.3 初始化

类的主动引用时,Java虚拟就会对其初始化,如下六种情况为主动使用:

  • 当创建某个类的新实例时,如通过new或者反射,克隆,反序列化等。
  • 当调用某个类的静态方法时。
  • 当使用某个类或接口的静态字段时。
  • 当调用Java API中的某些反射方法时,如类Class中的方法,或者java.lang.reflect中的类的方法时。
  • 当初始化一个子类时,如果其父类没有被初始化,则会先初始化它的父类。
  • 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)。

实际上,static块的执行发生在“初始化”的阶段。故此,只要一个类不止经历了装载,并且经历了初始化阶段,那么JVM就会完成对静态变量的初始化、静态块执行等工作。

下面我们看看执行static块的几种情况:

  • 使用new A()的过程

  • 使用Class.forName(“A”)的过程,因为这个过程相当于Class.forName(“A”,true,this.getClass().getClassLoader());

  • 使用Class.forName(“A”,false,this.getClass().getClassLoader())的过程则不会打印信息,因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。

4. 继承关系的代码块运行顺序

现在设计两个类:A 和 B,并且 B 继承 A 。实例代码如下:

class A {
    
    
    //构造方法
    A(){
    
    
        System.out.println("A构造方法");
    }
    //构造代码块
    {
    
    
        System.out.println("A构造代码块");
    }
    //静态代码块
    static {
    
    
        System.out.println("A静态代码块");
    }
}

class B extends A{
    
    
    //构造方法
    B(){
    
    
        System.out.println("B构造方法");
    }
    //构造代码块
    {
    
    
        System.out.println("B构造代码块");
    }
    //静态代码块
    static {
    
    
        System.out.println("B静态代码块");
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        B b = new B();
    }
}

测试结果:

A静态代码块
B静态代码块
A构造代码块
A构造方法
B构造代码块
B构造方法

分析:首先要知道静态代码块是随着类的加载而加载,而构造代码块和构造方法都是随着对象的创建而加载。

(1)在编译 B.java 时,由于 B 继承 A ,所以先加载了 A 类,因此 A 类的静态代码块首先执行,而后加载 B 类,B类的静态代码块执行。

(2)然后创建 B 的对象,大家都知道构造代码块优先于构造方法执行,这时候问题来了,这时应该先看 B 类的构造方法,B 类里的构造方法里有一句隐式的“super()”首先被执行,所以找到 A 类的构造方法,而 A 类的构造方法中也有一句隐式的“super()”执行(调用Object类的构造方法),但并没有什么返回结果,接下来才是在执行 A 类构造方法的方法体前先执行 A 类的构造代码块(输出”A构造代码块“),然后再执行 A 类构造方法的方法体(输出“A构造方法”),最后又回到 B 类的构造方法中,这时 B 类的super()已经执行完了, 然后在执行 B 类构造方法的方法体前先执行 B 类的构造代码块(输出“B构造代码块”),最后执行 B 类构造方法的方法体(输出“B构造方法”)。

5.阿里面试题

public class B {
    
    
    public static B b1 = new B();
    public static B b2 = new B();
    
    {
    
    
        System.out.println("构造块");
    }
    
    static {
    
    
        System.out.println("静态块");
    }
 
    public static void main(String[] args) {
    
    
        B b =new B(); 
    }
}

测试结果:

构造块
构造块
静态块
构造块

为什么不是:静态块、构造块、构造块、构造块??

原来是因为静态声明的缘故,把b1,b2也上升到静态位,从而与静态块处于同一优先级,同一优先级就按先后顺序来执行,所以执行顺序是构造对象、构造对象、静态块。

6. 总结

(1)虚拟机在首次加载Java类时,会对静态代码块、静态成员变量、静态方法进行一次初始化。静态代码块只执行一次

(2)构造代码块在每次new对象都会执行

(3)程序运行时,先加载父类(类加载顺序的问题)

(4)按照父子类继承关系进行初始化,先执行父类的初始化

(5)静态方法会被首先加载,但是不会被执行,只有调用的时候才会被执行

  • 无继承关系的初始化顺序:静态代码块/变量 → 非静态代码块/变量 → 构造方法

  • 有继承关系的初始化顺序:父类静态代码块/成员变量 → 子类静态代码块/变量 → 父类非静态代码块/变量 → 父类构造方法 → 子类非静态代码块/变量 → 子类构造方法

补充说明:

  1. 无法在静态方法里引用实例变量、也无法调用实例方法,但是可以调用静态变量和静态方法。

  2. 无法在静态方法里使用this关键字和super关键字,因为静态方法是属于类级别的,不存在关于对象的操作。

  3. 无法在静态方法里声明其他静态变量(类变量只能定义在类中,不能存在于方法中)

  4. 无法在静态方法里使用域修饰符来声明变量:public、protected、private,只能使用默认的访问域(这一点同样适用于实例方法)

猜你喜欢

转载自blog.csdn.net/weixin_43653599/article/details/104350391