2010年10月12日 星期二

Overloading VS. Overriding

剛學物件導向程式設計的人常常把Overriding與Overloading兩個詞搞混,不僅是因為這兩個單字看起來很像,連使用方式都很類似。於是愛用國貨的中文書市場為了造福國內廣大英文辨識力不良族群,推出了以下絕妙的通俗翻譯:
  • Overloading=多載。
  • Overriding=覆載。
這翻譯絕妙的地方在於繼承了英文單字看起來很類似的特性,從字面來看也很難明白其中意涵,於是還是讓人看得霧煞煞,令人不禁想豎起大拇指大讚「這就是物件導向啊!」
就在這個時候,國內的大學生發現這可能是教授為了能當更多人而玩的文字遊戲,又或者是當初的翻譯者害怕太多人學會之後飯碗不保,刻意翻得很奇怪,於是提出更明確的翻譯方式以自救:
  • Overloading = 給予太多工作 (load多到太over)。
  • Overriding =忤逆家長 (也就是ride在家長頭上太over的意思)。


給予太多工作,顧名思義就是給同一個人太大的工作量,讓他什麼事情都要包辦。在物件導向程式設計的世界裡,就是給予一個方法(method)或一個運算子(operator)多重任務的意思!最常見的例子就是Java中的加號運算子(+),可用在數字相加也可用在字串串接,相當於一個人揹了兩項任務。如下列程式碼,給予+號不同型態的運算元,就會進行不同的操作。

Code 1.
int i = 1+2+3;   // i = 6
String str = "Hello"+" World"; // str = "Hello World"

Java目前並不開放給程式員Overload運算子,只開放方法的Overloading。要在Java中Overload方法,只要使用同樣的名字與不同的參數形式宣告多個方法就行了,如下:

Code 2.
class Overloading{
    String hello(String str){
        return "Hello!";
}

    int hello(int i){
        return 100;
}

    String hello(String str1, String str2){
        return str1+str2;
}

    public static void main(String... arg){
        Overloading over = new Overloading();
     System.out.println(over.hello("Go!"));
      System.out.println(over.hello("Hello"," World"));
     System.out.println(over.hello(300));
}
}
執行結果
Hello!
Hello World
100

hello方法有三種宣告,分別接受不同型態與數量的參數。從呼叫方法的程式碼來看,像是我們讓hello方法處理三種不同的參數形式。然而實際上,這三個方法雖然有同樣的名稱,卻可以視為不同的方法。
原則上只要方法名稱一樣但方法簽名(Method Signature,即方法名稱加上參數形式)不同,就是合法的Overloading,回傳值型態則無所謂,呼叫方法的時候不要弄錯即可。實際上Java編譯器在編譯之後確實是產生3個不同的方法宣告。你可能會好奇這樣為什麼需要Overloading來擾亂視聽,其實沒有Overloading才真的很不方便,想像一下要替幾個功能一致只是參數形式不同的方法取名字的時候,光是要想出清楚卻又不能重複的名字,就讓人感到很挫折了。


忤逆家長
,顧名思義就是不理會家長的那一套,決心貫徹自己的做法!在物件導向設計的世界中,指的就是重新定義從父類別那繼承下來的方法。Overriding的概念比較難以文字解釋,因此話不多說,直接切入程式碼。
我們現在有一個父類別如下:
Code 3.
class Parent{
    protected Money work(){
        System.out.println("家長種田");
        return new Money(300);
    }
}

代表家長種田,每次賺300塊。Money類別內容如下:

Code 4.
class Money{
    private double money;
    public Money(double amount){
        this.money = amount;
}
public double getNTAmount(){
        return money;
}
}

現在這個家長生了個有志氣的孩子。這個孩子不想繼承家業,憑自己努力用功讀書,最終成為一個優秀的Java程式員。這個孩子雖然也要工作(work),卻不去繼承家長的工作方式,自己定義了work的實作,如下:
Code 5.
class Child extends Parent{
    public Money work(){
        System.out.println("兒子寫Java");
        return new Money(30);
    }
}

像這樣改寫從父類別繼承下來的方法,就是忤逆家長,就是Overriding!

