JAVA笔记-类的初始化及对象的初始化

时间:2022-02-20 17:25:34


类的初始化及对象初始化

初学Java,觉得类的初始化与对象的初始化这一块真的特别重要,翻了很多大神前辈的整理资料,还是有些懵懂,决定将资料整理下,也希望对后来的初学者有些许帮助。

 

看到 <clinit> 方法时,有些懵懂,翻阅资料后,最后的结论为下面这句话:(疑问:1:没有<clinit>方法的类,是不是意味着该类没有被初始化???)

在编译生成class文件时,会自动产生两个方法,一个是类或接口的初始化方法<clinit>,另一个是实例(对象)的初始化方法<init>

当类被第一次使用的时候会被初始化,而且只会被一个线程初始化一次。

<clinit>()方法的特点

 1)在执行子类的<clinit>()方法时会保证父类的该方法已经执行完毕。

2<clinit>()方法如果没有静态语句块(类变量)那么编译器不会为该类执行<clinit>()方法。

3)接口中无静态语句块但是有赋值操作(常量),因此接口也会有<clinit>()方法,但是接口的<clinit>()方法只有当子类和实现类中定义的变量有使用时(使用子类前会先初始化其父类)才会执行<clinit>()方法。

4<clinit>()方法的线程安全。



类初始化

 

类"初始化"阶段,它是一个类或接口被首次使用的前阶段中的最后一项工作,本阶段负责为类变量赋予正确的初始值。

 

简单来说,就是当类被第一次使用的时候会被初始化,而且只会被一个线程初始化一次。我们可以通过静态初始化器和静态变量初始化器来完成对类变量的初始化工作,比如,

 

(疑问2:是否可理解为类的初始化是static的初始化?)

public class Static Initializer {

static int i = 1;

 

static{

i=2;

}

}

 

静态变量初始化器和静态初始化器基本同实例变量初始化器和实例初始化器相同,也有相同的限制(按照编码顺序被执行,不能引用后定义和初始化的类变量)。静态变量初始化器和静态初始化器中的代码会被编译器放到一个名为static的方法中(static是Java语言的关键字,因此不能被用作方法名,但是JVM却没有这个限制),在类被第一次使用时,这个static方法就会被执行。

 

通过特殊的方式来使用未经初始化的实例变量,对于类变量也同样适用,比如,

 

publi class StaticInitializer {

static int j = getI();

static int i = 1;

 

static int getI () {

return i;

}

 

publicstatic void main(String[]args){

System.out.println(StaticInitializer.j);

}

}

 

上面这段代码的打印结果是0,类变量的值是i的默认值0。但是,由于静态方法是不能被覆写的,关于构造函数调用被覆写方法引起的问题不会在此出现。

 

Java 编译器把所有的类变量初始化语句和类型的静态初始化器通通收集到 <clinit> 方法内,该方法只能被 Jvm 调用,专门承担初始化工作。

 

除接口以外,初始化一个类之前必须保证其直接超类已被初始化,并且该初始化过程是由 Jvm 保证线程安全的。另外,并非所有的类都会拥有一个 <clinit>() 方法,在以下条件中该类不会拥有 <clinit>() 方法:

  • 该类既没有声明任何类变量,也没有静态初始化语句;
  • 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
  • 该类仅包含静态 final 变量(即常量)的类变量初始化语句,并且类变量初始化语句是编译时常量表达式。(关于static final修饰的变量,在系统中为宏变量,在编译的时候就已经分配好内存和值,是系统常量,在其他使用的地方都是直接替换的。反编译代码可以看到所有的static final变量(常量)都变成了常数。所以不论你什么时候初始化类,都是直接替换相减)
  • final的属性

1.     final 修饰对象的属性,这个属性不能自动初始化,必须手动初始化。

o   可以直接赋值初始化

o   也可以使用构造器进行初始化

2.     final属性初始化以后就不能再改变了。

3.     final 修饰的属性,仍然是实例变量,还是每个都有一个的属性。

