標籤:
繼承
上節我們談到,將現實中的概念映射為程式中的概念,我們談了類以及類之間的組合,現實中的概念間還有一種非常重要的關係,就是分類,分類有個根,然後向下不斷細化,形成一個層次分類體系。這種例子是非常多的:
在自然世界中,生物有動物和植物,動物有不同的科目,食肉動物、食草動物、雜食動物等,食肉動物有狼、狗、虎等,這些又分為不同的品種 ...
開啟電商網站,在顯著位置一般都有分類列表,比如家用電器、服裝,服裝有女裝、男裝,男裝有襯衫、牛仔褲等 ...
電腦程式經常使用類之間的繼承關係來表示對象之間的分類別關係。在繼承關係中,有父類和子類,比如動物類Animal和狗類Dog,Animal是父類,Dog是子類。父類也叫基類,子類也叫衍生類別,父類子類是相對的,一個類B可能是類A的子類,是類C的父類。
之所以叫繼承是因為,子類繼承了父類的屬性和行為,父類有的屬性和行為,子類都有。但子類可以增加子類特有的屬性和行為,某些父類有的行為,子類的實現方式可能與父類也不完全一樣。
使用繼承一方面可以複用代碼,公用的屬性和行為可以放到父類中,而子類只需要關注子類特有的就可以了,另一方面,不同子類的對象可以更為方便的被統一處理。
本節主要通過圖形處理中的一些簡單例子來介紹Java中的繼承,會介紹繼承的基本概念,關於繼承更深入的討論和實現原理,我們在後續章節介紹。
Object
在Java中,所有類都有一個父類,即使沒有聲明父類,也有一個隱含的父類,這個父類叫Object。Object沒有定義屬性,但定義了一些方法,如所示:
本節我們會介紹toString()方法,其他方法我們會在後續章節中逐步介紹。toString()方法的目的是返回一個對象的文本描述,這個方法可以直接被所有類使用。
比如說,對於我們之前介紹的Point類,可以這樣使用toString方法:
Point p = new Point(2,3);System.out.println(p.toString());
輸出類似這樣:
[email protected]
這是什麼意思呢?@之前是類名,@之後的內容是什麼呢?我們來看下toString的代碼:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
getClass().getName() 返回當前對象的類名,hashCode()返回一個對象的雜湊值,雜湊我們會在後續章節中介紹,這裡可以理解為是一個整數,這個整數預設情況下,通常是對 象的記憶體位址值,Integer.toHexString(hashCode())返回這個雜湊值的16進位表示。
為什麼要這麼寫呢?寫類名是可以理解的,表示對象的類型,而寫雜湊值則是不得已的,因為Object類並不知道具體對象的屬性,不知道怎麼用文本描述,但又需要區分不同對象,只能是寫一個雜湊值。
但子類是知道自己的屬性的,子類可以重寫父類的方法,以反映自己的不同實現。所謂重寫,就是定義和父類一樣的方法,並重新實現。
Point類 - 重寫toString()
我們再來看下Point類,這次我們重寫了toString()方法。
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public double distance(Point point){ return Math.sqrt(Math.pow(this.x-point.getX(),2) +Math.pow(this.y-point.getY(), 2)); } public int getX() { return x; } public int getY() { return y; } @Override public String toString() { return "("+x+","+y+")"; }}
toString方法前面有一個 @Override,這表示toString這個方法是重寫的父類的方法,重寫後的方法返回Point的x和y座標的值。重寫後,將調用子類的實現。比如,如下代碼的輸出就變成了:(2,3)
Point p = new Point(2,3);System.out.println(p.toString());
圖形處理類
接下來,我們以一些圖形處理中的例子來進一步解釋,先來看幅圖:
這都是一些基本的圖形,圖形有線、正方形、三角形、圓形等,圖形有不同的顏色。接下來,我們定義以下類來說明關於繼承的一些概念:
- 父類Shape,表示圖形。
- 類Circle,表示圓。
- 類Line,表示直線。
- 類ArrowLine,表示帶箭頭的直線。
圖形 (Shape)
所有圖形都有一個表示顏色的屬性,有一個表示繪製的方法,下面是代碼:
public class Shape { private static final String DEFAULT_COLOR = "black"; private String color; public Shape() { this(DEFAULT_COLOR); } public Shape(String color) { this.color = color; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public void draw(){ System.out.println("draw shape"); }}
以上代碼基本沒什麼可解釋的,執行個體變數color表示顏色,draw方法表示繪製,我們不會寫實際的繪製代碼,主要是示範繼承關係。
圓 (Circle)
圓繼承自Shape,但包括了額外的屬性,中心點和半徑,以及額外的方法area,用於計算面積,另外,重寫了draw方法,代碼如下:
public class Circle extends Shape { //中心點 private Point center; //半徑 private double r; public Circle(Point center, double r) { this.center = center; this.r = r; } @Override public void draw() { System.out.println("draw circle at " +center.toString()+" with r "+r +", using color : "+getColor()); } public double area(){ return Math.PI*r*r; }}
說明幾點:
- Java使用extends關鍵字標明繼承關係,一個類最多隻能有一個父類。
- 子類不能直接存取父類的私人屬性和方法,比如,在Circle中,不能直接存取shape的私人執行個體變數color。
- 除了私人的外,子類繼承了父類的其他屬性和方法,比如,在Circle的draw方法中,可以直接調用getColor()方法。
看下使用它的代碼:
public static void main(String[] args) { Point center = new Point(2,3); //建立圓,賦值給circle Circle circle = new Circle(center,2); //調用draw方法,會執行Circle的draw方法 circle.draw(); //輸出圓面積 System.out.println(circle.area());}
程式的輸出為:
draw circle at (2,3) with r 2.0, using color : black12.566370614359172
這裡比較奇怪的是,color是什麼時候賦值的?在new的過程中,父類的構造方法也會執行,且會優先於子類先執行。在這個例子中,父類Shape的預設構造方法會在子類Circle的構造方法之前執行。關於new過程的細節,我們會在後續章節進一步介紹。
直線 (Line)
線繼承自Shape,但有兩個點,有一個擷取長度的方法,另外,重寫了draw方法,代碼如下:
public class Line extends Shape { private Point start; private Point end; public Line(Point start, Point end, String color) { super(color); this.start = start; this.end = end; } public double length(){ return start.distance(end); } public Point getStart() { return start; } public Point getEnd() { return end; } @Override public void draw() { System.out.println("draw line from " + start.toString()+" to "+end.toString() + ",using color "+super.getColor()); }}
這裡我們要說明的是super這個關鍵字,super用於指代父類,可用於調用父類構造方法,訪問父類方法和變數:
- 在line構造方法中,super(color)表示調用父類的帶color參數的構造方法,調用父類構造方法時,super(...)必須放在第一行。
- 在draw方法中,super.getColor()表示調用父類的getColor方法,當然不寫super.也是可以的,因為這個方法子類沒有同名的,沒有歧義,當有歧義的時候,通過super.可以明確表示調用父類的。
- super同樣可以引用父類非私人的變數。
可以看出,super的使用與this有點像,但super和this是不同的,this引用一個對象,是實實在在存在的,可以作為函數參數,可以作為傳回值,但super只是一個關鍵字,不能作為參數和傳回值,它只是用於告訴編譯器訪問父類的相關變數和方法。
帶箭頭直線 (ArrowLine)
帶箭頭直線繼承自Line,但多了兩個屬性,分別表示兩端是否有箭頭,也重寫了draw方法,代碼如下:
public class ArrowLine extends Line { private boolean startArrow; private boolean endArrow; public ArrowLine(Point start, Point end, String color, boolean startArrow, boolean endArrow) { super(start, end, color); this.startArrow = startArrow; this.endArrow = endArrow; } @Override public void draw() { super.draw(); if(startArrow){ System.out.println("draw start arrow"); } if(endArrow){ System.out.println("draw end arrow"); } }}
ArrowLine繼承自Line,而Line繼承自Shape,ArrowLine的對象也有Shape的屬性和方法。
注意draw方法的第一行,super.draw()表示調用父類的draw()方法,這時候不帶super.是不行的,因為當前的方法也叫draw()。
需要說明的是,這裡ArrowLine繼承了Line,也可以直接在類Line裡加上屬性,而不需要單獨設計一個類ArrowLine,這裡主要是示範繼承的層次性。
圖形管理器
使用繼承的一個好處是可以統一處理不同子類型的對象,比如說,我們來看一個圖形管理者類,它負責管理畫板上的所有繪圖物件並負責繪製,在繪製代碼中,只需要將每個對象當做Shape並調用draw方法就可以了,系統會自動執行子類的draw方法。代碼如下:
public class ShapeManager { private static final int MAX_NUM = 100; private Shape[] shapes = new Shape[MAX_NUM]; private int shapeNum = 0; public void addShape(Shape shape){ if(shapeNum<MAX_NUM){ shapes[shapeNum++] = shape; } } public void draw(){ for(int i=0;i<shapeNum;i++){ shapes[i].draw(); } }}
ShapeManager使用一個數組儲存所有的shape,在draw方法中調用每個shape的draw方法。ShapeManager並不知道每個shape具體的類型,也不關心,但可以調用到子類的draw方法。
我們來看下使用ShapeManager的一個例子:
public static void main(String[] args) { ShapeManager manager = new ShapeManager(); manager.addShape(new Circle(new Point(4,4),3)); manager.addShape(new Line(new Point(2,3), new Point(3,4),"green")); manager.addShape(new ArrowLine(new Point(1,2), new Point(5,5),"black",false,true)); manager.draw();}
建立了三個shape,分別是一個圓、直線和帶箭頭的線,然後加到了shape manager中,然後調用manager的draw方法。
需要說明的是,在addShape方法中,參數Shape shape,聲明的類型是Shape,而實際的類型則分別是Circle,Line和ArrowLine。子類對象賦值給父類引用變數,這叫向上轉型,轉型就是轉換類型,向上轉型就是轉換為父類類型。
變數shape可以引用任何Shape子類類型的對象,這叫多態,即一種類型的變數,可引用多種實際類型對象。這樣,對於變數shape,它就有兩個類型,類型Shape,我們稱之為shape的靜態類型,類型Circle/Line/ArrowLine,我們稱之為shape的動態類型。在ShapeManager的draw方法中,shapes[i].draw()調用的是其對應動態類型的draw方法,這稱之為方法的動態綁定。
為什麼要有多態和動態綁定呢?建立對象的代碼 (ShapeManager以外的代碼)和操作對象的代碼(ShapeManager本身的代碼),經常不在一起,操作對象的代碼往往只知道對象是某種父類型,也往往只需要知道它是某種父類型就可以了。
可以說,多態和動態綁定是電腦程式的一種重要思維方式,使得操作對象的程式不需要關注對象的實際類型,從而可以統一處理不同對象,但又能實現每個對象的特有行為。後續章節我們會進一步介紹動態綁定的實現原理。
小結
本節介紹了繼承和多態的基本概念:
- 每個類有且只有一個父類,沒有聲明父類的其父類為Object,子類繼承了父類非private的屬性和方法,可以增加自己的屬性和方法,可以重寫父類的方法實現。
- new過程中,父類先進行初始化,可通過super調用父類相應的構造方法,沒有使用super的話,調用父類的預設構造方法。
- 子類變數和方法與父類重名的情況下,可通過super強制訪問父類的變數和方法。
- 子類對象可以賦值給父類引用變數,這叫多態,實際執行調用的是子類實現,這叫動態綁定。
但關於繼承,還有很多細節,比如執行個體變數重名的情況。另外,繼承雖然可以複用代碼,便於統一處理不同子類的對象,但繼承其實是把雙刃劍,使用不當,也有很多問題。讓我們下節來討論這些問題,而關於繼承和多態的實現原理,讓我們再下節來討論。
----------------
未完待續,查看最新文章,敬請關注公眾號“老馬說編程”(掃描下方二維碼),從入門到進階,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。原創文章,保留所有著作權。
電腦程式的思維邏輯 (15) - 初識繼承和多態