β

对象实例化的顺序

Ticmy 250 阅读

创建一个对象大概有以下几种方式:
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()

调用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:
: aload_0
: ldc           #2                  // String S()
: invokespecial #3                  // Method "<init>":(Ljava/lang/String;)V
: return

public S(java.lang.String) throws java.lang.Exception;
Code:
: aload_0
: invokespecial #4                  // Method P."<init>":()V
: aload_0
: iconst_4
: putfield      #1                  // Field value1:I
: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
: aload_1
: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: return

public S(int) throws java.lang.Exception;
Code:
: aload_0
: invokespecial #4                  // Method P."<init>":()V
: aload_0
: iconst_4
: putfield      #1                  // Field value1:I
: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc           #7                  // String abc
: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: 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:
: aload_0
: invokespecial #1                  // Method java/lang/Object."<init>":()V
: aload_0
: iconst_5
: putfield      #2                  // Field v1:I
: aload_0
: aload_0
: invokevirtual #3                  // Method getV1:()I
: putfield      #4                  // Field v2:I
: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc           #6                  // String P
: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: 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.

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

作者:Ticmy
享受工作、学习、生活之点滴
原文地址:对象实例化的顺序, 感谢原作者分享。

发表评论