重載-重寫區別(從方法調用層面分析)

大綱

前言

重載和重寫的區別是啥?看了靜態分派和動態分派你就知道了。java

上一篇咱們說了提到符號引用轉化爲直接引用的兩種方法,這節咱們就講下這兩種方法,由於引用轉化是方法的調用才執行的,因此先弄明白方法調用git


個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關,劍指offer題解(Java版),能夠點個star。能夠看個人github主頁,天天都在更新喲。github

邀請您跟我一同完成 repo面試


方法調用

方法執行有兩個步驟spring

  • 方法調用,肯定調用那個方法
  • 基於棧的解釋執行。真正執行方法的字節碼

因此方法調用並不等於方法的執行,他只是其中一個步驟。ide

方法調用有兩種源碼分析

  • 靜態解析
  • 分派(有靜有動)

還記得咱們上篇文章說的將字節碼中的符號引用轉化爲直接引用的兩種方法嗎?這節就講這兩個方法。他們都屬於方法的調用性能

重點講重載和重寫的區別,也就是分派的部分,面試應該也多問這個吧。優化

方法調用的字節碼指令

  • invokestatic:調用靜態方法
  • invokespecial:調用實例構造器<init>方法、私有方法和構造方法
  • invokevirtual:調用全部的虛方法
  • invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象
  • invokedynamic:
    • 先在運行時動態解析出調用點限定符所引用的方法,而後在執行該方法。
    • 和上面四個的區別
      • 以前的四個,分派邏輯是固化在Java虛擬機內部的
      • 這個指令的分派邏輯是由用戶本身所設定的引導方法決定的
  • 除了最後一個,其餘的四個的第一個參數都是被調用的方法的符號引用,是在編譯期就肯定的,因此缺少動態語言類型的支持。這也是Lambda表達式在1.8出現的緣由(1.7出現的這條語句奠基了他的基礎)

解析

是什麼

調用目標在程序代碼寫好,編譯器進行編譯時就必須肯定下來。這類方法的調用稱爲解析spa

還記得以前文章說的,在類加載的解析階段,會將一部分的符號引用轉化爲直接引用嗎,這種解析能成立的前提是:方法在程序真正運行以前就有一個可肯定的調用版本,而且這個版本在運行期間不可變

Java語言符合"編譯期可知,運行期不可變"這個要求的方法,主要有兩大類

  • 靜態方法
  • 私有方法
  • 前者和類型直接關聯,後者不可被外部訪問。這兩個特色就保證了他們不可能經過繼承或別的方式重寫其餘版本,因此適合類加載階段解析

咱們看上面的指令的前兩個invokestaticinvokespecial,只要能被這兩個指令調用的方法,均可以在解析階段肯定惟一的調用版本,因此符合這個條件的有四類

  • 靜態方法
  • 私有方法
  • 實例構造器
  • 父類方法

他們被稱爲 非虛方法,與之相對的,就被稱爲虛方法(final除外,由於他 不可繼承覆蓋,因此也只有一個調用版本,因此他也是非虛方法)

分派

分派按照調用多是靜態也可能動態,按照宗量數,可分爲單分派,多分派,所以組合一下,就有四種不一樣的分派

  • 靜態單分派
  • 靜態多分派
  • 動態單分派
  • 動態多分派

靜態分派

何爲靜態,就是發生在編譯期,因此肯定靜態分派的不是由虛擬機完成,而是編譯器,編譯器在編譯階段肯定版本。重載是典型的靜態分配

咱們看下這個代碼

package polymorphic;

public class StaticDispatch {
    static abstract class Human{

    }
    static class Man extends Human{

    }
    static class Woman extends Human{

    }

    public void sayHello(Man man){
        System.out.println("hello man");
    }
    public void sayHello(Woman woman){
        System.out.println("hello woman");
    }
    public void sayHello(Human human){
        System.out.println("hello guy");
    }

    public static void main(String[] args) {
        StaticDispatch dispatch = new StaticDispatch();
        Human man = new Man();
        dispatch.sayHello(man );
        Human woman = new Woman();
        dispatch.sayHello(woman );
    }
}
複製代碼

輸出是啥呢?

想解釋這個,得先知道兩個概念

  • 靜態類型(外觀類型)
  • 實際類型

以上面的爲例

區別:

  • 靜態類型的變化僅僅在使用時發生,自己的靜態類型不會發生變化,而且靜態類型是在編譯期可知的
  • 實際類型變化的結果,運行期纔可知,編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼

咱們一開始就說,靜態分派之因此稱爲靜態分派,是由於他在編譯期就肯定了,因此他就是經過靜態類型肯定的。

