初始化块和构造器、成员变量、成员方法一起,是唯四能在类中出现的成员。
- 初始化块的作用和构造器类似,目的是对对象进行初始化操作;
- 一个类中可以有多个初始化块;
- 初始化块只有两种修饰方式:
static
和default
。用static
修饰的叫类初始化块,没有修饰符的叫实例初始化块。 - 类初始化块先于实例初始化块执行。同样的修饰符,位置在前的初始化块先执行。
虽然但是,同样修饰符的代码块没必要分成一个个小块块。因为代码块就是在创建Java对象的时候隐式执行的,而且总是全部执行。所以分成小块块和合并成一个大块块,没有区别。
实例初始化块
public class Person
{
// 实例初始化块
{
var a = 6;
if (a > 4)
{
System.out.println("Person实例初始化块:局部变量a的值大于4");
}
System.out.println("Person的实例初始化块");
}
// 第二个实例初始化块
{
System.out.println("Person的第二个实例初始化块");
}
public Person()
{
System.out.println("Person的无参数构造器");
}
public static void main(String[] args)
{
new Person();
}
}
初始化块没有名字,所以无法通过显式手段调用它。它就在创建Java对象的时候隐式执行。
- 实例初始化块只在创建Java对象的时候隐式执行,且在构造器之前执行;
- 类初始化块在类初始化阶段隐式执行。
public class InstanceInitTest
{
// 实例初始化块
{
a = 6;
}
int a = 9;
public static void main(String[] args)
{
System.out.println(new InstanceInitTest().a);
}
}
最终输出结果是9.
Why?
- 代码没有报错,第5行中的a的类型由第7行决定,这是编译时确定的,a是InstanceInitTest类的成员变量;
- 实力初始化块在其他语句之前执行。因此,a首先赋值为6,然后执行成员变量声明语句,再次被赋值为9。
Java创建对象的过程
- 第一次创建某个类的对象时,要先加载类(为类成员变量、类方法,以及类代码块分配内存);
- 其余时刻,首先为该对象的所有实例变量分配空间;
- 接着,对实例变量进行初始化。
- 初始化顺序是:
- 实例初始化块或声明实例变量时指定的初始值;
- 构造器中指定的初始值。
对上面的代码,如果调换初始化块和成员变量声明语句的顺序,结果会改变:
public class InstanceInitTest
{
int a = 9;
// 实例初始化块
{
a = 6;
}
public static void main(String[] args)
{
System.out.println(new InstanceInitTest().a);
}
}
输出结果会变成6.
初始化块和构造器的异同
- 实力初始化块总是在构造器执行之前执行;
- 实例初始化块没法接受外部参数,因此它所能执行的初始化处理对于该类的所有对象都只能是相同的。
由此可以倒推出实例初始化块的作用:当有一段实例变量初始化代码完全相同,且不需要外部传参时,可以把这段代码放到实力初始化块中,方便代码阅读;
此外,多个构造器中不需要外部传参的操作,如果有相同的,也可以提取出来放到实例代码块中,以提升代码复用水平。
实例初始化块其实是个假象。使用javac编译Java类后,该类中的实例初始化块会消失:里面的代码会“回归”到每个构造器中,且位于构造器中代码的最前面。
类初始化块
类初始化块通常用于对类变量执行初始化处理。类初始化块不能对实例变量进行初始化。
类初始化块回向上追溯,先执行父类的类初始化块,依次往下。
class Root
{
static{
System.out.println("Root的类初始化块");
}
{
System.out.println("Root的实例初始化块");
}
}
class Mid extends Root
{
static {
System.out.println("Mid的类初始化块");
}
{
System.out.println("Mid的实例初始化块");
}
public Mid()
{
System.out.println("Mid的无参数构造器");
}
public Mid(String msg)
{
// 通过this调用无参数构造器
this();
System.out.println("Mid的带参数构造器,参数值: " + msg);
}
}
class Leaf extends Mid
{
static {
System.out.println("Leaf的类初始化块");
}
{
System.out.println("Leaf的实例初始化块");
}
public Leaf()
{
// 通过super调用父类中有一个String参数的构造器
super("疯狂Java讲义");
System.out.println("执行Leaf的构造器");
}
}
public class InstanceInitTest
{
public static void main(String[] args)
{
new Leaf();
new Leaf();
}
}
- 这段代码描述了三个类的继承关系,从父类到子类依次是:Root–>Mid–>Leaf
- main中,第一次执行
new Leaf()
,是第一次加载类。此时系统中不存在Leaf类,所以要先加载并初始化Leaf类。这是就会- 先执行类初始化块。这部分又会遵循回溯机制,先执行父类的类初始化块,再往下依次执行。因此会出现先执行Root的类初始化块,然后是Mid的类初始化块,最后是Leaf的类初始化块。
- 接着,执行实例初始化块和构造器。注意,前面说过实例初始化块其实就是构造器的一部分。所以这里会先执行Root的实例初始化块+构造器,然后是Mid的实例初始化块+构造器,最后是Leaf的实例初始化块+构造器。
- 第二次执行
new Leaf()
时,就不会再进行类初始化了。
类初始化块和声明类变量(static修饰)的初始化代码和上面说过的实例初始化一样,按顺序执行。
public class StaticInitTest
{
// 先执行类初始化块,将a赋值为6
static {
a = 6;
}
// 再将a类变量赋值为9
static int a = 9;
public static void main(String[] args)
{
// 下面的代码将输出9
System.out.println(StaticInitTest.a);
}
}