本篇文章将研究如何实现面向对象的继承和多态特性,同时实现invokevirtual
。
继承实现了数据与方法的复用。
类属性与实例属性
static
,是属于类的getstatic
、putstatic
指令操作实例属性每个对象各自一份,各管各的互不干扰,jvm中由getfield
、putfield
指令操作
上篇文章在没有考虑没有继承的情况下实现了getfield
、putfield
指令,本篇文章来对其进行修正。
public
、protected
与private
不管是父类的实例属性是public
、protected
还是private
,子类继承自父类时,这些属性都会一一继承,只不过private
的属性在子类中不能直接访问而已。你看不到它们,但它们确实存在。
现在问题来了:
public
、protected
实例属性时,索引是指向子类常量池的Fieldref
类型的结构,而这个结构的class_index
是指向当子类的,必须能够正确地解析到定义该属性的类解决方法:
实例属性的安排。在类的继承链上,自顶向下给每个类的实例属性从小到大依次指定索引,这样就保证每个类的实例属性在该继承链中索引是唯一的。如下图布局1所示:
因此我们在创建对象的时候必须从当前类开始,沿着继承链遍历父类,计算继承而来的属性个数并重新给每个类的实例属性指定索引(之前计算的的索引是在当前类的)。
修正之后创建对象的函数newObject
实现可以如下:
Object* newObject(OPENV *env, Class* pclass) {
CONSTANT_Class_info* parent_class_info;
Object *obj;
Class *tmp_class;
int i = 0, total_size = 0;
// step 1: load parent class recursively
tmp_class = pclass;
while (tmp_class->super_class) {
parent_class_info = tmp_class->constant_pool[tmp_class->super_class];
if (NULL == parent_class_info->pclass) {
parent_class_info->pclass = systemLoadClassRecursive(env, (CONSTANT_Utf8_info*)(tmp_class->constant_pool[parent_class_info->name_index]));
tmp_class->parent_class = parent_class_info->pclass;
}
tmp_class = tmp_class->parent_class;
}
// step 2: calculate fields size of parent class recursively, if not calculated yet
if (pclass->parent_fields_size == -1) {
if (pclass->parent_class == NULL) {
pclass->parent_fields_size = 0;
} else {
pclass->parent_fields_size = getClassFieldsSize(pclass->parent_class);
}
for(i=0; ifields_count; i++) {
// 重新计算属性的索引:过滤掉类属性
if (NOT_ACC_STATIC((pclass->fields[i])->access_flags)) {
(pclass->fields[i])->findex += pclass->parent_fields_size;
}
}
}
// 该对象属性的总大小 = 父类实例属性大小 + 本类实例属性大小
total_size = pclass->parent_fields_size + pclass->fields_size;
obj = (Object*)malloc(sizeof(Object) + ((total_size+1)<<2));
obj->fields = (char*)(obj+1);
obj->pclass = pclass;
obj->length = (total_size+1) << 2;
return obj;
}
由于父类可能需要多次用到,我们可以把解析好的父类保存在一个哈希中,用类的全限定名做为索引,如test/Parent,需要引用父类的时候先用哈希表中找,找不到就加载,加载完毕保存在哈希表中,为了方便,在当前类中设置个字段指向父类。
typedef struct _ClassFile{
...
struct _ClassFile *parent_class; // 指向父类
int parent_fields_size;
int fields_size;
} ClassFile;
typedef ClassFile Class;
(java/lang/Object是所有类的终极父类,我们一直往上解析的话最终会涉及到这个类,可以从Java的安装目录的jdk目录下找到src.zip这个包,这个包有jdk的源码,把它解压到指定目录,编译成class文件,到时我们的类加载函数从这里加载就行)
实例属性的解析。
实例属性的符号链的class_index指向子类。这时必须从子类的fields
数组中查找,找不到则上溯到父类,直到找到为止。拿下面的例子来说明下:
Parent.java
package test;
class Parent
{
public int x1;
protected int y1;
private int z1;
public void setZ1(int z1)
{
this.z1 = z1;
}
public int getZ1()
{
return this.z1;
}
protected int addXY()
{
return this.x1 + this.y1;
}
}
Child.java
package test;
class Child extends Parent
{
private int x2 = 4;
public int doSomething()
{
int xy = this.addXY(); // invoke protected method of parent class
return xy + super.getZ1() + this.x2;
}
}
TestInheritance.java
package test;
class TestInheritance
{
public static void main(String[] args)
{
Child someone = new Child();
someone.setZ1(3); // invoke public method of parent class
someone.x1 = 1; // access public field
someone.y1 = 2; // access protected field
int result = someone.doSomething();
}
}
Child类继承了Parent类,然后在TestInheritance类中,我们创建了Child类的一个实例,someone.x1=1
这行代码,访问的是Parent类中定义的实例属性。查找过程如下:
修正后的解析实例属性的代码可以如下:
void resolveClassInstanceField(Class* caller_class, CONSTANT_Fieldref_info **pfield_ref)
{
... // 省略
do {
callee_cp = callee_class->constant_pool;
fields_count = callee_class->fields_count;
for (i = 0; i < fields_count; i++) {
field = (field_info*)(callee_class->fields[i]);
tmp_field_name_utf8 = (CONSTANT_Utf8_info*)(callee_cp[field->name_index]);
if (NOT_ACC_STATIC(field->access_flags) &&
field_name_utf8->length == tmp_field_name_utf8->length &&
strcmp(field_name_utf8->bytes, tmp_field_name_utf8->bytes) == 0) {
tmp_field_descriptor_utf8 = (CONSTANT_Utf8_info*)(callee_cp[field->descriptor_index]);
if (field_descriptor_utf8->length == tmp_field_descriptor_utf8->length &&
strcmp(field_descriptor_utf8->bytes, tmp_field_descriptor_utf8->bytes) == 0) {
field_ref->ftype = field->ftype;
field_ref->findex = field->findex;
found = 1;
break;
}
}
}
if (found) {
break;
}
// 没找到就从父类中找
callee_class = callee_class->parent_class;
} while(callee_class != NULL);
... // 省略
}
与之前的代码相比,变化主要为:
do while
循环,用来自底向上遍历name_index
改成比较属性名字的字符串长度和内容(因为常量池不一样了)另外注意,上一篇我们是用当前类来创建对象的,现在可不是了,我们是在TestInheritance类中创建Child类,所以new
指令的实现需要修正下:
Opreturn do_new(OPENV *env)
{
Class* pclass;
PRINTSD(TO_SHORT(env->pc));
short index = TO_SHORT(env->pc);
Object *obj;
if (env->current_class->this_class == index) {
pclass = env->current_class;
} else {
// begin resolve non-current class
class_info = (CONSTANT_Class_info*)(env->current_class->constant_pool[index]);
// 如果该类没有解析过,则获取类名,根据类名从已加载类的哈希中查找
if (class_info->pclass == NULL) {
utf8_info = (CONSTANT_Utf8_info*)(env->current_class->constant_pool[class_info->name_index]);
pclass = class_info->pclass = findLoadedClass(utf8_info->bytes, utf8_info->length);
}
// 要是没找到就调用类加载函数加载(同时加载该类的父类...)
if (NULL == pclass) {
pclass = class_info->pclass = systemLoadClassRecursive(env, utf8_info);
}
// end resolve non-current class
}
obj = newObject(env, pclass);
PUSH_STACKR(env->current_stack, obj, Reference);
INC2_PC(env->pc);
}
上面TestInheritance.java的代码:
someone.setZ1(3) // 调用父类的public方法(隐式)
该行代码对应的字节码(取最后一行)是invokevirtual #4
,#4在当前常量池中指向的是Methodref类型的结构,该method的名称与类型为test/Child.setZ1:(I)V
。可是我们从源代码中可以看到,test/Child类并没有定义该方法,该方法是从父类test/Parent中继承过来的。
另一行代码:
int result = someone.doSomething(); // 调用本类的public方法
其中someone.doSomething()
对象的字节码为invokevirtual #7
,#7在当前常量池指向的是Methodref类型的结构,该method的名称与类型为test/Child.doSomething:()I
。该方法是在test/Child类中定义的public
方法。
Child.java中的代码:
// doSomething方法中
super.getZ1() // 显式调用父类的public方法
对应的字节码为invokespecial #4
,#4在当前常量池指向的是Methodref类型的结构,该method的名称与类型为test/Parent.getZ1:()I
。该方法是在test/Parent类中定义的public
方法。
代码 | 当前常量池中指向的类 | 定义该方法的类 | 方法类型 | 指令 |
---|---|---|---|---|
someone.setZ1(3) | test/Child | test/Parent | public | invokevirtual |
someone.doSomething() | test/Child | test/Child | public | invokevirtual |
super.getZ1() | test/Parent | test/Parent | public | invokespecial |
可见,显式调用父类的方法生成的是invokespecial
指令,而隐式调用父类的public
方法生成的是invokevirtual
指令。它们在当前常量池指向的类也不一样,invokespecial #n
,n指向的是定义该方法的类,而invokevirtual #n
,n指向的类则未必。
jvm(version8)中说道:
The difference between the invokespecial instruction and the invokevirtual instruction (§invokevirtual) is that invokevirtual invokes a method based on the class of the object. The invokespecial instruction is used to invoke instance initialization methods (§2.9) as well as private methods and methods of a superclass of the current class.
invokevirtual
是基于对象的类来调用的方法的,而invokespecial
用于调用实例初始化方法(构造函数吧),private
方法和当前类的父类的方法(需要显式调用,super.method())。所以invokevirtual
指令的实现中,方法的解析可以按如下流程:
不同类的对象对同一消息做出不同的响应叫做多态。多态需要三个条件:
下面是一个多态的例子:
Employee.java
package test;
class Employee
{
private int baseSalary;
Employee(int baseSalary)
{
this.baseSalary = baseSalary;
}
public int getSalary()
{
return this.baseSalary;
}
}
Engineer.java
package test;
class Engineer extends Employee
{
private int bonus;
Engineer(int baseSalary, int bonus)
{
super(baseSalary);
this.bonus = bonus;
}
public int getSalary()
{
return super.getSalary() + this.bonus;
}
}
Manager.java
package test;
class Manager extends Employee
{
private int bonus;
Manager(int baseSalary, int bonus)
{
super(baseSalary);
this.bonus = bonus;
}
public int getSalary()
{
return 2 * super.getSalary() + bonus;
}
}
TestPoly.java
package test;
class TestPoly
{
private int getSalary(Employee e)
{
// getSalary will be resolved to different method according the real Class of e
return e.getSalary(); // attention here
}
public static void main(String[] args)
{
TestPoly obj = new TestPoly();
Employee emp = new Employee(100);
Employee eng = new Engineer(150, 50); // upcasting
Employee mgr = new Manager(100, 100); // upcasting
int salary1 = obj.getSalary(emp);
int salary2 = obj.getSalary(eng);
int salary3 = obj.getSalary(mgr);
int result = salary1;
result = salary2;
result = salary3;
}
}
上面多态的例子,TestPoly这个类的getSalary方法:
e.getSalary()
这行代码对应的字节码为:
invokevirtual #2
其中#2指向的Methodref
,其对应的方法为:test/Employee.getSalary:()I。
实际上,当e
为不同类创建的对象时,该方法需要解析到创建该对象的类(可能是Empolyee的子类),不一定是Employee类。如果e
是Manager类创建的对象,我们就要把该方法解析到Manager类中定义的getSalary
方法。上面我们实现的invokevirtual
指令,解析方法的流程仍然是有效的,只不过由于一个Methodref
可能对应多个方法入口了,我们需要一个表来保存这些方法入口,这样后面重复调用时就可以先从这个表中查,查不到再解析。
// 方法表中的一项(方法入口)
typedef struct _MethodEntry {
Class *pclass; // 类
method_info *method; // 方法
struct _MethodEntry *next;
} MethodEntry;
// 方法表
typedef struct _MethodTable {
MethodEntry *head; // 指向第一项
MethodEntry *tail; // 指向最后一项
} MethodTable;
// 创建新的方法入口
MethodEntry* newMethodEntry(Class *pclass, method_info *method)
{
}
// 创建新的方法表
MethodTable* newMethodTable()
{
}
// 往方法表中添加一个方法入口
void addMethodEntry(MethodTable *mtable, MethodEntry *mte)
{
}
// 根据类从方法表中查找方法入口
MethodEntry* findMethodEntry(MethodTable *mtable, Class *pclass)
{
}
添加方法表后Methodref
如图所示:
invokevirtual
可如下实现:
Opreturn do_invokevirtual(OPENV *env)
{
PRINTSD(TO_SHORT(env->pc));
short mindex = TO_SHORT(env->pc);
INC2_PC(env->pc);
callClassVirtualMethod(env, mindex);
}
void callClassVirtualMethod(OPENV *current_env, int mindex)
{
... // 省略
// 如果还没有建立方法表,建立方法表
if (NULL == method_ref->mtable) {
method_ref->args_len = getMethodrefArgsLen(current_class, nt_info->descriptor_index);
method_ref->mtable = newMethodTable();
}
// 获取调用该方法的对象
caller_obj = *(Reference*)(current_env->current_stack->sp - ((method_ref->args_len+1)<<2));
// 从方法表中查找方法入口,没有找到就解析
if(NULL == (mentry = findMethodEntry(method_ref->mtable, caller_obj->pclass))) {
mentry = resolveClassVirtualMethod(caller_obj->pclass, &method_ref, (CONSTANT_Utf8_info*)(cp[nt_info->name_index]), (CONSTANT_Utf8_info*)(cp[nt_info->descriptor_index]));
}
// 调用该方法
callResolvedClassVirtualMethod(current_env, method_ref, mentry);
}
由于invokevirtual
是根据对象的类来调用的,所以需要获取对象的类,则需要先获取对象,而对象在操作数栈中,不知道方法参数的长度是定位的,需要用getMethodrefArgsLen
获取参数长度。
解析方法:
MethodEntry* resolveClassVirtualMethod(Class* caller_class, CONSTANT_Methodref_info **pmethod_ref, CONSTANT_Utf8_info *method_name_utf8, CONSTANT_Utf8_info *method_descriptor_utf8)
{
... // 省略
do {
callee_cp = callee_class->constant_pool;
for (i = 0; i < callee_class->methods_count; i++) {
method = (method_info*)(callee_class->methods[i]);
tmp_method_name_utf8 = (CONSTANT_Utf8_info*)(callee_cp[method->name_index]);
tmp_method_descriptor_utf8 = (CONSTANT_Utf8_info*)(callee_cp[method->descriptor_index]);
if (method_name_utf8->length == tmp_method_name_utf8->length &&
strcmp(method_name_utf8->bytes, tmp_method_name_utf8->bytes) == 0) {
if (method_descriptor_utf8->length == tmp_method_descriptor_utf8->length &&
strcmp(method_descriptor_utf8->bytes, tmp_method_descriptor_utf8->bytes) == 0) {
mentry = newMethodEntry(callee_class, method);
addMethodEntry(method_ref->mtable, mentry);
found = 1;
break;
}
}
}
if (found) {
break;
}
callee_class = callee_class->parent_class;
} while (callee_class != NULL);
... // 省略
}
跟解析字段流程类似。
方法解析出来,调用就很简单了,跟之前的做法类似,就不上代码了。
把上面的三个文件:Parent.java, Child.java, TestInheritance.java编译成字节码:
javac Parent.java Child.java TestInheritance.java
这三个文件都在test目录中。我们加载test/TestInheritance.class,运行main
函数,结果如下:
1. 在TestInheritance中创建Child类的实例someone // new 指令
2. someone调用Parent类的public方法setZ1将继承而来的private变量z1设为3 // invokevirtual
3. someone直接访问从Parent类继承而来的x1, y1属性,设置x1=1,y1=2 // setfield
4. someone调用Child类中定义的doSomething public方法 // invokevirtual
a. doSomething 中隐式调用Parent类的addXY protected方法 // invokevirtual
i. Parent类的addXY()方法把x1, y1的值相加并返回
b. doSomething 中用super.getZ1()显式调用Parent类的getZ1 public 方法 // invokespecial
c. 把几个数相加返回
最终执行的是x1 + y1 + z1 + x2 = 1 + 2 + 3 + 4 = 10
// 故意搞这么复杂就是为了测试我们的程序正确与否
可见程序运行正确。
把test/Employee.java, test/Engineer.java, test/Manager.java, test/TestPoly.java编译成字节码:
javac test/Employee.java test/Engineer.java test/Manager.java test/TestPoly.java
让我们的程序加载test/TestPoly.class运行,结果如下:
可见运行正确,方法被解析到了对象各自的类所定义的方法。
综上,本文实现了:
invokevirtual
指令暂时没有考虑接口(interface)。