因此重載的時候,是按照靜態類型來肯定,而且是編譯期肯定。

動態分派

與靜態分派對應的是動態分派,經典的例子就是重寫

咱們來看下面的這個代碼

package polymorphic;

public class DynamicDispatch {
    static abstract class Human{
        public abstract void sayHello();
    }
    static class Man extends Human{

        @Override
        public void sayHello() {
            System.out.println("Hello Man");
        }
    }
    static class Woman extends Human{

        @Override
        public void sayHello() {
            System.out.println("Hello Woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
    }

}
複製代碼

咱們看他的輸出結果

這個顯然不是經過靜態類型來判斷兩個變量的行爲,由於兩個變量的靜態類型同樣啊,都是Human

咱們來看他們字節碼的內容

我兩次調用的語句也是同樣啊,是什麼致使最後的行爲不同呢?

咱們就要從調用語句invokevirtual來講起,他在運行時解析過程大體分爲如下幾個步驟

  1. 找到操做數棧頂的第一個元素所指向的對象的實際類型,記做C
  2. 若是在類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限的校驗,若是經過則返回這個方法的直接引用,查找結束;若是不經過,則返回java.lang.IllegalAccessError異常
  3. 不然,按照繼承關係從下往上以此對C的各個父類進行第2步的搜索和驗證過程
  4. 若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常

咱們看到他是找的實際類型,因此兩次調用invokevirtual指令把常量池中的符號引用解析到了不一樣的直接引用上,這個過程就是Java重寫的本質

而這種在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派

咱們再修改下代碼來試一下上面的狀況

package polymorphic;

public class DynamicDispatch {
    static abstract class Animal{
        public abstract void sayHello();

    }

    static  class Human extends Animal{

        @Override
        public void sayHello() {
            System.out.println("Hello Human");
        }
    }
    static class Man extends Human{

// @Override
// public void sayHello() {
// System.out.println("Hello Man");
// }
    }
    static class Woman extends Human{

        @Override
        public void sayHello() {
            System.out.println("Hello Woman");
        }
    }

    public static void main(String[] args) {
        Animal man = new Man();
        Animal woman = new Woman();
        man.sayHello();
        woman.sayHello();
    }

}
複製代碼

咱們建立了一個抽象類Animal,而且Human繼承自Animal,而Man和Woman繼承自Human。而後把Man中的重寫方法註釋掉。

咱們看下他的輸出結果

能夠看到他輸出的是他的父類,由於Man類中並無相關的方法。他就順着Man類的父類依次往上查找

那麼他是怎麼實現的呢?

這個放到後面再講,咱們先把分派的類型講完,由於後面的圖用他的例子(我懶得本身畫圖,哈哈)

單分派多分派

咱們先記住結論,目前爲止JDK1.8,Java是靜態多分派,動態單分派的語言

單分派多分派的劃分的標準是啥,是根據宗量來判斷,那麼啥是宗量呢?

方法的接收者方法的參數統稱爲方法的宗量

方法的接收者是啥?

  • 咱們看上面的動態分派或者靜態分派,動態分派中方法的接收者是實際類型或者是實際類型往上的父類

  • 而在靜態分派中,方法的接收者都是靜態類型

方法的參數是啥?

  • 方法的參數就是咱們一直說的方法的參數

  • 上面的都是單分派,由於我以前的例子無論動態仍是靜態分派。只有方法的接收者,沒有方法的參數。咱們看下下面的例子就知道了。

package polymorphic;

public class Dispatch {
    static class Soft {

    }
    static class QQ extends Soft {

    }
    static class _360 extends Soft {}
    public static class Father{
        public void hardChoice(QQ qq){
            System.out.println("father choose qq");
        }
        public void hardChoice(Soft soft){
            System.out.println("father choose soft");
        }
        public void hardChoice(_360 _360){
            System.out.println("father choose 360");
        }
    }
    public static class Son extends Father{

        public void hardChoice(QQ qq){
            System.out.println("son choose qq");
        }

        public void hardChoice(_360 _360){
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {

        Father father =new Father();
        Father son =new Son();

        QQ qq = new QQ();
        father.hardChoice(qq);
        son.hardChoice(new _360());

    }
}
複製代碼

咱們看他的輸出結果

那麼這個結果是怎麼產生的呢?

編譯期

咱們按照步驟來,先看編譯期編譯器的選擇,也就是靜態分派的過程。

這個時候編譯器會根據兩個宗量來進行判斷,因此他是靜態多分派

