Upcasting, downcasting 上下轉型

Upcasting, downcasting by Sinipull
源:http://forum.codecall.net/topic/50451-upcasting-downcasting/
譯:RainMeoCat

Class:類別
Object:物件
Mammal:哺乳動物

Upcasting和Downcasting是Java裡很重要的一部分,使我們能夠使用簡單的語法來建構複雜的程式,給我們帶來了很大的便利,例如多型(Polymorphism)或將不同的物件分組。Java允許將子類別的物件視為任何父類別的物件。稱為向上轉型(Upcasting)。向上轉型是自動完成的,而向下轉型必須手動完成,我將盡力解釋為什麼會這樣。

繼承 Inheritance


這是動物階層的簡化版。可以看到,貓和狗都是哺乳動物,都是動物的延伸,而動物是物件的延伸。繼承於物件是默默做的,我的意思是,Java會將每一個不是繼承於任何一個Class的Class自動繼承於物件,所以全部都是物件(除了基本型別(primitive))。


如果你想問:貓不是物件吧?因為他繼承了哺乳動物,而不是物件啊!
通過繼承,Cat獲得了其祖先擁有的所有屬性。而物件又是貓的祖父母,意味著貓就是個物件,同時也是動物、哺乳動物,就現實上來說,一個哺乳類動物擁有乳腺,而動物是種生物,那貓也就擁有乳腺,同時也是種生物。
對於程式設計師來說,我們不需要為每個可能是動物的生物編寫其具有健康狀態的屬性,我們只需要寫一次,接著透過繼承來讓每種動物擁有其屬性就可以了。
來看看以下的範例:

class Animal {
    int health = 100;
}

class Mammal extends Animal { }

class Cat extends Mammal { }

class Dog extends Mammal { }

public class Test {
    public static void main(String[] args) {
        Cat c = new Cat();
        System.out.println(c.health);
        Dog d = new Dog();
        System.out.println(d.health);
    }
}

執行程式,Console上可以看到兩個100,這是因為貓和狗都從Animal繼承了健康狀態的屬性。

Upcasting 與 downcasting

首先,你必須知道一件事,透過轉型,你實際上並沒有更改物件本身,而只是對其進行了不同的標記。
例如,如果你實作了一隻貓並將其向上轉型為動物,這個不會停止成為貓。它仍然是一隻貓,但是它被當作任何其他動物對待,並且它的貓的屬性被隱藏,直到再次將其向下轉型到貓為止。
讓我們看一下轉型前和轉型後的物件的程式碼:

Cat c = new Cat();
System.out.println(c);
Mammal m = c; // upcasting
System.out.println(m);

/*
This printed:
Cat@a90653
Cat@a90653
*/

正如所看到的,這個物件向上轉型後還是一隻貓,它並沒有變成哺乳動物,它只是被貼上哺乳動物的標籤,當成了哺乳動物,這是沒有問題的,因為貓是哺乳動物。

請注意,即使它們都是哺乳動物,也無法將貓轉型為狗。以下圖片可以使這句話更清楚點。

儘管向上轉型不需要程式設計師手動轉型,但還是可以這樣做,看到以下程式碼:
Mammal m = new Cat();
等於
Mammal m = (Mammal)new Cat();
但是向下轉型必須手動完成:

Cat c1 = new Cat();
Animal a = c1; //自動把Cat向上轉型為Animal
Cat c2 = (Cat) a; //手動向下轉型為Cat

為什麼要這樣做?好吧,你可以看到向上轉型是一定可以成功的,但是向下轉型就不一定了,如果你有一組動物,你想把他全部向下轉型為貓,那麼就會有可能這些動物中有一些是狗,這將導致錯誤並拋出ClassCastException錯誤資訊

這裡要介紹一個非常有用的函數,稱為instanceof,此函數用來測試物件是否為某類別的實例,
看到以下程式碼:

Cat c1 = new Cat();
Animal a = c1; //向上轉型為動物
if(a instanceof Cat){ // 測試他是否是一隻貓
    System.out.println("是一隻毛茸茸貓咪,現在我可以放心的向下轉型而不會產生任何錯誤了!");
    Cat c2 = (Cat)a;
}

請注意,這兩種方式不一定每次都能夠轉型成功,如果你使用new Mammal()實作一個Mammal物件,你無法將其轉型為Cat或Dog,因為Mammal都不是Cat或Dog(這裡並不是指動物的關係,而是物件間的絕對關係,詳見下面的解釋)
例如:

Mammal m = new Mammal();
Cat c = (Cat)m;

這段程式碼雖然能夠通過編譯,但是會在執行期間拋出java.lang.ClassCastException: Mammal cannot be cast to Cat錯誤,因為我嘗試把不是Cat的Mammal轉型成Cat。

轉型背後的大體思路是:哪個物件是哪個?你應該問:
貓是一種哺乳動物嗎?是的,所以他可以被轉型。
哺乳動物是一種貓嗎?不是,不能轉型。
貓是一種狗嗎?不是,不能轉型。


在這邊有一個很重要的點:不要把變數當成實例,一個Cat物件實作後存放於被編譯器視為Mammal的++變數++裡可被轉型為Cat,反之Mammal物件實作後存放於被編譯器視為Mammal的++變數++裡不可被轉型為Cat

貓不能咕嚕叫了,因為他被當成了其他東西

如果你向上轉型一個物件,它將丟失其所有屬性,這些屬性是從其轉型物件的所有子類別的屬性。
例如,如果將貓向上轉型為動物,它將失去哺乳動物和貓的屬性。請注意,在將對象向下轉型到對應類別之前,資料不會消失,只是無法使用。

為什麼會這樣呢?如果你有一組動物,則不能確定哪些動物可以喵喵叫和哪些可以汪汪叫。這就是為什麼你不能讓Animal做只有狗或貓能做的事情。

然而以上狀況並不是一個很大的障礙,如果你使用多型,其特性能夠在呼叫函數時自動向下轉型,我不會在這裡深入探討,但你可以參考Turk4n所撰寫的多型教學:http://forum.codecall.net/topic/49980-polymorphism/

調用函數時的向上轉型

型別轉換的美妙之處在於程式設計師可以使許多不同的類別作為參數來傳入方法(method)內
例如:

public static void stroke(Animal a){
    System.out.println("you stroke the "+a);
}

此方法可以將任何的Animal或其子類別作為參數。例如:

Cat c = new Cat();		 
Dog d = new Dog();		 
stroke(c); // automatic upcast to an Animal
stroke(d); // automatic upcast to an Animal

這個做法是沒有任何問題的。

然而當你有一個貓的物件,但是被一個為Animal的變數所持有,那麼這個變數也無法作為一個參數來傳入限定為Cat類別的方法,必須先向下轉型才可以完成。

關於變數

變數能儲存其類別或在其類別的子類別的實例,舉例來說
Cat c;其變數能夠存有Cat以及繼承其類別的子類別的實例,Animal能存有Animal、Mammal等。
要記住的是,存放在其中的實例將永遠向上轉型為其變數的類別層級。

可是我真的敲想用一隻貓實作出一隻狗!!!

好吧,你並沒辦法用轉型來達成這件事,然而,物件就只是方法與屬性實作出來的,也就是說你可以用Cat的屬性與方法來在裡面實作出一隻狗。
假設你現在有一個Cat Class:

class Cat extends Mammal {
    Color furColor;
    int numberOfLives;
    int speed;
    int balance;
    int kittens = 0;

    Cat(Color f, int n, int s, int B){
        this.furColor = f;
        this.numberOfLives = n;
        this.speed = s;
        this.balance = b;
    }
}

和一個Dog Class

class Dog extends Mammal {
    Color furColor;
    int speed;
    int barkVolume;
    int puppies = 0;

    Dog(Color f, int n, int s, int B){
        this.furColor = f;
        this.speed = s;
        this.barkVolume = b;
    }
}

現在你想要用Cat實作一個Dog,你只需要在Cat底下新增一個方法,並將其屬性轉為其Dog的屬性實作後回傳一個Dog:

public Dog toDog(int barkVolume){
    Dog d = new Dog(furColor, speed, barkVolume);
    d.puppies = kittens;
    return d;
}

可以看到,這兩個類別沒有完全匹配,有些屬性沒有轉換,有些屬性還需要在外部新增,請注意到,我沒有轉換numberOfLives和Balance,並且barkVolume是全新的資料。如果你有兩個類別完全相等,且完全能轉換,那麼歡呼吧!儘管這個情況非常罕見。

之後就可以用以下的程式碼進行轉換:

Cat c = new Cat(Color.black, 9, 20, 40);
Dog d = c.toDog(50);

感謝閱讀!