注意:static 修饰的成员变量,是属于类的变量,是全体共享的同一个变量,与final属性不同。

 


类的初始化时机

 

Java 虚拟机规范为类的初始化时机做了严格定义:"initialize on first activeuse"--" 在首次主动使用时初始化"。这个规则直接影响着类装载、连接和初始化类的机制--因为在类型被初始化之前它必须已经被连接,然而在连接之前又必须保证它已经被装载了。

 

在与初始化时机相关的类装载时机问题上,Java 虚拟机规范并没有对其做严格的定义,这就使得 JVM 在实现上可以根据自己的特点提供采用不同的装载策略。

首次主动使用的情形:

  • 创建某个类的新实例时--new、反射、克隆或反序列化;
  • 调用某个类的静态方法时;
  • 使用某个类或接口的静态字段或对该字段赋值时(final字段除外);
  • 调用Java的某些反射方法时
  • 初始化某个类的子类时
  • 在虚拟机启动时某个含有main()方法的那个启动类。

除了以上几种情形以外,所有其它使用JAVA类型的方式都是被动使用的,他们不会导致类的初始化。

 

对象初始化

 

类的初始化在对象初始化之前。
在对象初始化之前,类的初始化一定是完成了的,即类的初始化永远在对象的初始化之前。

在类被装载、连接和初始化,这个类就随时都可能使用了。对象实例化和初始化是就是对象生命的起始阶段的活动,在这里我们主要讨论对象的初始化工作的相关特点。

 

Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "<init>()" 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 "<init>()" 方法.

 

通常来说,<init>() 方法内包括的代码内容大概为:调用另一个 <init>() 方法;对实例变量初始化;与其对应的构造方法内的代码。

 

如果构造方法是明确地从调用同一个类中的另一个构造方法开始,那它对应的 <init>() 方法体内包括的内容为:一个对本类的 <init>() 方法的调用;对应用构造方法内的所有字节码。

 

如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是 Object 对象,那 <init>() 法内则包括的内容为:一个对父类 <init>() 方法的调用;对实例变量初始化方法的字节码;最后是对应构造子的方法体字节码。

 

如果这个类是 Object,那么它的 <init>() 方法则不包括对父类 <init>() 方法的调用。

 

2.1. Java的构造函数

 对象的创建时都会调用构造函数,反射创建对象也调用时构造函数。

每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么Java编译器会为我们自动生成一个构造函数。构造函数与类中定义的其他方法基本一样,除了构造函数没有返回值,名字与类名一样之外。在生成的字节码中,这些构造函数会被命名成<init>方法,参数列表与Java语言书写的构造函数的参数列表相同(<init>这样的方法名在Java语言中是非法的,但是对于JVM来说,是合法的)。另外,构造函数也可以被重载。

 

Java要求一个对象被初始化之前,其超类也必须被初始化,这一点是在构造函数中保证的。Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们即没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用指令,比如,

 

public class ConstructorExample {

 

}

 

对于上面代码中定义的类,如果观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下,

 

 aload_0

  invokespecial #8;//Methodjava/lang/Object."<init>":()V

  return

 

上面代码的第二行就是调用Object对象的默认构造函数的指令。

 

正因为如此,如果我们显式调用超类的构造函数,那么调用指令必须放在构造函数所有代码的最前面,是构造函数的第一条指令。这么做才可以保证一个对象在初始化之前其所有的超类都被初始化完成。

 

如果我们在一个构造函数中调用另外一个构造函数,如下所示,

 

public class ConstructorExample {

private int i;

 

ConstructorExample(){

this(1);

....

}

 

ConstructorExample(inti){

....

this.i= i;

....

}

}

 

对于这种情况,Java只允许在ConstructorExample(int i)内出现调用超类的构造函数,也就是说,下面的代码编译是无法通过的,

 

public class ConstructorExample {

private int i;

 

ConstructorExample(){

super();

this(1);

....

}

 

ConstructorExample(inti){

....

this.i= i;

....

}

}

 

或者,

 

publicclass ConstructorExample {

privateint i;

 

ConstructorExample(){

this(1);

super();

....

}

 

ConstructorExample(inti){

....

this.i= i;

....

}

}

 

Java对构造函数作出这种限制,目的是为了要保证一个类中的实例变量在被使用之前已经被正确地初始化,不会导致程序执行过程中的错误。但是,与C或者C++不同,Java执行构造函数的过程与执行其他方法并没有什么区别,因此,如果我们不小心,有可能会导致在对象的构建过程中使用了没有被正确初始化的实例变量,如下所示,

 

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

protectedint 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. 实例变量初始化器与实例初始化器

 

我们可以在定义实例变量的同时,对实例变量进行赋值,赋值语句就时实例变量初始化器了,比如,

 

public class InstanceVariableInitializer {

private int i = 1;

private int j = i + 1;

}

 

如果我们以这种方式为实例变量赋值,那么在构造函数执行之前会先完成这些初始化操作。

 

我们还可以通过实例初始化器来执行对象的初始化操作,比如,

 

public class InstanceInitializer {

 

private int i = 1;

private int j;

 

{

j= 2;

}

}

 

上面代码中花括号内代码,在Java中就被称作实例初始化器,其中的代码同样会先于构造函数被执行。

 

如果我们定义了实例变量初始化器与实例初始化器,那么编译器会将其中的代码放到类的构造函数中去,这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。我们来看下下面这段Java代码被编译之后的字节码,Java代码如下,

 

public class InstanceInitializer {

 

private nt i = 1;

private int j;

 

{

j= 2;

}

 

public InstanceInitializer(){

i= 3;

j= 4;

}

}

 

编译之后的字节码如下,

 

 aload_0

  invokespecial #11;//Methodjava/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是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例初始化器或者实例变量初始化器使用在其后被定义和初始化的实例变量,比如,

 

public class InstanceInitializer {

{

j= i;

}

 

private int i = 1;

private int j;

}

 

public class InstanceInitializer {

private int j = i;

private int i = 1;

}

 

上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做,是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如,

 

public class InstanceInitializer {

private int j = getI();

private int i = 1;

 

public InstanceInitializer(){

i= 2;

}

 

privateint getI(){

return i;

}

 

publicstatic void main(String[]args){

InstanceInitializeri i = new InstanceInitializer();

System.out.println(ii.j);

}

}

 

如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,而不是经过实例变量初始化器和构造函数初始化之后的值。

 

引用

 

一个实例变量在对象初始化的过程中会被赋值几次?

 

在本文的前面部分,我们提到过,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。

 

如果我们在实例变量初始化器中对某个实例x变量做了初始化操作,那么这个时候,这个实例变量就被第二次赋值了。

 

如果我们在实例初始化器中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。

 

如果我们在类的构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。

 

也就是说,一个实例变量,在Java的对象初始化过程中,最多可以被初始化4次。

 

 


总结:

类的初始化在对象初始化之前。

在对象初始化之前,类的初始化一定是完成了的,即类的初始化永远在对象的初始化之前。

 

一:类的初始化在何时发生
类的初始化时机:“在首次主动使用时”
首次主动使用的情形如下:
1.在虚拟机启动时,某个含有main()方法的那个启动类。
2.调用某个类的静态方法时。
3.使用某个类或接口的静态字段或对该字段赋值时(final字段除外)。
4.初始化某个类的子类为时。
5.调用Java的某些反射方法时。
除了以上几种情况以外,所有其他使用Java类型方式都是被动使用的,它们不会导致类的初始化。


对象的初始化:
初始化时机:Java对象在其被创建时初始化。
初始化顺序:
1.分配内存,为实例变量赋予默认值(无法避免)。
2.实例变量,实例块初始化(初始化顺序与先后出现的顺序相关);
3.构造器初始化。


