大家應該都知道, 在C語言中是沒有重載的, 因為在編譯過程中編譯器只檢查函數(shù)名, 不管參數(shù)是否一致. 因此編譯器無法區(qū)分兩個同名函數(shù)的不同. 有時候在同一函數(shù)中需要不同入參會比較麻煩, 需要在方法結尾加上點標識來實現(xiàn)重載, 比如:
_syscall0(type,name)
_syscall1(type,name,type1,arg1)
_syscall2(type,name,type1,arg1,type2,arg2)
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)
_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)
_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)
而在Java和C#中, 編譯器允許在同一個類中的多個方法使用相同的方法名, 編譯時通過不同的參數(shù)類型列表來區(qū)分. 例如在下面的例子中非常方便的重載了構造器, 來實現(xiàn)多種構造方法:
class Tree(){
int height;
public Tree(){
height = 0;
}
public Tree(int initialHeight){
height = initialHeight;
}
}
這樣的寫法讓developer們不需要記一長串的方法名, 降低了編程的難度, 那么重載到底在JVM里面是如何實現(xiàn)的呢? 讓我們繼續(xù)深入~
Java在編譯時先將.java文件編譯成.class文件, 在class字節(jié)碼中, 方法簽名主要由三部分組成:
- 方法的標識(access_flag): public, private, static, final, synchronized, native等
- 方法的名稱(name)
- 方法的描述(descriptor), 包括方法的返回值類型和入參信息
這三個部分的內容可以唯一確定一個方法, 那么java重載的秘密也就揭開了, jvm在運行期間不僅檢查方法的名字, 同時還會區(qū)分入參和返回值.
我們可以實際看一下Tree.java 的字節(jié)碼文件來驗證一下, 用javap -verbose Tree.class 來解析Tree, 會發(fā)現(xiàn)這兩個構造器的字節(jié)碼是這樣的:
public Dao.Tree();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field height:I
9: return
LineNumberTable:
line 7: 0
line 8: 4
line 9: 9
public Dao.Tree(int);
descriptor: (I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field height:I
9: return
LineNumberTable:
line 11: 0
line 12: 4
line 13: 9
可以看到對于第一個構造方法, 標識為public, 名字為Dao.Tree(自己加了一個叫Dao的package...), 方法描述叫作()V.
而對于第二個構造方法, 標識為public, 名字同樣為Dao.Tree, 方法描述叫作(I)V. 這里返回值int之所以叫作I是因為JVM規(guī)范中規(guī)定了基本類型的簡寫, 這里的I就是int 的簡寫.
我們理解了方法重載的原理之后, 由此繼續(xù)深入思考下去, 照這么理解的話方法的返回值也會影響重載, 理論上只要編譯器可以確定一個方法的返回值是可以實現(xiàn)不同返回值的重載的. 但是要是出現(xiàn)如下情況的話編譯器就會無法判斷到底應該用哪個重載方法:
private int getHeight(String message){
System.out.println("return int!");
return height;
}
private Integer getHeight(String message){
System.out.println("return Integer!");
return height;
}
public static void main(String[] args) {
Tree tree = new Tree();
tree.getHeight("編譯器報錯");
}
getHeight(String)這個方法中, 因為返回值沒有賦值給任何變量, 因此編譯器無法判斷返回值, 此時編譯器報錯, 錯誤提示是該方法已經定義, 無法重復定義.
可以看到, 返回值是無法作為重載的區(qū)分的, 這不是因為JVM里面不能同時存在這兩個方法, 而是因為在某些情況下無法判斷返回值, 導致編譯器干脆直接不允許這樣寫(編譯器內心: 怪我嘍). 這樣的話想要實現(xiàn)返回值的重載就只能再寫一個不同名的方法了.
除此之外參數(shù)的順序也會決定函數(shù)的重載, 比如下面的兩個方法:
private int getHeight(String message, Integer a){
System.out.println(message + height);
return height;
}
private int getHeight(Integer a, String message){
return height;
}
字節(jié)碼為:
public int getHeight(java.lang.String, java.lang.Integer);
descriptor: (Ljava/lang/String;Ljava/lang/Integer;)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: aload_0
5: getfield #2 // Field height:I
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
13: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: aload_0
17: getfield #2 // Field height:I
20: ireturn
LineNumberTable:
line 16: 0
line 17: 16
public int getHeight(java.lang.Integer, java.lang.String);
descriptor: (Ljava/lang/Integer;Ljava/lang/String;)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=3, args_size=3
0: aload_0
1: getfield #2 // Field height:I
4: ireturn
LineNumberTable:
line 21: 0
可以看到, 入參不同順序也會生成兩個不同的方法, 唯一的問題是僅僅是依靠參數(shù)的順序來生成兩個不同的函數(shù)非常容易造成誤解, 一般要避免使用這樣的寫法.