深入理解Java中方法重載的實現原理


一、前言

  今天看《深入理解Java虛擬機》這本書的時候,看到了其中對方法重載(Overload)以及方法重寫(Override)的原理講解,頓時有了恍然大悟之感。這篇博客我就來參考書中的內容,講一講方法重載的實現原理。


二、正文

 2.1 什么是方法重載

  講重載的實現原理之前,還是先來說一說什么是方法重載。Java中的每一個方法,都有自己的簽名,或者也可以叫做標識,用來確認它的唯一性。在同一個類中,不能出現兩個簽名一樣的方法。而方法的簽名由什么組成呢?答案是方法名稱 + 參數列表,也就是說,一個類中不允許出現兩個方法名稱一樣,而且方法的參數列表也一樣的方法(一個static,一個非static也不行)。知道上面的概念后,我們就可以定義方法重載了:在同一個類中,擁有相同方法名稱,但是不同參數列表的多個方法,被稱為重載方法,這種形式被稱為方法的重載。例如下面幾個方法,就是重載的方法,它們擁有相同的名稱,但是參數列表不同:

void test(int a) {
    System.out.println("type int");
}

void test(String a) {
    System.out.println("type String");
}

void test(String arg1, int arg2){
    System.out.println("String + int");
}

void test(int arg1, String arg2){
    System.out.println("int + String");
}

  需要注意的是,參數列表的不同指的是參數的數量不同,或者在參數數量相同的情況下,相同位置的參數類型不同,比如上面最后兩個方法,雖然參數都是一個String,一個int,但是位置不同,所以也是允許的。可以注意到,最后兩個方法的參數名稱都是arg1arg2,且位置相同,但是並不影響,因為方法的簽名和參數的名稱無關,只和類型有關。

  最后需要注意的一點是,返回值並不能作為方法的重載條件,比如下面兩個方法:

// 無返回值
void test(int a) {
    System.out.println("type int");
}

// 返回值為int
int test(int a) {
    return a;
}

  若一個類中同時出現以下兩個方法,將會編譯錯誤,因為它們的方法名稱+參數列表是一致的,編譯器無法識別。為什么返回值不能作為重載的依據呢?很簡單,因為我們調用方法時,並不一定需要接收方法的返回值,比如下面這行代碼,對於上面兩個方法都是適用的,編譯器無法確定選擇哪一個:

public static void main (String[]args){
    test(1);
}

 2.2 如何選擇調用哪一個重載方法

  當出現多個重載的方法時,編譯器如何決定調用哪一個被重載的方法呢?相信很多人都知道,是根據調用方法時傳遞的實際參數類型來確定。比如說最開始列舉的四個test方法,如果我們使用test(1),那將調用void test(int a)這個方法;如果我們使用test("aaa"),那將調用void test(String a)這個方法。這個應該很好理解,編譯器在編譯期間,根據調用方法的實際參數類型,就能夠確定具體需要調用的哪一個方法。但是,這只是一種簡單的情況,下面來看看一種稍微復雜的情況,即繼承關系下的方法重載(看完后先猜猜輸出結果):

public class Main {
	// 聲明一個父類
    static class Human {
    }
    // 聲明兩個子類
    static class Man extends Human {
    }
    static class Woman extends Human {
    }

    // 三個重載方法,參數類型分別為以上三種類型
    static void sayHello(Human human){
        System.out.println("human say Hello");
    }
    static void sayHello(Man man){
        System.out.println("man say Hello");
    }
    static void sayHello(Woman woman){
        System.out.println("woman say Hello");
    }

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

  以上代碼的輸出結果如下:

human say Hello
human say Hello

  根據結果可以看到,最終都調用了參數為父類型MansayHello方法。這是為什么呢?這是因為對重載方法的選擇,是根據變量的靜態類型來確定的,而不是實際類型。比如代碼Human man = new Man()Human就是變量man的靜態類型,而Man是它的實際類型。我們都知道,在多態的情況下調用方法,會根據實際類型調用實際對象的方法,但是在重載中,是根據靜態類型來確定調用哪一個方法的。在上面的代碼中,manwoman對象的靜態類型都是Human,所以都調用static void sayHello(Human human)方法。和調用重寫方法不同,由於一個對象的靜態類型在編譯期間就可以確定,所以調用哪個重載方法是在編譯期就確定好了,這叫靜態分派,而調用重寫的方法卻要在運行時才能確定具體類型,這叫動態分派


 2.3 重載調用的優先級