Overriding的限制比較多,除了方法名稱必須要與「被忤逆」的方法一樣之外,方法的回值型態與參數形式也必須與父類別一模一樣(否則就變成Overloading)!另外,方法的存取權限只能和父類別的方法一樣或更開放。也就是說,若父類別方法是使用protected權限,子類別想要「忤逆」該方法的話,權限就只能是protected或public。還有一點得注意:Final的方法不予許「被忤逆」。
值得一提的是,Java 5 開始有一種例外情況予許子類別「忤逆」的方法的傳回值型態可以是「被忤逆」的方法的回傳值型態的子類別。簡單的說,Child類別的work方法的回傳值型態不一定要是Money類別,也可以是Money的子類別。這就叫共變回傳(Covarient Return)。
舉例來說,假如那個孩子成為Java程式員之後,因為勤奮工作,被主管推薦到矽谷的總公司上班,薪水也從台幣換成美金,我們就定義一個美金(USDollar)類別繼承Money類別:

Code 6.
class USDollar extends Money{
    public USDollar(double amount){
        super(amount);
}
    public double getNTAmount(){
        return super.getNTAmount()*32;
}
}

並且將Child中work方法的回傳值型態改為USDollar:
Code 7.
class Child extends Parent{
    public USDollar work(){
        System.out.println("兒子寫Java");
     return new USDollar(30);
}
}

由於USDollar是Money的子類別,因此在Java 5之後能夠作為「忤逆」方法的合法回傳值型態。
關於共變回傳,還是不太熟悉的人可以參考這篇文章 http://tw.knowledge.yahoo.com/question/question?qid=1508092008302
也許有人會覺得要記那麼多原則很煩,但其實萬變不離其宗,大原則就是子類別必須能被當作父類別使用(如Code 8)。這就是著名的Liskov代換原則(LSP)。假設子類別的「忤逆」方法的存取權限、參數形式或傳回值型態與父類別「被忤逆」的方法不相容,如何確保子類別能被當作父類型型態使用呢?這也就是為何「忤逆」方法的存取權限必須與「被忤逆」的方法一樣或更開放,回傳值型態也必須要能相容的原因。

Code 8.
class Main{
    public static void main(String... arg){
        Parent p = new Child(); //子類別實體當父類別用
     Money money = p.work();
     System.out.println("薪水:"+money.getNTAmount());
}
}
執行結果
兒子寫Java
薪水:960.0



在像Java或C++這種靜態語言中使用Overloading與Overriding會有一定的風險在,因為兩者很容易混淆,導致執行結果可能不是程式員所預期的。例如下列程式碼:

Code 9.
import java.util.*;
class Problem{
    public static void main(String... arg){
        List ‹String›  list = new LinkedList‹ String › ();
     list.add("Hello");
     list.add("World");
     list.remove(0);
     ((Collection)list).remove(0);
     System.out.println("length"+ list.size());
}
}

猜猜最後list中會有多少個元素?
答案是:1個

你一定會疑惑增加兩個元素後又刪除兩個元素,為什麼結果不是個空串列。這就要看看List介面的remove方法是Overloading還是Overriding了。Collection介面固然提供了一個remove方法:
Collection
boolean remove(Object o)

然而List介面不只繼承了Collection的remove,還Overload了另一個remove方法:
List
boolean remove(Object o)
E remove(int index)

因此當list.remove(0)被呼叫時,實際上是呼叫到List介面Overload的另一個remove方法(因為參數型態最接近),而不是定義在Collection中的remove方法。然而,當list被轉型為Collection型態並呼叫remove方法時,由於Collection介面並沒有定義接收int型態參數的remove方法,因此參數0會被Auto-boxing成Integer型態進而與Object型態相容(所有型態都間接或直接繼承Object類別),所以Collection介面的remove方法還是會被呼叫。但由於兩個remove方法行為不同(一個是移除Collection中的o元素,一個是移除List中第index個元素),導致結果不是我們所預期的空串列。這種情況下,若沒有去查JavaDoc看看Collection與List的差別,會以為List介面的remove方法是Overriding而不是Overloading,進而完全找不到問題出在哪裡,因此在使用上必須注意這點。

如果擔心在Overriding時因為參數型態打錯而變成Overloading,可以在方法前加上@Override這樣的Annotation。@Override會通知編譯器在編譯時確認該方法是否真的有Overriding。以上面的Child類別為例:
List
class Child extends Parent{
//打錯參數形式,造成Overloading
    @Override
    public USDollar work(String str){
        System.out.println("兒子寫Java");
     return new USDollar(30);
}
}
編譯訊息
method does not override or implement a method from a supertype
@Override
^
1 error

萬一打錯字變成Overloading就無法編譯,可以節省很多因為打錯字而浪費掉的debug的時間!

6 則留言: