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

在考虑类初始化时,我们都知道进行子类初始化时,如果父类没有初始化要先初始化子类。然而事情并没有一句话这么简单。
首先看看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个“雷区”,希望对大家的学习有所帮助。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索Java类初始化
Java类实例化
java实例化和初始化、java 实例初始化、java匿名类实例初始化、u8初始化数据库实例、实例化和初始化,以便于您获取更多的相关知识。

时间: 2024-09-19 04:36:43

Java类初始化和实例化中的2个“雷区”_java的相关文章

关于java类初始化顺序的问题

问题描述 关于java类初始化顺序的问题 正常来说一个类的初始化过程应该是: 1.全局静态变量 2.静态代码块 3.全局变量 4.代码块 5.构造器 有这么一个例子: public class LoadTest { //全局静态变量 static int staticI = 10; //全局变量 int i = 20; //构造器 private LoadTest() { System.out.println("staticI="+staticI); System.out.printl

java web-Javaweb在Java类的main方法中无法通过Proxool连接数据库

问题描述 Javaweb在Java类的main方法中无法通过Proxool连接数据库 Javaweb在Java类的main方法中无法通过Proxool连接数据库,报SQLException,找不到驱动,同样的代码启动tomcat后在服务器中能运行??????? 解决方案 有两个可能,一个是jar包没有引入全:还有一个就是配置文件的编码方式,把配置文件打开看看有没有乱码

java类的问题-android中的Context是所有类的父类吗,那么Object是Context的父类吗

问题描述 android中的Context是所有类的父类吗,那么Object是Context的父类吗 android中的Context是所有类的父类吗,那么Object是Context的父类吗,Context类有什么方法和属性 解决方案 关联到源码,点击进去看源码啊 解决方案二: android Context类android Context类android 学习一: Context 类 解决方案三: android中的Context是一个接口,它提供了一组通用的API,开发中使用的Contex

Java类初始化问题

问题描述 需要在一个单例模式类里初始化另外一个类,只是让另外一个类初始化,不做任何操作,请问用new方式而不赋给另外一个值,会不会造成单例模式类永久持有新new的类?大家只需要初始化一个类而不用操作都是怎么做的呢?谢谢啦!public class TestA { private static Test instance; private TestA { [color=red]new TestB(); //这里[/color] } public static TestA getInstance()

java使用Hashtable过滤数组中重复值的方法_java

本文实例讲述了java使用Hashtable过滤数组中重复值的方法.分享给大家供大家参考,具体如下: package org.eline.core.web.support; import java.util.Hashtable; /***************************** * * @author zdw * */ public class ObjectFilter { public static void main(String[] args) { // String 测试 S

java获取两个数组中不同数据的方法_java

本文实例讲述了java获取两个数组中不同数据的方法.分享给大家供大家参考.具体实现方法如下: public static <T> List<T> compare(T[] t1, T[] t2) { List<T> list1 = Arrays.asList(t1); List<T> list2 = new ArrayList<T>(); for (T t : t2) { if (!list1.contains(t)) { list2.add(t)

获取Java的MyBatis框架项目中的SqlSession的方法_java

从XML中构建SqlSessionFactory从XML文件中构建SqlSessionFactory的实例非常简单.这里建议你使用类路径下的资源文件来配置. String resource = "org/mybatis/example/Configuration.xml"; Reader reader = Resources.getResourceAsReader(resource); sqlMapper = new SqlSessionFactoryBuilder().build(r

在JavaScript中调用Java类和接口的方法_javascript技巧

前言 本文中所有的代码使用 JavaScript 编写,但你也可以用其他兼容 JSR 223 的脚本语言.这些例子可作为脚本文件也可以在交互式 Shell 中一次运行一个语句的方式来运行.在 JavaScript 中访问对象的属性和方法的语法与 Java 语言相同. 本文包含如下几部分: 1.访问 Java 类 为了在 JavaScript 中访问原生类型或者引用 Java 类型,可以调用 Java.type() 函数,该函数根据传入的完整类名返回对应对象的类型.下面代码显示如何获取不同的对象类

解析Java类和对象的初始化过程

本文主要对类和对象初始化全过程进行分析,通过一个实际问题引入,将源代码转换成 JVM 字节码后,对 JVM 执行过程的关键点进行全面解析,并在文中穿插入了相关 JVM 规范和 JVM 的部分内部理论知识,以理论与实际结合的方式介绍对象初始化和类初始化之间的协作以及可能存在的冲突问题. 问题引入 近日我在调试一个枚举类型的解析器程序,该解析器是将数据库内一万多条枚举代码装载到缓存中,为了实现快速定位枚举代码和具体枚举类别的所有枚举元素,该类在装载枚举代码的同时对其采取两种策略建立内存索引.由于该类