多態
對於某一個對象(事物),在不同的時刻體現出來的不同狀態叫做多態
如: 水的液態、固態和氣態
學生在上課時間上課,在吃飯時間吃飯,在休息的時候睡覺
在java中,對象變量是多態的,一個超類對象除了可以引用一個自身類對象,還可以引用它的派生類對象。通過繼承和方法重寫來實現多態。
對象間的轉型問題
在java中,每個對象變量都屬於一個類型,類型描述了這個變量所引用的以及能夠引用的對象類型, 將一個值存入變量時,編譯器會檢查是否允許該操作
1.向上轉型:將一個子類的引用賦給一個超類變量,編譯器是允許的,不用進行強制類型轉換。
格式: 超類 超類變量=new 子類();
2. 向下轉型:但是將一個超類的引用賦給 一個子類變量,必須進行強制類型轉換,這樣才能夠通過運行時的檢查
格式: 子類 子類對象變量名=(子類)父類對象引用
class Animal
{
public int age=40;
public void eat()
{
System.out.println("吃東西");
}
public void sleep()
{
System.out.println("睡覺");
}
}
class Cat extends Animal
{
public int age=5;
//重寫了父類的方法
public void eat()
{
System.out.println("貓吃老鼠");
}
public void playGame()
{
System.out.println("玩毛線球");
}
}
class Dog extends Animal
{
public int age=3;
//重寫了父類的方法
public void eat()
{
System.out.println("狗啃骨頭");
}
}
class AnimalTest
{
public static void main(String[] args)
{
//向上轉型:不需要強制類型轉換
Animal a=new Cat();
System.out.println(a.age); //調用的是父類的成員變量
a.eat(); //調用的是子類Cat的方法
//a.playGame(); //多態的弊端:無法訪問子類特有的方法 error:類型爲Animal的變量a找不到方法playGame()
a.sleep(); //調用的是子類Cat的方法
/*
結論:當超類類變量引用子類對象
訪問成員變量時:訪問的是父類的成員變量
訪問成員方法時:先訪問的是超類的此方法,如果此方法被子類覆蓋,則調用子類的方法。並且不能訪問子類特有的方法
*/
System.out.println("------------");
//向下轉型:需要強制類型轉換
Cat cat=(Cat)a;
System.out.println(cat.age); //訪問的是子類Cat的成員變量
cat.eat(); //訪問的是子類Cat的方法
cat.playGame(); //可以訪問子類Cat特有的方法
//Cat cat2=a; //error:Animal無法轉換爲Cat
System.out.println("------------");
//Dog dog=(Dog)a;
/*上條語句在編譯時並不會出現錯誤,這是因爲編譯器認爲超類變量a可能是Dog類,但是在運行時會拋出一個ClassCastException(類型轉換異常)
如果沒有捕獲這個異常,那麼程序就會異常終止,如何解決這個問題呢?
在進行類型轉換之前,先查看是否能夠成功的轉換,通過使用instanceof操作符來實現。如下
*/
if(a instanceof Dog)
{
Dog dog=(Dog)a;
System.out.println("可以進行轉換");
}
else
{
System.out.println("不可以進行轉換");
}
System.out.println("-----------instanceof 操作符-----------------");
System.out.println(a instanceof Dog);
System.out.println(a instanceof Cat);
System.out.println(null instanceof Cat);
System.out.println(a instanceof Animal);
}
}
instanceof (二元)操作符:
boolean result=Object instanceof class
參數:
- Object: 必選。任意對象表達式
- class: 必選。任意已定義的對象類
含義:如果Object是class的一個實例,則返回true,如果Object不是class的一個實例或者爲null時,返回false
運行結果
多態的好處:
1. 應用程序不必爲每一個派生類編寫功能調用,只需要對抽象超類進行處理即可。提高了程序的可複用性。(通過繼承來實現)
2. 派生類的功能可以被超類的方法或引用變量所調用,這叫向後兼容,可以提高可擴充性和可維護性。
多態的弊端:不能使用子類特有的方法
下面的例子可以看出使用多態時,在使用不同的Animal子類時,都利用了同一個Tool.eat()方法,提高了程序的複用性。同時,在創建新的子類時,也可以繼續使用Tool.eat()方法而不用進行修改,因此使用多態可以提高代碼的可擴充性和可維護性。
但是當使用父類對象變量引用子類對象時,派生類Cat特有的playGame()方法不能被調用。
class Animal
{
public int age=40;
public void eat()
{
System.out.println("吃東西");
}
public void sleep()
{
System.out.println("在晚上睡覺");
}
}
class Cat extends Animal
{
public int age=5;
//重寫了父類的方法
public void eat()
{
System.out.println("貓吃老鼠");
}
public void playGame()
{
System.out.println("貓喜歡玩毛線球");
}
}
class Dog extends Animal
{
public int age=3;
//重寫了父類的方法
public void eat()
{
System.out.println("狗啃骨頭");
}
}
class Pig extends Animal
{
public void eat()
{
System.out.println("豬吃白菜");
}
}
class Tool
{
public static void eat(Animal animal)
{
animal.eat();
animal.sleep();
}
}
class AnimalTest2
{
public static void main(String[] args)
{
Cat cat=new Cat();
Tool.eat(cat);
Dog dog=new Dog();
Tool.eat(dog);
Pig pig=new Pig();
Tool.eat(pig);
System.out.println("-------------------");
Animal a=new Cat();
//a.playGame(); //error:類型爲Animal 的變量a 找不到方法 playGame();
Tool.eat(a);
}
}
運行結果
多態的其他實現方式:
方法調用
如何調用方法呢?假設要調用x.f(args),Son是Father的子類
1.編譯器查看對象的聲明類型(即隱式參數類型)和方法名,獲得所有可能被調用的候選方法。(注:有可能存在着方法的重載,即有多個參數類型不一樣的方法)編譯器會列舉Son類中所有名爲f的方法和其超類中所有的非私有的名爲f方法,(超類的私有方法不可訪問)
2.編譯器查看調用方法時提供的參數類型。獲得方法名字和參數類型。如果在所有名爲f的方法中存在一個方法的參數類型與調用方法提供的實參類型匹配,就選擇這個方法,這個過程稱爲重載解析。這個過程是比較複雜的,如要考慮到參數類型轉換,如int 轉換爲double,Son轉換爲Father。如果編譯器沒有找到與參數類型相匹配的方法,或者發現經過類型轉換後有多個方法與之匹配,就會報告一個錯誤。
靜態綁定:如果是private方法、static方法、final方法或者構造器。那麼編譯器將可以準確的知道應該調用那個方法,我們將這種調用方式稱爲靜態綁定。
動態綁定:調用的方法依賴於隱式參數的實際類型,並且在運行時實現調用那個方法,我們稱這種調用方式爲動態綁定。
當程序運行,並採用動態綁定調用方法時,虛擬機一定調用與x所引用對象的實際類型最合適那個類的方法,假設x的實際類型是Son,它是Father的子類,如果Son類定義了方法f(String),就直接調用它,否則將在Son類的超類Father中尋找f(String),依次類推
方法表(method table):每次調用方法都要進行搜索,時間開銷非常大,因此,虛擬機事先爲每個類創建了一個方法表(method table)。其中列出了所有方法的簽名和實際調用的方法。這樣一來,在真正的調用方法的時候,虛擬機僅查找這個表就行了。
完整的過程;
Father s=new Father的子類(); //這裏Son還可以替換爲其他Father的子類
s.show();
1.編譯器查看s的聲明類型爲Father
2.編譯器將Father類方法表和其超類中所有的名爲show的方法列舉出來
3.編譯器查看調用方法時提供的參數類型。進行重載解析,如果在所有名爲show的方法中存在一個與提供的參數類型完全匹配, 就選擇這個方法。至此這個方法就已經找到了。
4.判斷該方法是靜態綁定還是動態綁定。
如果該方法是private方法、final方法或構造器,java虛擬機就調用這個方法。調用方法結束。
否則,在程序運行過程中,虛擬機提取s引用對象的實際類型的方法表。有可能是Son的方法表,也有可能是Father其他子類的方法表
5.假設s引用對象的實際類型是Son.
如果Son類定義了該方法,虛擬機就直接調用它。
否則在Son類的超類中尋找show()方法,依次類推。