自制Java虛擬機(五)實現繼承、多態、invokevirtual

自制Java虛擬機(五)實現繼承、多態、invokevirtual

本篇文章將研究如何實現面向對象的繼承和多態特性,同時實現invokevirtual

一、實例屬性的繼承

繼承實現了數據與方法的複用。

  1. 類屬性與實例屬性

    • 類屬性的修飾符要加上static,是屬於類的
    • 類屬性只有一份,該類創建的多個對象共享同一份類屬性,jvm中由getstaticputstatic指令操作
    • 實例屬性每個對象各自一份,各管各的互不干擾,jvm中由getfieldputfield指令操作

      上篇文章在沒有考慮沒有繼承的情況下實現了getfieldputfield指令,本篇文章來對其進行修正。

  2. publicprotectedprivate

    不管是父類的實例屬性是publicprotected還是private,子類繼承自父類時,這些屬性都會一一繼承,只不過private的屬性在子類中不能直接訪問而已。你看不到它們,但它們確實存在。

現在問題來了:

  • 創建對象時,我們要給從父類(以及父類的父類…)繼承過來的實例屬性分配內存,並指定索引以便訪問,如何安排這些索引?
  • 子類中訪問父類的publicprotected實例屬性時,索引是指向子類常量池的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);
    ... // 省略
}

與之前的代碼相比,變化主要爲:

  1. 查找的代碼外多包裹了一個do while循環,用來自底向上遍歷
  2. 屬性匹配由純比較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指令的實現中,方法的解析可以按如下流程:

這裏寫圖片描述

三、多態

不同類的對象對同一消息做出不同的響應叫做多態。多態需要三個條件:

  1. 有繼承關係
  2. 子類重寫父類的方法
  3. 父類引用指向子類對象。

下面是一個多態的例子:

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)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章