`

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

阅读更多

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

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  

相关推荐

    完美解决distinct中使用多个字段的方法

    完美解决distinct中使用多个字段的方法,完美解决distinct中使用多个字段的方法完美解决distinct中使用多个字段的方法完美解决distinct中使用多个字段的方法完美解决distinct中使用多个字段的方法

    微信小程序加密字段解密工具

    微信小程序加密字段解密工具代码,真的没搞懂微信怎么想的,微信退款,公众号消息,小程序,微信支付的加密解密方式全都不一样,每一个都要单独调试,简直要死人,那我就调试好一个就传一个上来

    将数据库中的两个字段合并为一个字段

    在数据库管理过程中,经常会遇到需要对数据进行整理和优化的情况,其中一个常见的需求就是将数据库中的两个字段合并为一个字段。这种操作不仅可以简化数据结构,还能提高数据查询的效率。接下来,我们将详细介绍如何...

    SAP 标准报表增加字段的方法介绍

    ### SAP标准报表增加字段的方法详解 #### 一、引言 在SAP系统中,标准报表为用户提供了一种快速获取企业关键数据的方式。然而,在实际业务操作过程中,有时标准报表提供的信息并不能完全满足用户的需求,这时就...

    根据mysql数据的一个字段数据修改另一个字段的数据

    要求:查询一个字段的数据,将每个数据拆分,取第一个字符,将第一个字符遍历出来,替换到另一个字段里面

    用反射来解决字段多带来的烦恼 不用一个一个字段来赋值了

    这将返回一个`FieldInfo[]`数组,包含了所有公共和私有字段。如果你想只获取公共字段,可以使用`GetFields(BindingFlags.Public)`: ```csharp FieldInfo[] fields = objectType.GetFields(BindingFlags.Public | ...

    oracle实现多字段匹配一个关键字查询

    在Oracle数据库中,有时我们需要对多个字段进行联合搜索,即多字段匹配一个关键字查询。本文将详细介绍两种在Oracle中实现这种查询的方法。 ### 一、使用管道符号(||)连接字段 这种方法通过使用Oracle中的字符串...

    将一个字段中的名称分为两个或多个字段未知

    标题 "将一个字段中的名称分为两个或多个字段" 暗示了我们在处理数据库或数据管理时遇到的问题,可能是在Access等数据库系统中。在Access中,经常需要对存储的数据进行清洗和重组,以满足特定的分析或报告需求。描述...

    sql_按照某一个字段进行去重后获取全部字段

    根据题目中提供的 SQL 语句,我们可以看到这是一个较为复杂的去重操作案例,它不仅仅使用了 GROUP BY 进行分组,还结合了 EXISTS 子查询来进一步过滤结果。 #### SQL 语句解析 ```sql SELECT * FROM person_real_...

    C#中类、属性、字段、方法举例

    在上述代码中,`MyClass`是一个包含一个公共字段`age`、一个私有字段`name`、一个公共属性`Name`以及一个构造函数和一个方法`DisplayInfo`的类。字段是存储数据的地方,而属性提供了对字段的安全访问,通常用get和...

    关于字段增强关于字段增强

    当一个输入字段与特定的功能代码关联时,系统会在DCI(DATA COMMUNICATIONS INPUT)阶段分支到一个函数模块。函数模块的命名规则如下: - 前缀:FIELD_EXIT_ - 中间部分:对应的数据元素名称 - 后缀:_0到_9(可选)...

    一个不错的处理shp小程序, 实现shp加载及后期渲染,字段分析

    标题中的“一个不错的处理shp小程序”指的是一个用于处理Shapefile格式数据的程序,这种程序通常用于地理信息系统(GIS)领域。Shapefile是一种常见的矢量数据格式,用于存储地理空间特征,如点、线和多边形。它由多...

    java实体类字段自定义-数据库字段和程序实体类属性不一致解决方案.docx

    例如,实体类中有一个字段名为 "userName",而数据库表中的字段名为 "USER_NAME"。这种情况下,需要实现实体类字段的自定义,以便与数据库字段保持一致。 二、解决方案 解决 Java 实体类字段自定义问题的思路是...

    SQLServer中如何将一个字段的多个记录值合在一行显示

    SQLServer 中将一个字段的多个记录值合并到一行显示的实现方法 SQL Server 是一种关系型数据库管理系统,具有强大的数据处理能力和存储能力。在实际应用中,我们经常需要将一个字段的多个记录值合并到一行显示,以...

    简单的小程序,小程序,就是一个小程序,赚积分下载

    该程序使用 Java 的图形用户界面(GUI)组件来创建一个带有按钮和文本字段的计算器界面,用户可以通过点击按钮来输入数字和运算符,最后显示计算结果。 Java 图形用户界面(GUI)组件 在本程序中,我们使用了 Java...

    关于属性与字段的区别

    属性和字段都可以用一个非 void 类型声明一个名称,例如 `class Example { int field; int Property { ... } }`。它们都可以用任意的访问修饰符,例如 `class Example { private int field; public int Property { ....

    请求重定向个请求分派

    * 请求分派只能将请求转发给同一个 Web 应用程序中的其他组件,而重定向不仅可以定向到当前应用程序中的其他资源,也可以重定向到其他站点的资源上。 * 重定向的访问过程结束后,浏览器地址栏中显示的 URL 会发生...

    泛微移动建模常见问题-表单页面,由一个字段的变化改变另一个字段的只读、编辑、必填状态

    移动建模常见问题-表单页面,由一个字段的变化改变另一个字段的只读、编辑、必填状态

    DEX文件字段和方法定义解析.zip

    在Android应用开发中,Dalvik Executable (DEX) 文件是一个至关重要的组成部分,它包含了应用程序的所有字节码、类定义、字段和方法。本教程将深入探讨DEX文件的字段和方法定义,并通过Python脚本进行解析。我们将...

    微信小程序项目实例——今日美食

    总的来说,“微信小程序项目实例——今日美食”是一个集前端技术、后端数据管理、用户体验设计于一体的综合项目,对于学习微信小程序开发和移动应用设计的人员来说,是一个很好的实践案例。通过分析和实施这个项目,...

Global site tag (gtag.js) - Google Analytics