Java对象初始化详解

简介: 在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。本文试图对Java如何执行对象的初始化做一个详细深入地介绍(与对象初始化相同,类在被加载之后也是需要初始化的,本文在最后也会对类的初始化进行介绍,相对于对象初始化来说,类的初始化要相对简单一些)。

在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。本文试图对Java如何执行对象的初始化做一个详细深入地介绍(与对象初始化相同,类在被加载之后也是需要初始化的,本文在最后也会对类的初始化进行介绍,相对于对象初始化来说,类的初始化要相对简单一些)。

1.Java对象何时被初始化

Java对象在其被创建时初始化,在Java代码中,有两种行为可以引起对象的创建。其中比较直观的一种,也就是通常所说的显式对象创建,就是通过new关键字来调用一个类的构造函数,通过构造函数来创建一个对象,这种方式在java规范中被称为“由执行类实例创建表达式而引起的对象创建”。
当然,除了显式地创建对象,以下的几种行为也会引起对象的创建,但是并不是通过new关键字来完成的,因此被称作隐式对象创建,他们分别是:

● 加载一个包含String字面量的类或者接口会引起一个新的String对象被创建,除非包含相同字面量的String对象已经存在与虚拟机内了(JVM会在内存中会为所有碰到String字面量维护一份列表,程序中使用的相同字面量都会指向同一个String对象),比如,

1
2
3
4
class StringLiteral {
     private String str = "literal" ;
     private static String sstr = "s_literal" ;
}

● 自动装箱机制可能会引起一个原子类型的包装类对象被创建,比如,

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),

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如何初始化对象

当一个对象被创建之后,虚拟机会为其分配内存,主要用来存放对象的实例变量及其从超类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值。

引用

关于实例变量隐藏

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编译器会为我们自动生成一个构造函数。构造函数与类中定义的其他方法基本一样,除了构造函数没有返回值,名字与类名一样之外。在生成的字节码中,这些构造函数会被命名成方法,参数列表与Java语言书写的构造函数的参数列表相同(这样的方法名在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节中关于构造函数调用被覆写方法引起的问题不会在此出现。

http://blog.jobbole.com/23939/

目录
相关文章
|
4天前
|
Java
深入JavaSE:详解Java对象的比较。
总的来说,Java对象的比较就像海洋生物的比较,有外在的,有内在的,有面对所有情况的,也有针对特殊情况的。理解并掌握这些比较方式,就能更好地驾驭Java的世界,游刃有余地操作Java对象。
25 12
|
1月前
|
编解码 JavaScript 前端开发
【Java进阶】详解JavaScript的BOM(浏览器对象模型)
总的来说,BOM提供了一种方式来与浏览器进行交互。通过BOM,你可以操作窗口、获取URL、操作历史、访问HTML文档、获取浏览器信息和屏幕信息等。虽然BOM并没有正式的标准,但大多数现代浏览器都实现了相似的功能,因此,你可以放心地在你的JavaScript代码中使用BOM。
73 23
|
1月前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
|
2月前
|
设计模式 缓存 Java
重学Java基础篇—Java对象创建的7种核心方式详解
本文全面解析了Java中对象的创建方式,涵盖基础到高级技术。包括`new关键字`直接实例化、反射机制动态创建、克隆与反序列化复用对象,以及工厂方法和建造者模式等设计模式的应用。同时探讨了Spring IOC容器等框架级创建方式,并对比各类方法的适用场景与优缺点。此外,还深入分析了动态代理、Unsafe类等扩展知识及注意事项。最后总结最佳实践,建议根据业务需求选择合适方式,在灵活性与性能间取得平衡。
123 3
|
1月前
|
存储 缓存 Java
理解Java引用数据类型:它们都是对象引用
本文深入探讨了Java中引用数据类型的本质及其相关特性。引用变量存储的是对象的内存地址而非对象本身,类似房子的地址而非房子本身。文章通过实例解析了引用赋值、比较(`==`与`equals()`的区别)以及包装类缓存机制等核心概念。此外,还介绍了Java引用类型的家族,包括类、接口、数组和枚举。理解这些内容有助于开发者避免常见错误,提升对Java内存模型的掌握,为高效编程奠定基础。
79 0
|
1月前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
42 0
|
2月前
|
存储 算法 安全
Java对象创建和访问
Java对象创建过程包括类加载检查、内存分配(指针碰撞或空闲列表)、内存初始化、对象头设置及初始化方法执行。访问方式有句柄和直接指针两种,前者稳定但需额外定位,后者速度快。对象创建涉及并发安全、垃圾回收等机制。
Java对象创建和访问
|
3月前
|
存储 Java
Java中判断一个对象是否是空内容
在 Java 中,不同类型的对象其“空内容”的定义和判断方式各异。对于基本数据类型的包装类,空指对象引用为 null;字符串的空包括 null、长度为 0 或仅含空白字符,可通过 length() 和 trim() 判断;集合类通过 isEmpty() 方法检查是否无元素;数组的空则指引用为 null 或长度为 0。
|
4月前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
4月前
|
安全 Java
Object取值转java对象
通过本文的介绍,我们了解了几种将 `Object`类型转换为Java对象的方法,包括强制类型转换、使用 `instanceof`检查类型和泛型方法等。此外,还探讨了在集合、反射和序列化等常见场景中的应用。掌握这些方法和技巧,有助于编写更健壮和类型安全的Java代码。
290 17

热门文章

最新文章