基本上代码块分为三种:静态(Static)代码块、构造(动态)代码块、普通(局部)代码块
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
1.代码块(又叫普通代码块、初始化块、局部代码块)
局部代码块:定义在方法内部的代码块。
作用:会缩小变量的使用范围,提前释放局部变量,节省内存。
代码块总结:
- 普通代码块定义在方法体中
- 普通代码块与构造代码块的格式一致都是
{}
- 普通代码块与构造代码块唯一能直接看出的区别是构造代码块是在类中定义的,而普通代码块是在方法体中定义的
测试案例:
public class TestLocalBlock {
public static void main(String[] args) {
int num = 100;
//局部代码块
{
int age = 20;
System.out.println("age = " + age);
System.out.println("num = " + num);
}
int num2 = 500;
System.out.println("num = " + num);
System.out.println("num2 = " + num2);
}
}
运行结果:
2.构造(动态)代码块:
听这名字就知道和构造方法离不开!没错,但是还是和构造方法有着本质区别,我们都知道,没个方法中都可以有很多构造方法,每创建一个对象其构造方法就执行一个,而一个构造方法可以创建N个对象,构造方法就比较“高冷”了,而构造代码块就比较“舔狗”了,只要该类实例了一个对象,构造代码就执行一次,利用每次创建对象的时候都会提前调用一次构造代码块特性,所以它可以做统计创建对象的次数功能。当然构造代码块用的相对少!
动态代码块:定义在方法外,类内部的代码块。
执行时机:创建对象时,触发动态代码块的执行。
作用:可为实例属性初始化。
构造(动态)代码块总结:
- 构造代码块在创建对象时被调用,每次创建对象都会调用一次
- 构造代码块优先于构造函数执行,同时构造代码块的运行依赖于构造函数
- 构造代码块在类中定义
测试代码:
public class TestDynamicBlock {
public static void main(String[] args) {
new MyClass();
}
}
class MyClass{
String field = "实例属性";
{
System.out.println(field);
System.out.println("动态代码块");
}
public MyClass(){
System.out.println("构造方法");
}
}
运行结果:
实例属性
动态代码块
构造方法
3.静态代码块(也叫静态块、静态初始化块)
Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次,也就是说这些代码不需要实例化类就能够被调用。一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块,所以静态块常用来执行类属性的初始化!
静态代码块:使用Static修饰的动态代码块
执行时机:类加载时,触发静态代码块的执行(仅一次)。
作用:可为静态属性初始化。
注:方法只有被调用才会执行。
Static代码块总结:
- Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次
- 静态块常用来执行类属性的初始化
- 静态块优先于各种代码块以及构造函数,如果一个类中有多个静态代码块,会按照书写顺序依次执行
- 静态代码块可以定义在类的任何地方中除了方法体中【这里的方法体是任何方法体】
- 静态代码块不能访问普通变量
测试代码:
public class TestStaticBlock {
public static void main(String[] args) {
}
}
class MyClass{
static String sField = "静态属性";
static {
System.out.println(sField);
System.out.println("静态代码块");
}
public static void method(){
/**
* 无代码
* 只为触发静态属性的初始化
* 和静态代码块的执行
* */
}
}
运行结果:
静态属性
静态代码块
4.类加载
类加载: JVM首次使用某个类时,需通过CLASSPATH查找该类的.class文件。 将.class文件中对类的描述信息加载到内存中,进行保存。 如:包名、类名、父类、属性、方法、构造方法...
JVM并不是一开始就会将所有的类加载到内存,而是用到某个类,才会去加载,只加载一次。
类加载分为三个部分:加载、连接、初始化
触发类加载:
- 创建对象。Student s=new Student();
- 创建子类对象。
- 访问静态属性。
- 调用静态方法。
- 主动加载:Class.forName(“全限定名”);
4.1 加载
类的加载主要的职责为将.class文件的二进制字节流读入内存(JDK1.7及之前为JVM内存,JDK1.8及之后为本地内存),并在堆内存中为之创建Class对象,作为.class进入内存后的数据的访问入口。在这里只是读入二进制字节流,后续的验证阶段就是要拿二进制字节流来验证.class文件,验证通过,才会将.class文件转为运行时数据结构。
4.2 连接
类的连接分为三个阶段:验证、准备、解析。
验证: 该阶段主要是为了保证加载进来的字节流符合JVM的规范,不会对JVM有安全性问题。其中有对元数据的验证,例如检查类是否继承了被final修饰的类;还有对符号引用的验证,例如校验符号引用是否可以通过全限定名找到,或者是检查符号引用的权限(private、public)是否符合语法规定等。
准备: 准备阶段的主要任务是为类的类变量开辟空间并赋默认值。
- 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
- 静态变量是引用类型的,默认值为null
- 静态常量默认值为声明时设定的值
例如:public static final int i = 3; 在准备阶段,i的值即为3
解析: 该阶段的主要职责为将Class在常量池中的符号引用转变为直接引用,此处针对的是静态方法及属性和私有方法与属性,因为这类方法与私有方法不能被重写,静态属性在运行期也没有多态这一说,即在编译器可知,运行期不可变,所以适合在该阶段解析,譬如类方法main替换为直接引用,为静态连接,区别于运行时的动态连接(后续我会写关于JVM内存结构的文章,在讲解栈帧时会介绍动态链接)。
符号引用即字符串,说白了可以是一个字段名,或者一个方法名;直接引用即偏移量,说白了就是类的元信息位于内存的地址串,例如,一个类的方法为test(),则符号引用即为test,这个方法存在于内存中的地址假设为0x123456,则这个地址则为直接引用。
4.3 初始化
该阶段主要是为类的类变量初始化值的,初始化有两种方式:
- 在声明类变量时,直接给变量赋值
- 在静态初始化块为类变量赋值
4.4 类加载顺序
- 先加载并连接当前类
- 父类没有被加载,则去加载、连接、初始化父类,依旧是先加载并连接,然后再判断有无父类,如此循环,所以JVM先将Object加载
- 如果类中有初始化语句,包括声明时赋值与静态初始化块,则按顺序进行初始化
由此可以理解类的初始化顺序:先执行父类静态变量赋值、父类静态初始化块,再执行子类静态属性赋值、静态初始化块。
4.5 类的加载时机(包括加载、连接、初始化)
- 创建该类的实例
- 调用该类的类方法
- 访问类或接口的类变量,或为类变量赋值
- 利用反射Class.forName(String name, boolean initialize,ClassLoader loader);当使用ClassLoader类的loadClass()方法来加载类时,该类只进行加载阶段,而不会经历初始化阶段,使用Class类的静态方法forName(),根据initialize来决定会不会初始化该类,不传该参数默认强制初始化
- 初始化该类的子类
- 运行main方法,main方法所在类会被加载
案例:
解析:
在main方法中,执行Demo.show()语句,调用静态方法,所以Demo类开始加载。在准备阶段静态变量count1和count2被系统赋默认值为0。然后再初始化过程中,先执行new Demo()操作创建demo对象,同时调用无参构造,count1和count2的值变为1.然后执行下面俩条语句,count1没有被初始化,所以值仍为1,而count2被初始化为0,所以最终输出结果如图所示。