前言
面向對象有三大特徵:封裝、繼承、多態。
封裝隱藏了類的內部實現機制,可以在不影響使用者的前提下改變類的內部結構,繼承是爲了重用父類代碼,而多態呢?今天我就談談自己對多態的理解。
多態
多態是指同一消息可以根據發送對象的不同而採用多種不同的行爲方
式。多態具有以下幾個優點:
1. 消除類型之間的耦合關係
2. 可替換性
3. 可擴充性
4. 接口性
5. 靈活性
6. 簡化性
多態存在的三個必要條件:繼承、重寫、父類引用指向子類對象
多態的形式:
Parent p = new Child();
向上轉型
要理解多態,首先需要了解向上轉型。例如我定義了一個Shape類,子類Circle繼承自Shape類,實例化一個Circle對象,可以這樣表示
Shape s = new Circle();
簡單來說,就是:父類引用指向子類對象。
那麼向上轉型有啥好處呢?首先我們來看看如果沒有向上轉型:
public class Shape {
public void draw(){
System.out.println("draw shape");
}
}
public class Circle extends Shape{
public void draw(){
System.out.println("draw circle");
}
}
public class Square extends Shape{
public void draw(){
System.out.println("draw square");
}
}
public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Circle c){
c.draw();
}
public void draw(Square s){
s.draw();
}
}
最後將打印
draw circle
這麼做是可以的,但是有個主要缺點,若我們需要添加一個新的Shape子類,則必須要在Painter中添加一個新的draw()方法,若遇到需要大量Shape子類工作的情況呢,這個將變爲很糟糕,因此,多態就很好地幫我們解決了這個問題。
若使用多態,Painter類只需要這樣設計。
public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Shape s){
s.draw();
}
}
當Circle實例傳給draw()時,draw()會將Circle實例當做Shape對象,因此對Shape所做的任何操作都將被Circle所接收到。當然,這也是有前提的,Shape的子類必須重寫
Shape的方法。若子類沒有重寫父類的方法,則最終會調用的是父類中的方法,因此最好將抽象的部分設爲抽象方法,這樣子類在繼承的時候若沒有重寫,編譯器將會報錯。
綁定
我們只需要子類重寫父類方法,在需要的時候將子類實例傳給父類引用,便可完成向上轉型。那麼編譯器是如何區分傳給父類引用的是哪個子類實例呢,其實編譯器是一直不知道對象的類型,但JAVA提供了一種解決辦法,後期綁定,也就是在運行時根據對象的類型進行綁定。因此後期綁定也叫動態綁定或運行時綁定。
《JAVA編程思想》中提到,Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有的方法都是動態綁定。這意味着通常情況下,我們不必判定是否應該進行後期綁定。若將方法設爲final類型,不僅可以防止其他人重寫該方法,也可以有效地”關閉”動態綁定。
動態綁定內部機制
爲了提高動態分派時方法查找的效率,JVM 會在鏈接類的過程中,給類分配相應的方法表內存空間。每個類對應一個方法表。
一個類的方法表包含類的所有方法入口地址,從父類繼承的方法放在前面,接下來是接口方法和自定義的方法。當我們調用某個方法時,JVM會從方法表中查找相應的方法,其過程如下:
- 首先編譯器確定對象的聲明類型和方法名。然後找當前類中方法名字匹配的所有方法(由於重載,可能存在多個),然後在其父類中也找類似的屬性爲public的方法;
- 編譯器查看調用方法的參數類型,先在本類中找,然後在超類中找,這一過程稱爲重載解析(overloading resolution)。若沒找到,或在同一個類中找到多個,均報錯。
- 若爲private、static或者final修飾的方法,爲靜態綁定,可直接知道調用的是哪個方法,此情況下就省去了剩下的步驟;
- 在程序運行時,JVM會根據對象的實際類型從方法表中調用最合適的方法。
可擴展性
由於引入了多態機制,我們在對現有的代碼進行擴展時,而不需要修改現有的方法。還是以Shape爲例,向其添加一個size()方法,並在子類中實現該方法,即使如此,我們也不必修改Painter中draw()方法,原代碼依然可以穩健運行。具體實現如下:
public class Shape {
public void draw(){
System.out.println("draw shape");
}
public void size(){
//TODO
}
}
public class Circle extends Shape{
public void draw(){
System.out.println("draw circle");
}
public void size(){
//TODO
}
}
public class Square extends Shape{
public void draw(){
System.out.println("draw square");
}
public void size(){
//TODO
}
}
public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Shape s){
s.draw();
}
}
這個例子很好地體現了多態的特性,我們對代碼所做的修改,不會對程序中其他不應受到影響的部分產生破壞。
向下轉型類型判斷
由於向上轉型會丟失具體的類型信息,比如Shape的子類Circle中有額外的color()方法,將Circle實例轉爲Shape類型,這樣做是安全的,因爲父類不會具有大於子類的接口,因此通過父類調用的方法都是可行的。
而對於向下轉型,我們無法知道一個父類會轉爲哪個子類類型,因此也無法確保被調用的方法是那個類中所含有的。如下所示:
public class Shape {
}
public class Circle extends Shape{
public void color(){
System.out.println("paint yellow");
}
}
public class Square extends Shape{
public void size(){
System.out.println("40 x 40");
}
}
public class Painter{
public static void main(String[] args){
Shape shape = new Circle();
Square square = (Square)shape;
square.size(); // ClassCastException
}
}
將Shape實例強轉爲Square類型,編譯器是不會報錯的,因爲Square是Shape的子類。當用強轉後的Square實例調用Circle中的color()方法,編譯器就會報一個ClassCastException錯誤。
爲解決上述問題,我們可以使用 ’instanceof關鍵字‘ 來確保不會出現ClassCastException錯誤。
將Painter改爲:
public class Painter{
public static void main(String[] args){
Shape shape = new Circle();
if(shape instanceof Square){
Square square = (Square)shape;
square.size();
}
}
}
寫在最後
本菜鳥也跟隨潮流,開通了基於HEXO的個人博客。歡迎各位關注:https://pomelojiang.github.io/
參考
- 《JAVA編程思想》
- 深入理解JVM方法調用的內部機制