Java基础知识01--Java类的初始化顺序(静态变量、普通成员变量、静态代码块、构造方法的执行顺序);static静态代码块执行时机
1.代码块的分类
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
1.1 静态代码块(也叫静态块、静态初始化块)
Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次,也就是说这些代码不需要实例化类就能够被调用。一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块,所以静态块常用来执行类属性的初始化!
关于Static静态代码块的五个小结点
1、Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次
2、静态块常用来执行类属性的初始化
3、静态块优先于各种代码块以及构造函数,如果一个类中有多个静态代码块,会按照书写顺序依次执行
4、静态代码块可以定义在类的任何地方中除了方法体中【这里的方法体是任何方法体】
5、静态代码块不能访问普通变量
针对4中描述静态代码块不能存在任何方法体中的原因其实也是很简单,由于普通方法是通过加载类,然后new出实例化对象,通过对象才能运行这个方法,而静态代码块只需要加载类之后就能运行了。对于静态方法,在类加载的时候,静态方法也已经加载了,但是我们必须要通过类名或者对象名才能访问,也就是说相比于静态代码块,静态代码块是自己主动运行的,而静态方法是被动调用运行的。不管是哪种方法,我们需要明确静态代码块的存在在类加载的时候就自动运行了,而放在不管是普通方法还是静态方法中,都是不能自动运行的。
1.2 构造代码块(也叫构造初始化块)
听这名字就知道和构造方法离不开!没错,但是还是和构造方法有着本质区别,我们都知道,没个方法中都可以有很多构造方法,每创建一个对象其构造方法就执行一个,而一个构造方法可以创建N个对象,构造方法就比较“高冷”了,而构造代码块就比较“舔狗”了,只要该类实例了一个对象,构造代码就执行一次,利用每次创建对象的时候都会提前调用一次构造代码块特性,所以它可以做统计创建对象的次数功能。当然构造代码块用的相对少!
构造代码块小结:
1、构造代码块在创建对象时被调用,每次创建对象都会调用一次
2、构造代码块优先于构造函数执行,同时构造代码块的运行依赖于构造函数
3、构造代码块在类中定义
针对2中的“依赖”可理解为如果不实例化对象(也就是不执行构造方法),构造代码块是不会执行的!
1.3 代码块(又叫普通代码块、初始化块)
代码块小结
1、普通代码块定义在方法体中
2、普通代码块与构造代码块的格式一致都是{}
3、普通代码块与构造代码块唯一能直接看出的区别是构造代码块是在类中定义的,而普通代码块是在方法体中定义的
1.4 代码测试
package com.ttbank.flep.test; /** * @Author lucky * @Date 2022/6/28 16:57 */ public class Initializationblock { int intA; int intB; public Initializationblock() { System.out.println("无参构造器00000000"); } public Initializationblock(int a) { System.out.println("一个参数的构造器"); } { intA = 10; intB = 15; System.out.println("构造初始化块11111"); } { System.out.println("构造初始化块22222"); } { System.out.println("构造初始化块33333"); } //静态初始化块 static { System.out.println("静态初始化块01010101"); } static { System.out.println("静态初始化块0202020202"); } public void method(){ { System.out.println("普通初始化块"); } } }
测试demo
package com.ttbank.flep.test; /** * @Author lucky * @Date 2022/6/28 16:58 */ public class Test3 { /** * @param args * 因为静态块是在类的初始化阶段完成的, * 因此在创建某个类的第二个对象时,该类的静态块就不会执行了 * * 在单个类中,静态初始化块,初始化块,构造器 * 多个类的继承中初始化块、静态初始化块、构造器的执行顺序 在继承中,先后执行父类A的静态块,父类B的静态块,最后子类的静态块,然后再执行父类A的非静态块和构造器,然后是B类的非静态块和构造器,最后执行子类的非静态块和构造器 */ public static void main(String[] args) { Initializationblock initializationblock = new Initializationblock(); initializationblock.method(); System.out.println("------------"); //多打印几个对象的目的是:好看出Static静态代码块只执行一次!!! Initializationblock initializationblock2 = new Initializationblock(); //因为静态块是在类的初始化阶段完成的,因此在创建某个类的第二个对象时,该类的静态块就不会执行了 initializationblock2.method(); Initializationblock initializationblock3 = new Initializationblock(); initializationblock3.method(); } }
打印结果
静态初始化块01010101 静态初始化块0202020202 构造初始化块11111 构造初始化块22222 构造初始化块33333 无参构造器00000000 普通初始化块 ------------ 构造初始化块11111 构造初始化块22222 构造初始化块33333 无参构造器00000000 普通初始化块 构造初始化块11111 构造初始化块22222 构造初始化块33333 无参构造器00000000 普通初始化块
得出结论:执行顺序静态代码块 > 构造代码块 > 构造函数 > 普通代码块
2.static静态代码块执行时机
static块真正的执行时机。如果了解JVM原理,我们知道,一个类的运行分为以下步骤:
(1) 装载
(2) 连接
(3) 初始化
2.1 装载阶段
其中装载阶段又三个基本动作组成:
(1) 通过类型的完全限定名,产生一个代表该类型的二进制数据流
(2) 解析这个二进制数据流为方法区内的内部数据结
(3) 构创建一个表示该类型的java.lang.Class类的实例
另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。
2.2 连接阶段
连接阶段又分为三部分:
(1) 验证,确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),另外还需要进行符号引用的验证。
(2) 准备,Java虚拟机为类变量分配内存,设置默认初始值。
(3) 解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
2.3 初始化阶段
当一个类被主动使用时,Java虚拟就会对其初始化,如下六种情况为主动使用:
(1) 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
(2) 当调用某个类的静态方法时
(3) 当使用某个类或接口的静态字段时
(4) 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
(5) 当初始化某个子类时
(6) 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。
2.4 总结
实际上,static块的执行发生在“初始化”的阶段。初始化阶段,jvm主要完成对静态变量的初始化,静态块执行等工作。
下面我们看看执行static块的几种情况:
1、第一次new A()的过程会打印"";因为这个过程包括了初始化
2、第一次Class.forName("A")的过程会打印"";因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader());
3、第一次Class.forName("A",false,this.getClass().getClassLoader())的过程则不会打印""。因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。
3.单个类的初始化顺序
对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。
图示:
通过下面的测试代码来验证这一点:
package com.ttbank.flep.file.test; public class TradeTest { // 静态变量 public static String staticField = "静态变量"; // 变量 public String field = "变量"; // 静态初始化块 static { System.out.println(staticField); //注意:静态初始化块中使用静态变量,所以静态变量要在静态代码块前 System.out.println("静态初始化块"); } // 初始化块 { System.out.println(field); System.out.println("初始化块"); } // 构造方法 public TradeTest() { System.out.println("构造方法"); } public static void main(String[] args) { new TradeTest(); new TradeTest(); } }
执行效果图:
静态代码块,在虚拟机加载类的时候就会加载执行,而且只执行一次;
非静态代码块,在创建对象的时候(即new一个对象的时候)执行,每次创建对象都会执行一次。
4.对于继承的情况初始化顺序
父类:
package com.ttbank.flep.file.test; public class Father { // 静态变量 public static String f_StaticField = "父类--静态变量"; protected int i = 1; protected int j = 8; // 变量 public String f_Field = "父类--变量"; // 静态初始化块 static { System.out.println(f_StaticField); System.out.println("父类--静态初始化块"); } // 初始化块 { System.out.println(f_Field); System.out.println("父类--初始化块"); } // 构造方法 public Father() { System.out.println("父类--构造器"); System.out.println("i=" + i + ", j=" + j); j = 9; } }
子类:
package com.ttbank.flep.file.test; public class Son extends Father { // 静态变量 public static String s_StaticField = "子类--静态变量"; // 变量 public String s_Field = "子类--变量"; // 静态初始化块 static { System.out.println(s_StaticField); System.out.println("子类--静态初始化块"); } // 初始化块 { System.out.println(s_Field); System.out.println("子类--初始化块"); } // 构造器 public Son() { System.out.println("子类--构造器"); System.out.println("i=" + i + ",j=" + j); } // 程序入口 public static void main(String[] args) { new Son(); } }
执行结果:
结论:执行的大致顺序如下,
(1) 在一个不存在继承的类中:初始化static变量,执行static初始化块-->初始化普通成员变量(如果有赋值语句),执行普通初始化块-->构造方法
(2)在一个存在继承的类中:初始化父类static成员变量,运行父类static初始化块-->初始化子类static成员变量,运行子类static初始化块-->初始化父类实例成员变量(如果有赋值语句),执行父类普通初始化块-->父类构造方法-->初始化子类实例成员变量(如果有赋值语句)及普通初始化块-->子类构造方法。
参考文献:https://blog.csdn.net/xyajia/article/details/80922329、
https://blog.csdn.net/qq_44543508/article/details/102593419---推荐
https://blog.csdn.net/berber78/article/details/46472789---推荐