  • 靜態類型(Father仍是Son)
  • 方法參數(QQ仍是_360)

而後他會產生兩條invokevirtual語句,一條指向Father.hardChoice(QQ),一條指向Father.hardChoice(_360)

運行期

這個時候,咱們已經肯定了參數是QQ或者_360其中一個,所以虛擬機就不會再管參數了,這個時候肯定的只有實際類型究竟是Father仍是Son.

因此只有一個類型宗量。所以它是動態單分派

因此才輸出了上面的結果。由此回答了我一開始的那句話"截至目前(JDK1.8),Java是靜態多分派,動態單分派的語言"

可是我引用的是書上的例子,我以爲書上還有一點缺陷。他並無驗證編譯期的時候的參數選擇是根據那個類型來判斷的。我修改一下上面的代碼:

package polymorphic;

public class Dispatch {
    static class Soft {

    }
    static class QQ extends Soft {

    }
    static class _360 extends Soft {}
    public  static class Person{
        public void hardChoice(QQ qq){
            System.out.println("father choose qq");
        }
        public void hardChoice(Soft soft){
            System.out.println("father choose soft");
        }
        public void hardChoice(_360 _360){
            System.out.println("father choose 360");
        }
    }
    public static class Father extends Person{
        public void hardChoice(QQ qq){
            System.out.println("father choose qq");
        }
        public void hardChoice(Soft soft){
            System.out.println("father choose soft");
        }
        public void hardChoice(_360 _360){
            System.out.println("father choose 360");
        }
    }
    public static class Son extends Father{

        public void hardChoice(QQ qq){
            System.out.println("son choose qq");
        }
        public void hardChoice(Soft soft){
            System.out.println("son choose soft");
        }

        public void hardChoice(_360 _360){
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {

        Person father =new Father();
        Person son =new Son();

        Soft qq = new QQ();
        Soft _360 = new _360();
        father.hardChoice(qq);
        son.hardChoice(_360);

    }
}
複製代碼

你能夠看到我改動了三處,一處是參數類也有父類Soft,一處是Father和Son有父類Person。一處是Father和Son中添加了Soft參數的語句

咱們來看下他這樣子輸出的結果

咱們看到 最後的輸出語句是Soft,也就是說參數也是編譯期肯定的,而且是他的靜態類型Soft,而不是實際類型QQ或者_360。

經過個人這個例子你應該更清楚所謂的"靜態多分派,動態單分派"

動態分派的實現

因爲動態分派是很是頻繁的動做,並且動態分派的方法版本選擇過程須要運行時在類的元數據中搜素合適的目標方法,所以在虛擬機的實際實現中基於性能的考慮,大部分的實現都不會真正的進行如此頻繁的搜索

面對這種狀況,最經常使用的"穩定優化"手段就是爲類在方法區中創建一張虛方法表(Virtural Method Table,也稱"vtable"與此對應的,若是是接口,就是接口方法表 Interface Method Table,簡稱itable)。

咱們以上面的Father和Son爲例,(最開始的,不是我修改的)

那麼他是啥呢?

  • 虛方法表中存放着各個方法的實際入口地址

  • 若是某個方法在子類中沒有重寫

    • 那麼子類虛方法表中的地址入口和父類相同方法的的地址入口一致的
  • 若是子類重寫了

    • 子類方法表中的地址入口將會替換成子類實現版本的入口地址

咱們看上面的圖就能明白,Son重寫了來自Father的所有方法(clone這些是Object中的,不是Father的),因此Son的那些方法的入口地址沒有指向Father的。而他們都沒有重寫來自Object的方法,因此那些方法都指向Object方法的入口地址

若是咱們修改了代碼(註釋掉子類的hardChoice(QQ)方法)

public static class Son extends Father{

        //public void hardChoice(QQ qq){
        // System.out.println("son choose qq");
        //}

        public void hardChoice(_360 _360){
            System.out.println("son choose 360");
        }
    }
複製代碼

那麼上面的圖片就會變成這樣

爲了程序實現上的方便,具備相同簽名的方法,在父類、子類的虛方法表中都應當具備同樣索引序號

這樣當類型變換時,僅須要變動查找的方法表,就能夠從不一樣的虛方法表中按索引轉換出所需的入口地址

總結

  • 方法調用有兩種
    • 解析
    • 分派
  • 分派分爲
    • 靜態分派
      • 編譯期
      • 典型表明-重載
    • 動態分派
      • 運行期
      • 典型表明 -重寫
  • Java目前(JDK1.8,靜態多分派,動態單分派)
相關文章
相關標籤/搜索