欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java类初始化和实例化中的2个“雷区”

程序员文章站 2024-03-09 11:33:47
在考虑类初始化时,我们都知道进行子类初始化时,如果父类没有初始化要先初始化子类。然而事情并没有一句话这么简单。 首先看看java中初始化触发的条件: (1)在使用n...

在考虑类初始化时,我们都知道进行子类初始化时,如果父类没有初始化要先初始化子类。然而事情并没有一句话这么简单。
首先看看java中初始化触发的条件:

(1)在使用new实例化对象,访问静态数据和方法时,也就是遇到指令:new,getstatic/putstatic和invokestatic时;
(2)使用反射对类进行调用时;
(3)当初始化一个类时,父类如果没有进行初始化,先触发父类的初始化;
(4)执行入口main方法所在的类;
(5)jdk1.7动态语言支持中方法句柄所在的类,如果没有初始化触发起初始化;

经过编译后生成一个<clinit>方法,类的初始化就在这个方法中进行,该方法只执行,由jvm保证这一点,并进行同步控制;
其中条件(3),从方法调用的角度来看,是子类的<clinit>会在开始时递归的调用父类的<clinit>,这类似与我们在子类构造器中必须首先调用父类的构造器;
但需要注意的是“触发”并不是完成初始化,这意味着有可能子类的初始化会提前于父类初始化结束,这就是“危险”的所在。

1. 一个类初始化的例子:
这个例子我使用一个外围类包含2个有继承关系的静态成员类,因为外围类的初始化和静态成员类没有因果关系,因此这样展示是安全和方便的;
父类a和子类b分别包含main函数,由上面的触发条件(4)可知,通过分别调用这个两个main函数来触发不同的类初始化路径;
这个例子的问题在于父类包含子类的static引用并在定义处进行初始化的问题:

public class wrapperclass { 
  private static class a { 
    static { 
      system.out.println("类a初始化开始..."); 
    } 
    //父类包含子类的static引用 
    private static b b = new b(); 
    protected static int aint = 9; 
 
    static { 
      system.out.println("类a初始化结束..."); 
    } 
 
    public static void main(string[] args) { 
 
    } 
  } 
 
  private static class b extends a { 
    static { 
      system.out.println("类b初始化开始..."); 
    } 
    //子类的域依赖于父类的域 
    private static int bint = 9 + a.aint; 
 
    public b() { 
      //构造器依赖类的static域 
      system.out.println("类b的构造器调用 " + "bint的值" + bint); 
    } 
 
    static { 
      system.out.println("类b初始化结束... " + "aint的值:" + bint); 
    } 
 
    public static void main(string[] args) { 
 
    } 
  } 
} 

情景一:入口为类b的main函数时输出结果:

/** 
   * 类a初始化开始... 
   * 类b的构造器调用 bint的值0 
   * 类a初始化结束... 
   * 类b初始化开始... 
   * 类b初始化结束... aint的值:18 
   */ 

分析:可以看到,main函数的调用触发了类b的初始化,进入类b的<clinit>方法,类a作为其父类先开始初始化进入了a的<clinit>方法,其中有一个语句new b();这时会进行b的实例化,这是已经在类b的<clinit>中了,main线程已经获得锁开始执行类b的<clinit>,我们开头说过jvm会保证一个类的初始化方法只被执行一次,jvm收到new指令后不会再进入类b的<clinit>方法而是直接进行实例化,但是此时类b还没有完成类初始化,所以可以看到bint的值为0(这个0是类加载中准备阶段分配方法区内存后进行的置零初始化);
因此,可以得出,再父类中包含子类类型的static域并进行赋值动作,会可能导致子类实例化在类初始化完成前进行;

情景二:入口为类a的main函数时输出结果:

/** 
   * 类a初始化开始... 
   * 类b初始化开始... 
   * 类b初始化结束... aint的值:9 
   * 类b的构造器调用 bint的值9 
   * 类a初始化结束... 
   */ 

分析:经过情景一的分析,我们知道,由类b的初始化触发类a的初始化,会导致类a中类变量b的实例化在类b初始化完成前进行,那如果先初始化类a是不是就可以在类变量实例化的时候先触发类b的初始化,从而使得初始化在实例化前呢?答案是肯定的,但是这仍然有问题。
根据输出,可以看到,类b的初始化在类a的初始化完成前进行了,这导致了像类变量aint的变量在类b初始化完成后才进行初始化,所以类b中的域bint获取到的aint的值是“0”,而不是我们预期的“18”;

结论:综上,可以得出,在父类中包含子类类型的类变量,并在定义出进行实例化是非常危险的行为,具体情况可能不会向例子一样直白,调用方法在定义处赋值一样隐含着危险,即使要包含子类类型的static域,也应该通过static方法进行赋值,因为jvm可以保证在static方法调用前完成所有的初始化动作(当然这种保证也是你不应该包含static b b = new b();这样的初始化行为);

2. 一个实例化的例子:
首先需要知道对象创建的过程:
(1)遇到new指令,检查类是否完成了加载,验证,准备,解析,初始化(解析过程就是符号引用解析成直接引用,比如方法名就是一个符号引用,可以在初始化完成后使用这个符号引用的时候进行,正是为了支持动态绑定),没有完成先进行这些过程;
(2)分配内存,采用空闲列表或者指针碰撞的方法,并将新分配的内存“置零”,因此所有的实例变量在此环节都进行了一次默认初始化为0(引用为null)的过程;
(3)执行<init>方法,包括检查调用父类的<init>方法(构造器),实例变量定义出的赋值动作,实例化器顺序执行,最后调用构造器中的动作。

这个例子可能更为大家所熟知,也就是它违反了“不要在构造器,clone方法和readobject方法中调用可被覆盖的方法”。其原因就在于java中的多态,也就是动态绑定。
父类a的构造器中包含一个protected方法,类b是其子类。

public class wronginstantiation { 
  private static class a { 
    public a() { 
      dosomething(); 
    } 
 
    protected void dosomething() { 
      system.out.println("a's dosomething"); 
    } 
  } 
 
  private static class b extends a { 
    private int bint = 9; 
 
    @override 
    protected void dosomething() { 
      system.out.println("b's dosomething, bint: " + bint); 
    } 
  } 
 
  public static void main(string[] args) { 
    b b = new b(); 
  } 
} 

输出结果:

/** 
   * b's dosomething, bint: 0 
   */ 

分析:首先需要知道,在没有显示提供构造器时java编译器会生成默认构造器,并在开始处调用父类的构造器,因此类b的构造器开始会先调用类a的构造器。
类a中调用了protected方法dosomething,从输出结果中我们看到实际上调用的是子类的方法实现,而此时子类的实例化还未开始,因此bint并没有如“预期”那样是9,而是0;
这就是由于动态绑定,dosomething是一个protected方法,因此它是通过invokevirtual指令调用的,该指令根据对象实例的类型找到对应的方法实现(这里就是b的实例对象,对应方法就是类b的方法实现)执行,故而有此结果。

结论:正如前面说的“不要在构造器,clone方法和readobject方法中调用可被覆盖的方法”。

以上就是为大家介绍的java类初始化和实例化中的2个“雷区”,希望对大家的学习有所帮助。