继承时:
1.先执行父类static,再执行子类static(类的初始化)
2.执行父类实例变量、实例块初始化,再初始化构造器。
3.执行子类实例变量、实例块初始块,再初始化构造器。


源代码如下:
package Test02;


class Super {
{
System.out.println("父类实例变量初始化");
}
static {
System.out.println("父类静态块初始化");
}


public Super() {


System.out.println("父类构造器初始化");
}
}
public class Sub extends Super {
{
System.out.println("子类实例变量初始化");
}
static {
System.out.println("子类静态块初始化");
}


public Sub() {
System.out.println("子类构造器初始化");
}

public static void main(String[] args) {
System.out.println("main");
Sub s;
System.out.println("-先执行声明-");
s = new Sub();
}
}


输出结果:
父类静态块初始化
子类静态块初始化
main
-先执行声明-
父类实例变量初始化
父类构造器初始化
子类实例变量初始化
子类构造器初始化


执行过程分析:
1.访问Sub.main()(这是程序入口,是static方法),于是开始加载Sub类,但发现该类有基类(父类),于是加载父类Super(如果上面还有父类,会继续加载,直到object类止)(从下往上一层一层加载);
2.类全部加载完成后,执行根类(object)的static初始化(返回结果为空,至少可以确定没有类似我们测试用的类似输出语句),然后执行Super的static初始化输出“父类静态块初始化”,接着执行子类static初始化,输出“子类静态块初始化”(从上往下一层一层执行)。
这个顺序保证了,派生类(子类)的static初始化有可能要依赖父类成员的能够正确初始化。
3.之后开始执行main()方法体,执行第一个输出语句“main”,再声明变量,执行输出语句“先执行声明”,执行newSub()创建对象,找到子类构造器,执行第一行的super(),
调用父类Super的构造器(因为Super也是object的子类),之后执行到object类,(返回结果为空,至少可以确定没有类似我们测试用的类似输出语句),执行后返回父类Super。
4.执行父类Super实例变量、实例块初始化,再初始化构造器。执行完毕后,返回到子类。 依次输出“父类实例变量初始化”“父类构造器初始化”
5.执行子类sub实例变量、实例块初始块,最后初始化构造器。依次输出:“子类实例变量初始化”“子类构造器初始化”



另一段代码:需要继续研究下。

学习类变量初始化时机时,发现一个问题。代码如下:

class Price {

               //类成员INSTANCEPrice实例

               static Price INSTANCE = new Price(2.8);

               //默认价格initPrice

               static double initPrice = 20;

               //当前价格

               double currentPrice;

               //构造函数

               public Price(double discount){

                                              System.out.println("执行了构造函数 initPrice:"+ initPrice);

                                              currentPrice = initPrice - discount;

               }

}

public class PriceTest {

               public static void main(String[] args) {

                                   System.out.println(Price.INSTANCE.currentPrice);

                                   Price p = new Price(2.8);

                                   System.out.println(p.currentPrice);

        }

}

 

执行如果如下:

执行了构造函数 initPrice:0.0

-2.8

执行了构造函数 initPrice:20.0

17.2

 

当我在Price类中把initPricefinal修饰,即定义为:final static double initPrice = 20; 其它不变,执行的结果如下:

执行了构造函数 initPrice:20.0

17.2

执行了构造函数 initPrice:20.0

17.2

 

我想问一下,当类变量被final,static修饰时,类变量的初始化工作是怎样的呢?

2013-07-04 10:54

 

这段是疯狂java里面的吧。

初始化一个类分两步:

1.初始化内存空间,第一次初始化类的时候先分配内存空间,此刻INSTANCEnullinitPrice0currentPrice0.0

2.初始化代码,按顺序执行,先初始化INSTANCE实例,得到的值就是currentPrice-2.8

 

关于final修饰的变量,在系统中为宏变量,在编译的时候就已经分配好内存和值,是系统常量,在其他使用的地方都是直接替换的。反编译代码可以看到所有的final变量都变成了常数。所以不论你什么时候初始化类,都是直接替换相减