Java类的初始化与实例对象的初始化
Java对象初始化详解
来源:MySun
在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编译器会为我们自动生成一个构造函数。构造函数与类中定义的其他方法基本一样,除了构造函数没有返回值,名字与类名一样之外。在生成的字节码中,这些构造函数会被命名成<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的默认值1,j的值是0。但是,由于静态方法是不能被覆写的,因此第2节中关于构造函数调用被覆写方法引起的问题不会在此出现。
Java类的初始化与实例对象的初始化的更多相关文章
- java 类的加载,链接,初始化
本篇的话题,讨论Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码 ...
- 【Java基础】Java类的加载和对象创建流程的详细分析
相信我们在面试Java的时候总会有一些公司要做笔试题目的,而Java类的加载和对象创建流程的知识点也是常见的题目之一.接下来通过实例详细的分析一下. 实例问题 实例代码 Parent类 package ...
- JAVA类的加载、连接与初始化
JAVA类的加载.连接与初始化 类的声明周期总共分为5个步骤1.加载2.连接3.初始化4.使用5.卸载 当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载.连接和初始化,而连接这个类 ...
- Java类的加载和对象创建流程的详细分析
相信我们在面试Java的时候总会有一些公司要做笔试题目的,而Java类的加载和对象创建流程的知识点也是常见的题目之一.接下来通过实例详细的分析一下: package com.test; public ...
- Java类的加载、链接和初始化
一.Java的类加载机制回顾与总结: 我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象.这样 ...
- java类到底是如何加载并初始化的?
Java虚拟机如何把编译好的.class文件加载到虚拟机里面?加载之后如何初始化类?静态类变量和实例类变量的初始化过程是否相同,分别是如何初始化的呢?这篇文章就 是解决上面3个问题的. 若有不正之处, ...
- java类(Class)的概念;对象的概念,声明类的属性 和方法,局部变量和成员变量,面向对象编程思维,抽象的概念
类(Class)的概念 类是对一组具有相同特征和行为的对象的抽象描述. 理解: [1] 类包含了两个要素:特性和行为 => 同一类事物具有相同的特征和行为. [2] 类是一个群体性概念.例如:网 ...
- (转)java类到底是如何加载并初始化的?
Java虚拟机如何把编译好的.class文件加载到虚拟机里面?加载之后如何初始化类?静态类变量和实例类变量的初始化过程是否相同,分别是如何初始化的呢?这篇文章就 是解决上面3个问题的. 若有不正之处, ...
- java 类的加载、连接和初始化
JVM和类 调用Java命令运行Java程序时,该命令将会启动一条Java虚拟机进程,不管该Java程序启动了多少条线程,创建了多少个变量,它们都处于该Java虚拟机进程里,共享该JVM进程的内存区. ...
随机推荐
- zookeeper的c API 单线程与多线程问题 cli_st和cli_mt
同样的程序,在centos和ubuntu上都没有问题,在solaris上问题却多多,据说是solaris管理更加严格. zookeeper_init方法,在传入一个错误的host也能初始化出一个非空的 ...
- (转)JAVA正则表达式语法大全
[正则表达式]文本框输入内容控制 整数或者小数:^[0-9]+\.{0,1}[0-9]{0,2}$ 只能输入数字:"^[0-9]*$". 只能输入n位的数字:"^\d{n ...
- React中利用axios来实现数据请求
axios是基于Promise来封装的,通常我们会用axios在数据请求这块作如下配置: 一.拦截器 有注释,不难理解,通常请求头参数不是写死的,应该是去浏览器中读的,例如,login之后返回toke ...
- MPAndroidChart Wiki(译文)~Part 5
19. ChartData子类 这篇wiki主要关注ChartData子类的具体介绍.至于此部分没有提及到的ChartData的子类,代表他们没有特性功能需要介绍. BarData 方法 使用 set ...
- 评价指标的计算:accuracy、precision、recall、F1-score等
记正样本为P,负样本为N,下表比较完整地总结了准确率accuracy.精度precision.召回率recall.F1-score等评价指标的计算方式: (右键点击在新页面打开,可查看清晰图像) 简单 ...
- wampserver搭建本地服务器
打开..\wamp\bin\apache\apache2.4.9\conf\httpd.conf配置文件, <Directory "c:/wamp/www/"> # # ...
- learn Linux sed command
learn Linux sed command 一.参考文档: . sed命令详解 http://qifuguang.me/2015/09/21/sed%E5%91%BD%E4%BB%A4%E8%AF ...
- Spring之基本关键策略
目的 为了简化Java开发. 策略 基于POJO(普通Java类)的轻量级和最小侵入性编程: 通过依赖注入(DI)和面向接口实现松耦合: 基于切面和惯例进行声明式编程: 通过切面和模板减少样板式代码. ...
- JSON简介[转]
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式. 易于人阅读和编写.同时也易于机器解析和生成. 它基于JavaScript Programming Lan ...
- threejs Object的点击(鼠标)事件(获取点击事件的object)
objects=[]; raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); //监听全局点击事件,通过ray检测选中哪一个o ...