`

由一个小程序引发的思考 — 关于字段和方法的分派

阅读更多

面向对象三大特征封装、继承和多态,此处我们一般都知道方法的多态性,覆盖和重载。但是字段呢?当然根据定义,跟字段无关,也就是不能覆盖?先看一个小程序:

package com.yymt.jvm.method.dispatch;

public class DispatchTest {
	public static void main(String[] args) {
		Base b = new Sub();
		System.out.println(b.x);
	}
}

class Base {
	int x = 10;

	public Base() {
		this.printMessage();
		x = 20;
	}

	public void printMessage() {
		System.out.println("Base.x = " + x);
	}
}

class Sub extends Base {
	int x = 30;

	public Sub() {
		this.printMessage();
		x = 40;
	}

	public void printMessage() {
		System.out.println("Sub.x = " + x);
	}
}

  输出是什么?也许你已经见到过类似题目了,答案也很明显。但是为什么是这样呢?还是先输出下答案吧:

Sub.x = 0
Sub.x = 30
20

  来仔细分析下这个过程,首先从main方法入手:

Base b = new Sub();

  此处创建一个Sub实例,会调用Sub的构造函数:

public Sub() {
	this.printMessage();
	x = 40;
}

  但是根据jvm规范,构造函数会被编译成名称为<init>的方法。构造函数如果没有显式调用this(xxx)或者super(xxx),即当前类别的构造函数或者超类构造函数,编译时调用超类的无参构造函数作为<init>方法第一条指令,也就是说此处会调用Base的无参构造函数:

public Base() {
	this.printMessage();
	x = 20;
}

  注意,作为构造函数第一条语句。而类的实例字段直接在声明的时候初始化的话,或者是代码块,会被收集起来放到<init>方法里,按照语句赋值顺序放进来,当然也是放在超类构造函数之后的。后续才是构造函数里边的代码内容。所以此处相当于:

public Sub() {
	super();//Base()
	x = 30;
	this.printMessage();
	x = 40;
}

顺便看下字节码,跟上边源码一致吧?

  // Method descriptor #8 ()V
  // Stack: 2, Locals: 1
  public Sub();
       aload_0 [this]
       invokespecial com.yymt.jvm.method.dispatch.Base() [10]
       aload_0 [this]
       bipush 30
       putfield com.yymt.jvm.method.dispatch.Sub.x : int [12]
       aload_0 [this]
       invokevirtual com.yymt.jvm.method.dispatch.Sub.printMessage() : void [14]
       aload_0 [this]
       bipush 40
       putfield com.yymt.jvm.method.dispatch.Sub.x : int [12]
       return
      Line numbers:
        [pc: 0, line: 26]
        [pc: 4, line: 24]
        [pc: 10, line: 27]
        [pc: 14, line: 28]
        [pc: 20, line: 29]
      Local variable table:
        [pc: 0, pc: 21] local: this index: 0 type: com.yymt.jvm.method.dispatch.Sub

  调用哪个方法?

但是由于java的方法是动态分派的,会根据运行时实例类型来决定调用谁的方法,此处this为Sub的实例,我们在new Sub()嘛,所以super()中调用的是Sub的printMessage方法,此处输出就是Sub.x = ?。 

调用哪个字段?

继续分析Base的构造函数,其等价于:

public Base() {
	super();//Object()
	x = 10;
	this.printMessage();
	x = 20;
}

  来看下Base()的字节码,上边源码一致的:

// Method descriptor #8 ()V
// Stack: 2, Locals: 1
public Base();
	  aload_0 [this]
	  invokespecial java.lang.Object() [10]
	  aload_0 [this]
	  bipush 10
	  putfield com.yymt.jvm.method.dispatch.Base.x : int [12]
	  aload_0 [this]
	  invokevirtual com.yymt.jvm.method.dispatch.Base.printMessage() : void [14]
	  aload_0 [this]
	  bipush 20
	  putfield com.yymt.jvm.method.dispatch.Base.x : int [12]
    return
    Line numbers:
      [pc: 0, line: 13]
      [pc: 4, line: 11]
      [pc: 10, line: 14]
      [pc: 14, line: 15]
      [pc: 20, line: 16]
    Local variable table:
      [pc: 0, pc: 21] local: this index: 0 type: com.yymt.jvm.method.dispatch.Base

  在此处给x赋值为10,然后this.printMessage()是不是就是输出Sub.x = 10呢?慢着慢着,为什么会是Sub.x = 10,如果我根据方法调用的逻辑来推导,应该是Sub.x = Sub实例中x此刻的值啊!此处推导用的是方法的动态分派,这种方法适用于字段么??我们换个角度,用静态派发来分析下,即编译时决定调用的版本!我们已经知道,重载是静态派发,重写是动态派发的。好吧,我们根据静态类型来分析(Base的构造函数中)this.printMessage(),此处this是Sub的实例。上边我们已经分析,此处(Base的构造函数中)this.printMessage调用的是Sub的printMessage方法:

public void printMessage() {
	System.out.println("Sub.x = " + x);
}

  那编译时,x就是Sub的x的,因为在Sub方法中调用的嘛!看字节码:

// Method descriptor #8 ()V
// Stack: 4, Locals: 1
public void printMessage();
		getstatic java.lang.System.out : java.io.PrintStream [21]
		new java.lang.StringBuilder [27]
		dup
		ldc <String "Sub.x = "> [29]
		invokespecial java.lang.StringBuilder(java.lang.String) [31]
		aload_0 [this]
		getfield com.yymt.jvm.method.dispatch.Sub.x : int [12]
		invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [34]
		invokevirtual java.lang.StringBuilder.toString() : java.lang.String [38]
		invokevirtual java.io.PrintStream.println(java.lang.String) : void [42]
		return
    Line numbers:
      [pc: 0, line: 32]
      [pc: 25, line: 33]
    Local variable table:
      [pc: 0, pc: 26] local: this index: 0 type: com.yymt.jvm.method.dispatch.Sub

  忽略掉编译时把String相加转换成了StringBuilder,只看后续对x的调用:

aload_0 [this]
getfield com.yymt.jvm.method.dispatch.Sub.x : int [12]

  看到了,编译器决定调用的是Sub.x,那运行期就是Sub.x了么?如果是静态分派,的确已经是了,如果是动态分派,aload_0的this也是Sub,所以还是Sub.x?乱了乱了!当然,派发本身就是决定方法的调用版本,定义上就没有把决定字段的调用版本归结到派发里!用派发来推导本身就没有理论上的支撑的。

我们还是来分析下getfield指令吧,这个比较靠谱,但是,但是查了一下,getField指令只是从获取类的实例域,放入栈中,完全没有像方法调用一样,还分为invokevirtual是运行期根据类型来决定,invokespecial是调用私有、构造、超类方法,invokestatic、invokeinterface这种分类,也没有说明白是对象字段在内存中存放时候是不存在像方法一样只有一个表项,实际测试来看,超类和子类的同名同类型实例字段是存储了两份,根据静态类型的不同,调用是不同的。getfield也是根据指令后边操作数指向的常量池中实际的类型类决定调用哪一个的。此处推导getfield是根据静态类型决定字段调用!实际上,方法的静态分派也就是编译期决定方法的调用版本,跟编译期决定字段的调用版本意思上是一致的,只是决定方法的调用才叫分派。

所以此处就是调用Sub.x了。在Sub构造函数中由于x的赋值在super之后,所以调用super的时候x还是默认值,即为0。所以Base里边this.printMessage()时B的实例x=0。

最后的b.x,按照上边推断,是根据字段的静态类型来决定调用的,这条语句运行完后,x是Base中x的值,所以此处就是20了!

System.out.println(b.x);
  字节码:
aload_1 [b]
getfield com.yymt.jvm.method.dispatch.Base.x : int [25]
  以上分析中,当然只有一个Sub实例,Base.x和Sub.x只代表指向这个实例的不同字段而已!后续需要好好分析下实例字段在内存中的是如何存放的了。
最后补充一下Base的printMessage中调用的x,编译期是Base.x,如果Sub中不重写printMessage方法,输出又会是什么?
不知道分析的对不对,欢迎讨论!
0
1
分享到:
评论
1 楼 mynotes 2011-11-08  

相关推荐

Global site tag (gtag.js) - Google Analytics