  接下來,我們再來看一個更加復雜的情況,如下代碼:

public class Test {

    static void sayHello(char arg) {
        System.out.println("hello, char");
    }

    static void sayHello(int arg) {
        System.out.println("hello, int");
    }

    static void sayHello(long arg) {
        System.out.println("hello, long");
    }

    static void sayHello(Character arg) {
        System.out.println("hello, Character");
    }

    static void sayHello(Serializable org) {
        System.out.println("hello, Serializable");
    }

    static void sayHello(Object arg) {
        System.out.println("hello, object");
    }

    static void sayHello(char... org) {
        System.out.println("hello, char...");
    }

    public static void main(String[] args) {
        sayHello('a');
    }
}

  上面對sayHello方法重載了七次,這七個重載方法都只有一個參數,但是參數的類型各不相同。在main方法中,我們調用sayHello方法,並傳入一個字符'a',結果不出意料,輸出如下:

"hello, char"

  這個結果應該不會有意外,畢竟'a'就是一個字符,調用參數為char的方法合情合理。接着,我們將sayHello(char arg)方法注釋掉,再來看看運行結果:

"hello, int"

  當參數為char的方法被注釋后,編譯器選擇了參數為int的方法。這也不難理解,這里發生了自動類型轉換,將字符a轉換成了它的Unicode編碼(97),因此調用sayHello(int arg)是合適的。接着,我們將sayHello(int arg)也注釋掉,看看輸出結果:

"hello, long"

  這時候調用了參數類型為long的方法,也就是說這里發生了兩次轉換,先將a轉換成int類型的97,再將97轉換為long類型的97L,接着再調用相應的方法。上面的代碼中我沒有寫參數為floatdouble的方法,不然這種轉換還會繼續,而順序是char->int->long->float->double。但是不會被轉換成byteshort,因為這不是安全的轉換,byte只有一個字節,而char有兩個字節,所以不行;而short雖然有兩個字節,但是有一半是負數,char的編碼不存在負數,所以也不行。好了,接下來我們將sayHello(long arg)也注釋,看看結果:

"hello, Character"

  根據結果可以發現,這里發生了一次自動裝箱,將a封裝成了一個Character對象,然后調用了相應的方法。這也是合情合理的。然后,我們再注釋sayHello(Character arg)方法,再次運行:

"hello, Serializable"

  先在這個結果就有一點迷惑了,這么連Serializable都行?這是因為Character類實現了Serializable接口,也就是說這里發生了兩次轉換,先將'a'封裝成Character對象,再轉型成為它的父類型Serializable。所以,當我們調用重載的方法時,如果不存在對應的類型,則編譯器會從下往上,依次尋找當前類型的父類型,直到找到第一個父類型滿足某一個重載方法為止,若直到最后都沒有找到,就會編譯錯誤。Character類實現了兩個接口,一個是Serializable,一個是Comparable<Character>,如果同時存在這兩個參數類型的重載方法,編譯器將會報錯,因為這兩個類型是同級別的,不知道該選擇哪一個。這種情況下,我們可以使用顯示的類型轉換,來選擇需要調用的方法。好了,我們現在將sayHello(Serializable org) 也注釋,看看結果:

"hello, object"

  可以看到,這時候調用了參數類型為Object的重載方法。這正好驗證了我們上面說的結論——從下往上尋找父類型的重載方法,因為Object就是所有類的父類(除了Object本身)。然后,我們再注釋sayHello(Object arg)

"hello, char..."

  可以看到,調用了可變參數類型的方法,這時候的a被當成了一個數組元素。所以,可變成參數類型的優先級是最低的。如果此時還有一個sayHello(int... org),則在注釋完sayHello(char... org)后,將調用它,正好又對應上了我們前面說的 char->int->long->float->double的順序,這個順序在可變長類型中也適用。

  說到這里,我們應該能夠明白,在方法調用有多個選擇的情況下,編譯器總是會根據優先級,選擇最適合的那個。而關於這個優先級如何決定,可以去看看Java語言規范,其中對這部分做了詳細規定。


三、總結

  說了這么多,最關鍵的一點還是:重載是根據變量的靜態類型進行選擇的。只要理解了這一點,對於重載也就很容易弄懂了。最后還要說一點,無論對重載理解有多么深刻,想最后一個例子中這樣模棱兩可的代碼還是不要寫為好,畢竟可(rong)讀(yi)性(ai)太(da)差了。希望這篇博客對想要了解重載的人有所幫助吧。


四、參考

  • 《深入理解Java虛擬機》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM