計算機程式的思維邏輯 (15) – 初識繼承和多型

swiftma發表於2019-03-03

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (15) – 初識繼承和多型

繼承

上節我們談到,將現實中的概念對映為程式中的概念,我們談了類以及類之間的組合,現實中的概念間還有一種非常重要的關係,就是分類,分類有個根,然後向下不斷細化,形成一個層次分類體系。這種例子是非常多的:

在自然世界中,生物有動物和植物,動物有不同的科目,食肉動物、食草動物、雜食動物等,食肉動物有狼、狗、虎等,這些又分為不同的品種 …

開啟電商網站,在顯著位置一般都有分類列表,比如家用電器、服裝,服裝有女裝、男裝,男裝有襯衫、牛仔褲等 …

計算機程式經常使用類之間的繼承關係來表示物件之間的分類關係。在繼承關係中,有父類子類,比如動物類Animal和狗類Dog,Animal是父類,Dog是子類。父類也叫基類,子類也叫派生類,父類子類是相對的,一個類B可能是類A的子類,是類C的父類。

之所以叫繼承是因為,子類繼承了父類的屬性和行為,父類有的屬性和行為,子類都有。但子類可以增加子類特有的屬性和行為,某些父類有的行為,子類的實現方式可能與父類也不完全一樣。

使用繼承一方面可以複用程式碼,公共的屬性和行為可以放到父類中,而子類只需要關注子類特有的就可以了,另一方面,不同子類的物件可以更為方便的被統一處理

本節主要通過圖形處理中的一些簡單例子來介紹Java中的繼承,會介紹繼承的基本概念,關於繼承更深入的討論和實現原理,我們在後續章節介紹。

Object

在Java中,所有類都有一個父類,即使沒有宣告父類,也有一個隱含的父類,這個父類叫Object。Object沒有定義屬性,但定義了一些方法,如下圖所示:

計算機程式的思維邏輯 (15) – 初識繼承和多型

本節我們會介紹toString()方法,其他方法我們會在後續章節中逐步介紹。toString()方法的目的是返回一個物件的文字描述,這個方法可以直接被所有類使用。

比如說,對於我們之前介紹的Point類,可以這樣使用toString方法:

Point p = new Point(2,3);
System.out.println(p.toString()); 
複製程式碼

輸出類似這樣:

Point@76f9aa66
複製程式碼

這是什麼意思呢?@之前是類名,@之後的內容是什麼呢?我們來看下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());
複製程式碼

圖形處理類

接下來,我們以一些圖形處理中的例子來進一步解釋,先來看幅圖:

計算機程式的思維邏輯 (15) – 初識繼承和多型

這都是一些基本的圖形,圖形有線、正方形、三角形、圓形等,圖形有不同的顏色。接下來,我們定義以下類來說明關於繼承的一些概念:

  • 父類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 : black
12.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) – 初識繼承和多型

相關文章