类的实例化是指创建一个类的实例(对象)的过程;
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。
一个对象在可以被使用之前必须要被正确地实例化。从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。
一、Java对象创建过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
实例变量初始化与实例代码初始化发生在构造函数执行之前。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。即:
父类构造函数
实例变量赋值,实例代码块
本身构造函数代码
相当于
Java是按照代码顺序来执行实例变量初始化和实例代码块的,不允许顺序靠前的实例代码块使用在其后面定义的实例变量。
以下代码报错:使用了一个为定义的变量。
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性
Java强制要求除Object对象之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,若既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会自动生成一个对超类构造函数的调用。
嗯。。其实这里有点不明白。
类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象,这里所谓的实例构造器<init>()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的。
一个实例变量在对象初始化的过程中会被赋值几次?(4次)
JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,且该赋值过程是没有办法避免的,1次。
若在声明实例变量x的同时对其进行了赋值操作,2次。
若在实例代码块中,又对变量x做了初始化操作,3次。
若在构造函数中,也对变量x做了初始化操作,4次。
总的来说,类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数init<>()。
每次实例化都存在,实例变量赋值,实例代码块执行,实例函数执行的过程
二. 典型案例分析
父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。至于为什么是这样的一个过程,笔者在本文的姊妹篇《 深入理解Java对象的创建过程:类的初始化与实例化》很好的解释了这个问题。
那么,我们看看下面的程序的输出结果:
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}
static StaticTest st = new StaticTest();
static { //静态代码块
System.out.println("1");
}
{ // 实例代码块
System.out.println("2");
}
StaticTest() { // 实例构造器
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() { // 静态方法
System.out.println("4");
}
int a = 110; // 实例变量
static int b = 112; // 静态变量
}/* Output:
2
3
a=110,b=0
1
4
*///:~
大家能得到正确答案吗?虽然笔者勉强猜出了正确答案,但总感觉怪怪的。因为在初始化阶段,当JVM对类StaticTest进行初始化时,首先会执行下面的语句:
static StaticTest st = new StaticTest();
实例初始化不一定要在类初始化结束之后才开始初始化。 下面我们结合类的加载过程说明这个问题。
我们知道,类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,并且只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,因此我们只针对这两个阶段进行分析:
首先,在类的准备阶段需要做的是为类变量(static变量)分配内存并设置默认值(零值),因此在该阶段结束后,类变量st将变为null、b变为0。特别需要注意的是,如果类变量是final的,那么编译器在编译时就会为value生成ConstantValue属性,并在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值。也就是说,如果上述程度对变量b采用如下定义方式时:
static final int b=112
那么,在准备阶段b的值就是112,而不再是0了。
此外,在类的初始化阶段需要做的是执行类构造器<clinit>(),需要指出的是,类构造器本质上是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器<clinit>()。因此,对上述程序而言,JVM将先执行第一条静态变量的赋值语句:
st = new StaticTest ()
实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置。这就导致了实例初始化完全发生在静态初始化之前,当然,这也是导致a为110b为0的原因。
因此,上述程序的StaticTest类构造器<clinit>()的实现等价于:
public class StaticTest {
<clinit>(){
a = 110; // 实例变量
System.out.println("2"); // 实例代码块
System.out.println("3"); // 实例构造器中代码的执行
System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行
类变量st被初始化
System.out.println("1"); //静态代码块
类变量b被初始化为112
}
}
因此,上述程序会有上面的输出结果。下面,我们对上述程序稍作改动,如下所示:
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}
static StaticTest st = new StaticTest();
static {
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest() {
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() {
System.out.println("4");
}
int a = 110;
static int b = 112;
static StaticTest st1 = new StaticTest();
}
在程序最后的一行,增加以下代码行:
static StaticTest st1 = new StaticTest();
- 1
那么,此时程序的输出又是什么呢?如果你对上述的内容理解很好的话,不难得出结论(只有执行完上述代码行后,StaticTest类才被初始化完成),即:
2
3
a=110,b=0
1
2
3
a=110,b=112
4
另外,下面这道经典题目也很有意思,如下:
class Foo {
int i = 1;
Foo() {
System.out.println(i);
int x = getValue();
System.out.println(x);
}
{
i = 2;
}
protected int getValue() {
return i;
}
}
//子类
class Bar extends Foo {
int j = 1;
Bar() {
j = 2;
}
{
j = 3;
}
@Override
protected int getValue() {
return j;
}
}
public class ConstructorExample {
public static void main(String... args) {
Bar bar = new Bar();
System.out.println(bar.getValue());
}
}
输出:
2
0
2