一、初始化初识
1. 初始化是什么
Java语言规范中了Java有两种数据类型:简单类型和引用类型。
简单类型包括boolean类型和数字类型,其中数字类型包括整数类型byte、short、int、long和char以及浮点类型float和double。引用类型包括类类型、接口类型和数组类型。
初始化就是对这些不同类型的变量进行初次赋值。
2.为什么要进行初始化
我们通过C语言的代码来说明,看如下代码:
#include <stdio.h>
int main(void)
{
int i;
printf("i = %d\n", i);
return 0;
}
这里我们定义了一个变量i,但是没有对它赋值,也就是没有对它进行初始化。
我们来看一下执行的结果:
i = 41
Process returned 0 (0x0) execution time : 0.010 s
Press any key to continue
说明:在你机子上运行值会不同。
这里输出了一个莫名其妙的值。定义了i则是为i分配了内存空间,这个内存空间可能以前的程序使用过,而数据未清理,所以出现了这个数字。
现在想想如果后面程序拿这个值去做操作的话,就会出现一些莫名其妙的结果。
C语言没有强制让我们做初始化或者是帮我们做些事情,就会引起一些问题,Java语言对初始化做了一些改进来帮助我们解决这样的问题。
Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。
二、如何初始化
Java语言支持的变量的种类有三种:
- 类变量:独立于方法之外的变量,用 static 修饰。
- 实例变量:独立于方法之外的变量,不过没有 static 修饰。
- 局部变量:类的方法中的变量。
我们下面会来介绍这三种变量的初始化方式。
1. 关于默认值
当一个变量作为类的成员时,即使没有进行初始化,Java也确保它获得一个默认值。
简单类型默认值如下图所示:
如果这个类成员变量是引用类型,如果不将其初始化,此引用就会获得一个特殊值null。
这里要注意,上面说的是当变量作为某个类的字段时会确保未初始化有默认值,但是如果变量是局部的,那么就必须显示地进行初始化,如未初始化,Java编译时会以提示错误的方式来告知:
public class InitialExample1 {
public void test() {
int i;
System.out.println(i);
}
}
编译结果:
D:\>javac InitialExample1.java
InitialExample1.java:5: 错误: 可能尚未初始化变量i
System.out.println(i);
^
1 个错误
2. 局部变量初始化
局部变量初始化很简单,就是在定义变量的时候设置值,而且是必须设置。
3. 实例变量初始化
实例变量的初始化方式有下面三种:
3.1 直接初始化
在定义变量的时候直接赋值。
public class InitialExample2 {
private int i = 10;
}
3.2 通过构造器进行初始化
在构造方法中对实例变量进行初始化。
public class InitialExample3 {
private int i;
public InitialExample3() {
i = 10;
}
}
3.3 通过实例初始化块进行初始化
在类中声明的实例初始化块会在类的实例被创建时执行。格式如下:
{
// 这里做初始化
}
实例初始化块允许通过关键词this来引用当前对象,允许使用关键词super,并允许使用任何在作用域中的类型变量。
实例如下:
public class InitialExample4 {
private int i;
// 实例初始化块
{
i = 10;
}
}
实例初始化块执行先与构造方法
public class InitialExample5 {
private int i;
{
i = 10;
}
public InitialExample5() {
i = 11;
}
public static void main(String[] args) {
InitialExample5 i5= new InitialExample5 ();
System.out.println(i5.i);
}
}
输出结果为:
11
多个实例初始块执行顺序与定义顺序一致
public class InitialExample6 {
private int i;
{
i = 10;
}
{
i = 11;
}
{
i = 12;
}
public static void main(String[] args) {
InitialExample6 i6= new InitialExample6 ();
System.out.println(i6.i);
}
}
输出:
12
4. 类变量初始化
4.1 直接初始化
在定义类变量的时候进行初始化:
private static int i = 1;
4.2 通过构造器初始化
虽然说,通过构造器来初始化类变量,不会出现语法上的错误,但是既然定义成类变量了说明不想与具体的类的实例有关,所有这种初始化方法其实没什么意义。
4.3 通过静态块初始化
与实例初始化块类似,类变量可以使用静态块进行初始化,静态块格式如下:
static {
// 执行类变量的初始化
}
示例如下:
public class InitialExample7 {
private static int i;
static {
i = 10;
}
}
三、<init>和<clinit>:JVM角度看初始化
上面我们是从Java语言层面上来看变量的初始化,这个章节从JVM的角度来看一下初始化的相关过程。
1. <clinit>
前面说到了类变量的初始化方法,但是我们没有提到什么时候初始化类变量。
那是什么时候呢?
答案:是在类载入过程的最后一个阶段也就是类初始化阶段进行。初始化(Initialization)对于类或接口来说,就是执行它的初始化方法,也就是<clinit>方法,这个方法是编译器为我们添加的,被JVM自身隐式调用,没有任何虚拟机字节码指令可以调用这个方法,只有在类的初始化阶段中被虚拟机自身调用。
1.1 <clinit>的产生
<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
编译器收集的顺序是由语句在源文件中出现顺序决定的。
1.2 第一个被JVM执行的<clinit>方法的类肯定是java.lang.Object
<clinit>方法与类的构造函数(或者说实例构造器<init>方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>方法执行之前,父类的<clinit>方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>方法的类肯定是java.lang.Object。
1.3 父类静态语句块优先于子类的变量赋值操作
由于父类的<clinit>方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
public class InitialExample8 {
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
程序输出结果为:
2
1.4 接口与<clinit>
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成<clinit>方法。但接口和类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。
1.5 多线程与<clinit>
虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>方法完毕。
同时需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>方法的哪条线程退出<clinit>方法后,其他线程唤醒之后不会再次进入<clinit>方法。同一类加载器下,一个类型只会被初始化一次。
2. <init>
上面介绍了实例变量初始化的方法,不过也是没有说什么时候对实例变量进行初始化。
那么是什么时候呢?
答案:类的载入完成之后(加载、链接和初始化),接下来虚拟机为新生的对象分配内存,内存分配完成之后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),接下来虚拟机对对象进行必要的设置。
零值与具体数据类型相对应。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚刚开始。接下来调用指令执行实例初始化方法(<init>)方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
实例变量的初始化工作就在<init>方法中完成。
<init>方法是编译器产生的,只能在实例的初始化期间,通过Java虚拟机的invokespecial指令来调用。
2.1 构造方法与<init>
构造方法是Java语言程序层面上实例初始化的体现,而<init>是字节码层面上对实例初始化的体现。对于类中的每一个构造方法,编译器都会生成一个与之对应的<init>方法。如果类中没有声明任何构造方法,编译器也会生成一个<init>方法作为它的无参构造方法的体现(这也就是为什么类中没有定义任何的构造方法,也可以通过new 类名()这样的方式创建对象的原因)。
2.2 <init>中的内容
在描述<clinit>产生的时候,我们说到过:
由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
那么<init>是不是也是编译器自动收集实例变量的赋值和实例初始化块中的语句合并产生的呢。
大体上可以认为类似,不过还是有些区别,相比之下会复杂些。
概括来说,一个<init>方法中可能包含三种代码:
- 调用另一个<init>方法
- 实现对任何实例变量的初始化(直接赋值和实例初始化块)
- 构造方法体的代码
通过一个示例来看一下<init>方法中的内容:
public class Init1 {
private String name = "Rocky1";
{
name = "Rocky2";
}
public Init1(){
name = "Rocky3";
}
public Init1(String name) {
this.name = name;
}
}
通过javap查看字节码,字节码如下:
{
public Init1();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Rocky1
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: ldc #4 // String Rocky2
13: putfield #3 // Field name:Ljava/lang/String;
16: aload_0
17: ldc #5 // String Rocky3
19: putfield #3 // Field name:Ljava/lang/String;
22: return
LineNumberTable:
line 9: 0
line 3: 4
line 6: 10
line 10: 16
line 11: 22
public Init1(java.lang.String);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Rocky1
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: ldc #4 // String Rocky2
13: putfield #3 // Field name:Ljava/lang/String;
16: aload_0
17: aload_1
18: putfield #3 // Field name:Ljava/lang/String;
21: return
LineNumberTable:
line 13: 0
line 3: 4
line 6: 10
line 14: 16
line 15: 21
}
内容说明如下图红色标记:
调用另一个<init>方法
这个描述中的"另一个"指的是哪一个呢?无非就是两种:当前类的另一个和父类中的某一个。
这个"调用"也有默认调用和明确调用之说。
明确调用:我们知道在构造方法中可以使用this调用当前类的另一个构造方法,或者使用super调用父类的某个构造方法,这个是我们做的一个明确调用。当我们在构造方法中使用了this或者super关键词,那么编译器就会处理成调用当前类的另外一个<init>方法或者调用父类的某个<init>方法。
默认调用:默认调用就是我们在源代码中没有明确得指名调用其他构造方法,但是编译器为我们做了处理,在我们从源码无法看到的情况下插入了其他<init>的调用。比如,如果当前类不是java.lang.Object类(并且没有明确得指明父类),而任何类都是java.lang.Object的子类,所以编译后的字节码就会为我们插入Object类的<init>方法的调用。这样执行子类实例初始化之前,保证父类中的变量的初始化提前完成。
默认调用父类的无参<init>方法
如果构造方法没有明确地从this或者super调用开始,对应的<init>方法默认会调用父类的无参数<init>方法。
注意java.lang.Object没有超类,所以上面所说不适用。
实例一:
public class Init1 {
private String name;
public Init1(String name) {
this.name = name;
}
}
javap查看字节码:
{
public Init1(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field name:Ljava/lang/String;
9: return
LineNumberTable:
line 5: 0
line 6: 4
line 7: 9
}
可以看到调用了父类java.lang.Object的<init>方法。
实例二:
public class Init2Parent {
}
public class Init2 extends Init2Parent {
private String name;
public Init2(String name) {
this.name = name;
}
}
javap查看Init2类的字节码:
{
public Init2(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method Init2Parent."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field name:Ljava/lang/String;
9: return
LineNumberTable:
line 5: 0
line 6: 4
line 7: 9
}
可以看到这里调用了父类Init2Parent的<init>方法。
使用this调用当前类的其他构造方法
构造方法中有this方法调用,则说明是在该构造方法中调用了本类中的另一个构造方法。则该构造方法对应的<init>方法就包含两部分内容:
- 一个同类的<init>方法的调用
- 实现了对应构造方法的方法体的字节码
public class Init2 {
private String name = "Rocky1";
{
name = "Rocky2";
}
public Init2(){
name = "Rocky3";
}
public Init2(String name) {
this();
this.name = name;
}
}
使用javap查看生成的字节码,如下:
{
public Init2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Rocky1
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: ldc #4 // String Rocky2
13: putfield #3 // Field name:Ljava/lang/String;
16: aload_0
17: ldc #5 // String Rocky3
19: putfield #3 // Field name:Ljava/lang/String;
22: return
LineNumberTable:
line 9: 0
line 3: 4
line 6: 10
line 10: 16
line 11: 22
public Init2(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #6 // Method "<init>":()V
4: aload_0
5: aload_1
6: putfield #3 // Field name:Ljava/lang/String;
9: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 9
}
SourceFile: "Init2.java"
这里
1: invokespecial #6 // Method "<init>":()V
可以看到调用了本类中的另一个<init>方法
使用super调用父类的构造方法
如果构造方法通过明确地调用父类的构造方法(一个super调用)开始,它的<init>方法会调用对应的父类的<init>方法。
public class Init3Parent {
private String name;
public Init3Parent(String name) {
this.name = name;
}
}
public class Init3 extends Init3Parent {
public Init3(String name) {
super(name);
}
}
使用javap查看Init3类的字节码:
{
public Init3(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #1 // Method Init3Parent."<init>":(Ljava/lang/String;)V
5: return
LineNumberTable:
line 4: 0
line 5: 5
}
这里:
2: invokespecial #1 // Method Init3Parent."<init>":(Ljava/lang/String;)V
调用了父类指定的<init>方法。
四、继承与初始化
上面的章节中我们已经提到过继承关系中涉及类初始化和实例初始化的问题,这一小节我们单独提出来做一下巩固说明。
1. 继承与类初始化
先来看一下下面的一段代码:
class A {
static int super_var = 1;
static {
System.out.println("super");
}
}
class B extends A {
static int sub_var = 2;
static {
System.out.println("sub");
}
}
访问子类中定义的静态数据
public class Demo{
public static void main(String []args) {
System.out.println(B.sub_var);
}
}
输出结果:
rockydeMacBook-Pro:Desktop rocky$ java Demo
super
sub
2
分析:
访问B.sub_var,JVM会对B类进行载入,执行载入的一系列过程(加载、链接、初始化)。
JVM有如下规定:如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
所以访问子类B的静态变量会导致父类先进行初始化,输出就为上面的结果。
访问父类定义的静态数据
public class Demo {
public static void main(String []args){
System.out.println(B.super_var);
}
}
和上面的Demo类不同的是,这里直接通过子类来访问父类中定义的静态字段。
输出结果如下:
super
1
从结果可以看到,通过子类直接访问父类的静态字段,子类并没有执行初始化。
那么,这是为什么呢?
Java语言规范作了如下的说明:
A reference to a
static
field causes initialization of only the class or interface that actually declares it, even though it might be referred to through the name of a subclass, a subinterface, or a class that implements an interface.
这个说明很清晰了:对static域的引用只会导致实际声明它的类或接口被初始化,即使可能是通过子类名、子类接口名或实现了某个接口的类名而被引用,也是如此。
2. 继承与实例初始化
先来看一下下面的一段代码:
class A {
public A() {
System.out.println("super");
}
}
class B extends A {
public B() {
System.out.println("sub");
}
}
public class Demo{
public static void main(String []args) {
new B();
}
}
输出结果如下:
rockydeMacBook-Pro:Desktop rocky$ java Demo
super
sub
对于这个输出结果应该不会很惊讶吧!? 上面写<init>的章节已经说了,虽然在子类的构造方法中没有显示得调用父类的构造方法,但是编译器会在子类的<init>方法中默认插入父类的无参<init>方法。
分析解决上面的代码输出很简单,这里我们要说的是一个题外化:我们创建父对象了吗?
创建了父对象吗?
从上面我们可以看到,创建子类对象的时候会调用父类的构造方法,但是这是不是意味着父类对象被创建了吗?
答案:没有创建父对象。
虽然父类中的构造方法被调用了,但是并不意味着我们就创建了一个父对象。
通过下面的描述就可以论证我们的观点:我们可以继承一个抽象类,然后创建子类的对象,父类的构造方法同样会被调用,但是父类是一个抽象的类它根本就没法被实例化,何来创建父类对象之说。