一、前言
今天看《深入理解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
,但是位置不同,所以也是允許的。可以注意到,最后兩個方法的參數名稱都是arg1
和arg2
,且位置相同,但是並不影響,因為方法的簽名和參數的名稱無關,只和類型有關。
最后需要注意的一點是,返回值並不能作為方法的重載條件,比如下面兩個方法:
// 無返回值
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
根據結果可以看到,最終都調用了參數為父類型Man
的sayHello
方法。這是為什么呢?這是因為對重載方法的選擇,是根據變量的靜態類型來確定的,而不是實際類型。比如代碼Human man = new Man()
,Human
就是變量man
的靜態類型,而Man
是它的實際類型。我們都知道,在多態的情況下調用方法,會根據實際類型調用實際對象的方法,但是在重載中,是根據靜態類型來確定調用哪一個方法的。在上面的代碼中,man
和woman
對象的靜態類型都是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
,接着再調用相應的方法。上面的代碼中我沒有寫參數為float
和double
的方法,不然這種轉換還會繼續,而順序是char->int->long->float->double
。但是不會被轉換成byte
和short
,因為這不是安全的轉換,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虛擬機》