出处:http://blog.jobbole.com/23939/java
在Java中,一个对象在能够被使用以前必需要被正确地初始化,这一点是Java规范规定的。本文试图对Java如何执行对象的初始化作一个详细深 入地介绍(与对象初始化相同,类在被加载以后也是须要初始化的,本文在最后也会对类的初始化进行介绍,相对于对象初始化来讲,类的初始化要相对简单一 些)。编程
1.Java对象什么时候被初始化app
Java对象在其被建立时初始化,在Java代码中,有两种行为能够引发对象的建立。其中比较直观的一种,也就是一般所说的显式对象建立,就是经过 new关键字来调用一个类的构造函数,经过构造函数来建立一个对象,这种方式在java规范中被称为“由执行类实例建立表达式而引发的对象建立”。
固然,除了显式地建立对象,如下的几种行为也会引发对象的建立,可是并非经过new关键字来完成的,所以被称做隐式对象建立,他们分别是:ide
● 加载一个包含String字面量的类或者接口会引发一个新的String对象被建立,除非包含相同字面量的String对象已经存在与虚拟机内了(JVM 会在内存中会为全部碰到String字面量维护一份列表,程序中使用的相同字面量都会指向同一个String对象),好比,函数
1
2
3
4
|
class StringLiteral {
private String str = "literal" ;
private static String sstr = "s_literal" ;
}
|
● 自动装箱机制可能会引发一个原子类型的包装类对象被建立,好比,ui
1
2
3
|
class PrimitiveWrapper {
private Integer iWrapper = 1 ;
}
|
● String链接符也可能会引发新的String或者StringBuilder对象被建立,同时还可能引发原子类型的包装对象被建立,好比 (本人试了下,在mac ox下1.6.0_29版本的javac,对待下面的代码会经过StringBuilder来完成字符串的链接,并无将i包装成Integer,由于 StringBuilder的append方法有一个重载,其方法参数是int),this
1
2
3
4
5
6
7
|
public class StringConcatenation {
private static int i = 1 ;
public static void main(String... args) {
System.out.println( "literal" + i);
}
}
|
2.Java如何初始化对象编码
当一个对象被建立以后,虚拟机会为其分配内存,主要用来存放对象的实例变量及其从超类继承过来的实例变量(即便这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值。spa
引用线程
关于实例变量隐藏
1
2
3
4
5
6
7
8
9
10
11
|
class Foo {
int i = 0 ;
}
class Bar extends Foo {
int i = 1 ;
public static void main(String... args) {
Foo foo = new Bar();
System.out.println(foo.i);
}
}
|
上面的代码中,Foo和Bar中都定义了变量i,在main方法中,咱们用Foo引用一个Bar对象,若是实例变量与方法同样,容许被覆盖,那么打印的结果应该是1,可是实际的结果确是0。
可是若是咱们在Bar的方法中直接使用i,那么用的会是Bar对象本身定义的实例变量i,这就是隐藏,Bar对象中的i把Foo对象中的i给隐藏了,这条规则对于静态变量一样适用。
在内存分配完成以后,java的虚拟机就会开始对新建立的对象执行初始化操做,由于java规范要求在一个对象的引用可见以前须要对其进行初始化。在Java中,三种执行对象初始化的结构,分别是实例初始化器、实例变量初始化器以及构造函数。
2.1. Java的构造函数
每个Java中的对象都至少会有一个构造函数,若是咱们没有显式定义构造函数,那么Java编译器会为咱们自动生成一个构造函数。构造函数与类中 定义的其余方法基本同样,除了构造函数没有返回值,名字与类名同样以外。在生成的字节码中,这些构造函数会被命名成<init>方法,参数列 表与Java语言书写的构造函数的参数列表相同(<init>这样的方法名在Java语言中是非法的,可是对于JVM来讲,是合法的)。另 外,构造函数也能够被重载。
Java要求一个对象被初始化以前,其超类也必须被初始化,这一点是在构造函数中保证的。Java强制要求Object对象(Object是 Java的顶层对象,没有超类)以外的全部对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其余的构造函数,若是咱们即没有调用其 他的构造函数,也没有显式调用超类的构造函数,那么编译器会为咱们自动生成一个对超类构造函数的调用指令,好比,
1
2
3
|
public class ConstructorExample {
}
|
对于上面代码中定义的类,若是观察编译以后的字节码,咱们会发现编译器为咱们生成一个构造函数,以下,
1
2
3
|
aload_0
invokespecial # 8 ; //Method java/lang/Object."<init>":()V
return
|
上面代码的第二行就是调用Object对象的默认构造函数的指令。
正由于如此,若是咱们显式调用超类的构造函数,那么调用指令必须放在构造函数全部代码的最前面,是构造函数的第一条指令。这么作才能够保证一个对象在初始化以前其全部的超类都被初始化完成。
若是咱们在一个构造函数中调用另一个构造函数,以下所示,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class ConstructorExample {
private int i;
ConstructorExample() {
this ( 1 );
....
}
ConstructorExample( int i) {
....
this .i = i;
....
}
}
|
对于这种状况,Java只容许在ConstructorExample(int i)内出现调用超类的构造函数,也就是说,下面的代码编译是没法经过的,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class ConstructorExample {
private int i;
ConstructorExample() {
super ();
this ( 1 );
....
}
ConstructorExample( int i) {
....
this .i = i;
....
}
}
|
或者,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class ConstructorExample {
private int i;
ConstructorExample() {
this ( 1 );
super ();
....
}
ConstructorExample( int i) {
....
this .i = i;
....
}
}
|
Java对构造函数做出这种限制,目的是为了要保证一个类中的实例变量在被使用以前已经被正确地初始化,不会致使程序执行过程当中的错误。可是,与C 或者C++不一样,Java执行构造函数的过程与执行其余方法并无什么区别,所以,若是咱们不当心,有可能会致使在对象的构建过程当中使用了没有被正确初始 化的实例变量,以下所示,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
class Foo {
int i;
Foo() {
i = 1 ;
int x = getValue();
System.out.println(x);
}
protected int getValue() {
return i;
}
}
class Bar extends Foo {
int j;
Bar() {
j = 2 ;
}
@Override
protected int getValue() {
return j;
}
}
public class ConstructorExample {
public static void main(String... args) {
Bar bar = new Bar();
}
}
|
若是运行上面这段代码,会发现打印出来的结果既不是1,也不是2,而是0。根本缘由就是Bar重载了Foo中的getValue方法。在执行Bar 的构造函数是,编译器会为咱们在Bar构造函数开头插入调用Foo的构造函数的代码,而在Foo的构造函数中调用了getValue方法。因为Java对 构造函数的执行没有作特殊处理,所以这个getValue方法是被Bar重载的那个getValue方法,而在调用Bar的getValue方法 时,Bar的构造函数尚未被执行,这个时候j的值仍是默认值0,所以咱们就看到了打印出来的0。
2.2. 实例变量初始化器与实例初始化器
咱们能够在定义实例变量的同时,对实例变量进行赋值,赋值语句就时实例变量初始化器了,好比,
1
2
3
4
|
public class InstanceVariableInitializer {
private int i = 1 ;
private int j = i + 1 ;
}
|
若是咱们以这种方式为实例变量赋值,那么在构造函数执行以前会先完成这些初始化操做。
咱们还能够经过实例初始化器来执行对象的初始化操做,好比,
1
2
3
4
5
6
7
8
9
|
public class InstanceInitializer {
private int i = 1 ;
private int j;
{
j = 2 ;
}
}
|
上面代码中花括号内代码,在Java中就被称做实例初始化器,其中的代码一样会先于构造函数被执行。
若是咱们定义了实例变量初始化器与实例初始化器,那么编译器会将其中的代码放到类的构造函数中去,这些代码会被放在对超类构造函数的调用语句以后 (还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数自己的代码以前。咱们来看下下面这段Java代码被编译以后的字 节码,Java代码以下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class InstanceInitializer {
private int i = 1 ;
private int j;
{
j = 2 ;
}
public InstanceInitializer() {
i = 3 ;
j = 4 ;
}
}
|
编译以后的字节码以下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
aload_0
invokespecial # 11 ; //Method java/lang/Object."<init>":()V
aload_0
iconst_1
putfield # 13 ; //Field i:I
aload_0
iconst_2
putfield # 15 ; //Field j:I
aload_0
iconst_3
putfield # 13 ; //Field i:I
aload_0
iconst_4
putfield # 15 ; //Field j:I
return
|
上面的字节码,第4,5行是执行的是源代码中i=1的操做,第6,7行执行的源代码中j=2的操做,第8-11行才是构造函数中i=3和j=4的操做。
Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,而且不容许顺序靠前的实例初始化器或者实例变量初始化器使用在其后被定义和初始化的实例变量,好比,
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class InstanceInitializer {
{
j = i;
}
private int i = 1 ;
private int j;
}
public class InstanceInitializer {
private int j = i;
private int i = 1 ;
}
|
上面的这些代码都是没法经过编译的,编译器会抱怨说咱们使用了一个未经定义的变量。之因此要这么作,是为了保证一个变量在被使用以前已经被正确地初始化。可是咱们仍然有办法绕过这种检查,好比,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class InstanceInitializer {
private int j = getI();
private int i = 1 ;
public InstanceInitializer() {
i = 2 ;
}
private int getI() {
return i;
}
public static void main(String[] args) {
InstanceInitializer ii = new InstanceInitializer();
System.out.println(ii.j);
}
}
|
若是咱们执行上面这段代码,那么会发现打印的结果是0。所以咱们能够确信,变量j被赋予了i的默认值0,而不是通过实例变量初始化器和构造函数初始化以后的值。
引用
一个实例变量在对象初始化的过程当中会被赋值几回?
在本文的前面部分,咱们提到过,JVM在为一个对象分配完内存以后,会给每个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。
若是咱们在实例变量初始化器中对某个实例x变量作了初始化操做,那么这个时候,这个实例变量就被第二次赋值了。
若是咱们在实例初始化器中,又对变量x作了初始化操做,那么这个时候,这个实例变量就被第三次赋值了。
若是咱们在类的构造函数中,也对变量x作了初始化操做,那么这个时候,变量x就被第四次赋值。
也就是说,一个实例变量,在Java的对象初始化过程当中,最多能够被初始化4次。
2.3. 总结
经过上面的介绍,咱们对Java中初始化对象的几种方式以及经过何种方式执行初始化代码有了了解,同时也对何种状况下咱们可能会使用到未经初始化的变量进行了介绍。在对这些问题有了详细的了解以后,就能够在编码中规避一些风险,保证一个对象在可见以前是彻底被初始化的。
3.关于类的初始化
Java规范中关于类在什么时候被初始化有详细的介绍,在3.0规范中的12.4.1节能够找到,这里就再也不多说 了。简单来讲,就是当类被第一次使用的时候会被初始化,并且只会被一个线程初始化一次。咱们能够经过静态初始化器和静态变量初始化器来完成对类变量的初始 化工做,好比,
1
2
3
4
5
6
7
|
public class StaticInitializer {
static int i = 1 ;
static {
i = 2 ;
}
}
|
上面经过两种方式对类变量i进行了赋值操做,分别经过静态变量初始化器(代码第2行)以及静态初始化器(代码第5-6行)完成。
静态变量初始化器和静态初始化器基本同实例变量初始化器和实例初始化器相同,也有相同的限制(按照编码顺序被执行,不能引用后定义和初始化的类变 量)。静态变量初始化器和静态初始化器中的代码会被编译器放到一个名为static的方法中(static是Java语言的关键字,所以不能被用做方法 名,可是JVM却没有这个限制),在类被第一次使用时,这个static方法就会被执行。上面的Java代码编译以后的字节码以下,咱们看到其中的 static方法,
1
2
3
4
5
6
7
8
|
static {};
Code:
Stack= 1 , Locals= 0 , Args_size= 0
iconst_1
putstatic # 10 ; //Field i:I
iconst_2
putstatic # 10 ; //Field i:I
return
|
在第2节中,咱们介绍了能够经过特殊的方式来使用未经初始化的实例变量,对于类变量也一样适用,好比,
1
2
3
4
5
6
7
8
9
10
11
12
|
public class StaticInitializer {
static int j = getI();
static int i = 1 ;
static int getI () {
return i;
}
public static void main(String[] args) {
System.out.println(StaticInitializer.j);
}
}
|
上面这段代码的打印结果是0,类变量的值是i的默认值0。可是,因为静态方法是不能被覆写的,所以第2节中关于构造函数调用被覆写方法引发的问题不会在此出现。