• 对象实例化的顺序


    对象实例化的顺序

    分类:java基础日期:2012-11-08作者:ticmy

     

    创建一个对象大概有以下几种方式:
    1、通过new关键字,如new Object();
    2、通过某些反射类的newInstance方法,如Class#newInstance、Constructor#newInstance;
    3、如果对象是Cloneable的,通过clone方法;
    4、通过ObjectInputStream#readObject反序列化;
    以上是通过java程序可以创建出对象的方式,jvm中还有一些隐式创建对象的地方,譬如:
    1、启动一个类,main方法的参数String数组是隐式创建的,如果指定了一个或多个String对象,还要创建这些String对象;
    2、读入一个class二进制数据的时候,创建一个与之对应的java.lang.Class类的对象;
    3、在使用“+”进行字符串变量连接时,可能会创建StringBuffer/StringBuilder对象;
    如此等等。

    那么,在程序中通过new或newInstance创建对象(后面说创建对象均指这两种方式)的时候,构造方法、实例变量、父类构造方法、父类实例变量等的执行顺序是怎样的?

    在创建对象的时候,首先会分配内存,此时所有实例变量均为默认值,然后做初始化实例变量、构造方法调用等操作。对于类变量,在创建对象之前,加载类的时候已经做掉了,这里为避免干扰,忽略掉。

    先来一个例子:

    public class Init {
        public static void main(String[] args) throws Exception {
            S s = new S();
            System.out.println(s.getV2());
        }
    }
     
    class P {
        private int v1 = 5;
        private int v2 = getV1();
        public P() throws Exception {
            System.out.println("P");
        }
         
        public int getV1() {
            return v1;
        }
        public int getV2() {
            return v2;
        }
    }
     
    class S extends P {
        private int value1 = 4;
         
        public int getV1() {
            return value1;
        }
         
        public S() throws Exception {
            this("S()");
        }
         
        public S(String msg) throws Exception {
            System.out.println(msg);
        }
         
        public S(int v) throws Exception {
            super();
            System.out.println("abc");
        }
    }

    执行结果如下:

    P
    S()
    0
    

    调用s.getV2()的值为0,是为什么呢,内部的机制是怎样的?先来了解点其它内容。
    在编译代码的时候,会为每个构造方法生成一个对应的方法,方法名叫<init>。但并不是直接将构造方法体作为<init>方法的内容,它有这样的规则:

    如果构造方法中的第一条语句是通过this调用本类的其它构造方法,如类S的第一个构造方法,其完整的构造方法体就是对应的<init>方法的方法体。编译器不会为其添加一个super调用了。

    如果构造方法中的第一条语句不是通过this调用本类的其它构造方法,会按以下内容与顺序组成<init>方法体:
    1、超类<init>方法的调用。如果是显式的调用了超类构造方法,将会使用对应的超类<init>方法,如果没有写,编译器会生成一个超类无参<init>方法的调用;
    2、实例变量初始化代码,按实例变量在类中出现的顺序;
    3、构造方法中的其它方法体(如果第一句是super(…)调用,则不包含该句)。

    如果构造方法中包含super(…)或this(…)调用,那么它们只能作为该构造方法的第一条语句,也就是说连try…catch都不可以有。因为必须为第一条语句,所以super(…)和this(…)调用是不会出现在一起的。

    用javap -c S反编译获得各<init>方法的字节码如下:

    public S() throws java.lang.Exception;
    Code:
       0: aload_0
       1: ldc           #2                  // String S()
       3: invokespecial #3                  // Method "<init>":(Ljava/lang/String;)V
       6: return
     
    public S(java.lang.String) throws java.lang.Exception;
    Code:
       0: aload_0
       1: invokespecial #4                  // Method P."<init>":()V
       4: aload_0
       5: iconst_4
       6: putfield      #1                  // Field value1:I
       9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: aload_1
      13: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: return
     
    public S(int) throws java.lang.Exception;
    Code:
       0: aload_0
       1: invokespecial #4                  // Method P."<init>":()V
       4: aload_0
       5: iconst_4
       6: putfield      #1                  // Field value1:I
       9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #7                  // String abc
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: return

    可以看出,在类S的第一个构造方法s()中,字节码表现出的只是调用了本类的<init>(String)方法;从第二个构造方法S(String)其对应的<init>字节码中可以看出,虽然没有用super(…)显式的调用超类的构造方法,编译器还是生成了偏移量为0,1的指令用于调用超类的<init>方法,偏移量为4,5,6的指令是给变量value1赋值为4,后面即为构造方法体——唯一的一条打印语句;再看S(int)对应的<init>方法,构造方法的第一条语句为super(…)调用,对应着<init>中的偏移量为0,1的指令;偏移量为4,5,6的指令是为实例变量value1赋值为4;最后是打印字符串abc。

    再看看P反编译后构造方法体:

    public P() throws java.lang.Exception;
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_5
       6: putfield      #2                  // Field v1:I
       9: aload_0
      10: aload_0
      11: invokevirtual #3                  // Method getV1:()I
      14: putfield      #4                  // Field v2:I
      17: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      20: ldc           #6                  // String P
      22: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      25: return

    偏移量为0,1的指令调用Object的<init>方法,也就是类P的超类;指令4,5,6为v1赋值为5,指令9,10,11,14是为实例变量v2获取值并赋值;剩余的是打印字符串P。

    从上面几个构造方法与对应的<init>方法分析中可以看出,它们都是符合前文所述的规则的。那么对于本文开头的例子v2的值为何为0就好分析了:
    1、new S()的时候调用的是S类的<init>()方法,它去调用了本类的<init>(String)方法,在这个<init>中,先去调用超类,也就是P的<init>()方法,P的<init>()方法又去调用P的超类Object的<init>()方法,然后去给v1,v2赋值,v2赋值调用的是getV1(),而这个方法是一个多态方法,它会去调用new对象的那个类的getV1方法,也就是S中的getV1,S中的getV1返回的是value1的值,而此时value1赋值操作还没有做呢,所以返回的是它的默认值0.

    如此,整个创建对象期间的顺序就一清二楚了。

  • 相关阅读:
    linux 后台启动 nodejs httpserver
    Abp vNext获取Token
    git log 修改date显示格式
    解决Visual Studio关闭后马上删除项目无法删除的问题
    Docker DeskTop 起不来啦,Docker 报错
    (笔记)电磁兼容之差模噪声与共模噪声(第四讲)
    (笔记)电磁兼容之差模与共模辐射场强计算公式(第五讲)
    mysql使用存储过程批量给表加字段
    基于gradle的Groovy之Spock测试框架入门三
    基于gradle的Groovy之Spock测试框架入门一
  • 原文地址:https://www.cnblogs.com/01picker/p/4306225.html
Copyright © 2020-2023  润新知