自制Java虛擬機(五)實現繼承、多態、invokevirtual
本篇文章將研究如何實現面向對象的繼承和多態特性,同時實現invokevirtual
。
一、實例屬性的繼承
繼承實現了數據與方法的複用。
類屬性與實例屬性
- 類屬性的修飾符要加上
static
,是屬於類的 - 類屬性只有一份,該類創建的多個對象共享同一份類屬性,jvm中由
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; i<pclass->fields_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);
}
二、invokevirtual中方法的解析
invokevirtual與invokespecial
上面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);
... // 省略
}
跟解析字段流程類似。
方法解析出來,調用就很簡單了,跟之前的做法類似,就不上代碼了。
四、測試
1. 實例屬性的繼承與普通的invokevirtual
把上面的三個文件: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
// 故意搞這麼複雜就是爲了測試我們的程序正確與否
可見程序運行正確。
2. 多態的測試
把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運行,結果如下:
可見運行正確,方法被解析到了對象各自的類所定義的方法。
五、總結
綜上,本文實現了:
- 加載多個class文件
- 面向對象的繼承特性
- 面向對象的多態特性
invokevirtual
指令
暫時沒有考慮接口(interface)。