日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

java用继承编写宠物乐园_MoreThanJavaDay 5:面向对象进阶继承详解

發布時間:2025/3/19 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java用继承编写宠物乐园_MoreThanJavaDay 5:面向对象进阶继承详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之后,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高質量的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支持是我前進的最大的動力!

Part 1. 繼承概述

上一篇文章 中我們簡單介紹了繼承的作用,它允許創建 具有邏輯等級結構的類體系,形成一個繼承樹。

Animal 繼承樹

繼承使您可以基于現有類定義新類。新類與現有類相似,但是可能具有其他實例變量和方法。這使編程更加容易,因為您可以 在現有的類上構建,而不必從頭開始。

繼承是現代軟件取得巨大成功的部分原因。程序員能夠在先前的工作基礎上繼續發展并不斷改進和升級現有軟件。

面向對象之前,寫代碼的一些問題

如果你有一個類的源代碼,你可以復制代碼并改變它變成你想要的樣子。在面向對象編程之前,就是這樣子做的。但至少有兩個問題:

? 很難保持僅僅有條。

假設您已經有了幾十個需要的類,并且需要基于原始類創造新的一些類,再基于新的類創造出更新的類,最終您將獲得數十個源文件,這些源文件都是通過其他已更改的源文件的另外版本。

假設現在在一個源文件中發現了錯誤,一些基于它的源文件需要進行修復,但是對于其他源文件來說,并不需要!沒有細致的寫代碼的計劃,您最終會陷入混亂....

? 需要學習原始代碼。

假設您有一個復雜的類,基本上可以完成所需的工作,但是您需要進行一些小的修改。如果您修改了源代碼,即使是進行了很小的更改,也可能會破壞某些內容。因此,您必須研究原始代碼以確保所做的更改正確,這可能并不容易。

Java 的自動繼承機制極大地緩解了這兩個問題。

單繼承

用于作為新類模板的類稱為 父類 (或超類或基類),基于父類創建的類稱為 子類 (或派生類)。

就像上圖中演示的那樣,箭頭從子類指向父類。(在上圖中,云表示類,而矩形表示對象,這樣的表示的方法來自于 Grady Booch 寫的《面向對象的分析和設計》一書。而在官方的 UML-統一建模語言 中,類和對象都用矩形表示,請注意這一點)

在 Java 中,子類僅從一個父類繼承特征,這被稱為 單繼承 (與人類不同)。

有些語言允許"孩子"從多個"父母"那里繼承,這被稱為 多繼承。但由于具有多重繼承,有時很難說出哪個父母為孩子貢獻了哪些特征 (跟人類一樣..)。

Java 通過使用單繼承避免了這些問題。(意思 Java 只允許單繼承)

is-a 關系

上圖顯示了一個父類 (Video 視頻類),一個子類 (Movie 電影類)。它們之間的實線表示 "is-a" 的關系:電影是視頻。

注意,繼承是在類之間,而不是在對象之間。 (上圖兩朵云都代表類)

父類是構造對象時使用的藍圖,子類用于構造看起來像父對象的對象,但具有附加功能的對象。

類之間的關系簡述

簡單地說,類和類之間的關系有三種:is-a、has-a 和 use-a。

  • is-a 關系也叫繼承或泛化,比如學生和人的關系、手機和電子產品的關系都屬于繼承關系;
  • has-a 關系通常稱之為關聯,比如部門和員工的關系、汽車和引擎的關系都屬于關聯關系;關聯關系如果是整體和部分的關聯,那么我們稱之為 聚合關系;如果整體進一步負責了部分的生命周期 (整體和部分是不可分割的,同時同在也同時消亡),那么這種就是最強的關聯關系,我們稱之為 合成 關系。
  • use-a 關系通常稱之為依賴,比如司機有一個駕駛的行為 (方法),其中 (的參數) 使用到了汽車,那么司機和汽車的關系就是依賴關系。

利用類之間的這些關系,我們可以在已有類的基礎上來完成某些操作,也可以在已有類的基礎上創建新的類,這些都是實現代碼復用的重要手段。復用現有的代碼不僅可以減少開發的工作量,也有利于代碼的管理和維護,這是我們在日常工作中都會使用到的技術手段。

層級結構

上圖顯示了一個父類和一個子類的 層次結構,以及從每個類構造的一些對象。這些對象用矩形表示,以表達它們比設計的類更真實。

在層次結構中,每個類最多有一個父類,但可能有幾個子類。層次結構頂部的類沒有父級。此類稱為層次結構的

另外,一個類可以是另一個子類的父類,也可以是父類的子類。就像人類一樣,一個人是某些人類的孩子,也是其他人類的父母。(但在 Java 中,一個孩子只有一個父母)

Part 2. 繼承的實現

從父類派生子類的語法是使用 extend 關鍵字:

class?ChildClass?extend?ParentClass?{
????//?子類的新成員和構造函數....
}

父類的成員 (變量和方法) 通過繼承包含在子類中。其他成員將在其類定義中添加到子類。

視頻觀影 App 示例

Java 編程是通過創建類層次結構并從中實例化對象來完成的。您可以擴展自己的類或擴展已經存在的類。Java 開發工具包 (JDK) 為您提供了豐富的基類集合,您可以根據需要擴展這些基類。

(如果某些類已經使用 final 修飾,則無法繼承)

下面演示了一個使用 Video 類作為基類的視頻觀影 App 的程序設計:

Video 基類:

class?Video?{

????private?String?title;???//?name?of?video
????private?int?length;?????//?number?of?minutes

????//?constructor
????public?Video(String?title,?int?length)?{
????????this.title?=?title;
????????this.length?=?length;
????}

????public?String?toString()?{
????????return?"title="?+?title?+?",?length="?+?length;
????}

????public?String?getTitle()?{?return?title;}
????public?void?setTitle(String?title)?{?this.title?=?title;}
????public?int?getLength()?{?return?length;}
????public?void?setLength(int?length)?{?this.length?=?length;}
}

Movie 電影類繼承 Video:

class?Movie?extends?Video?{

????private?String?director;//?name?of?the?director
????private?String?rating;??//?num?of?rating

????//?constructor
????public?Movie(String?title,?int?length,?String?director,?String?rating)?{
????????super(title,?length);
????????this.director?=?director;
????????this.rating?=?rating;
????}

????public?String?getDirector()?{?return?director;?}
????public?String?getRating()?{?return?rating;?}
}

這兩個類均已定義:Video 類可用于構造視頻類型的對象,現在 Movie 類可用于構造電影類型的對象。

Movie 類具有在 Video 中定義的成員變量和公共方法。

使用父類的構造函數

查看上方的示例,在 Movie 類的初始化構造函數中有一條 super(title, length); 的語句,是 "調用父類 Video 中帶有 title、length 參數的構造器" 的簡寫形式。

由于 Movie 類的構造器不能訪問 Video 類的私有字段,所以必須通過一個構造器來初始化這些私有字段。可以利用特殊的 super 語法調用這個構造器。

重要說明:super() 必須是子類構造函數中的第一條語句。 (這意味子類構造器總是會先調用父類的構造器) 這件事經常被忽略,導致的結果就是一些神秘的編譯器錯誤消息。

如果子類的構造器沒有顯式地調用父類的構造器,將自動地調用父類的無參構造器。如果父類沒有無參數的構造器,并且在子類的構造器中又沒有顯式地調用父類的其他構造器,Java 編譯器就會報告一個錯誤。(在我們的例子中 Video 缺少無參數的構造函數,故?上面圖片代碼會報錯)

創建一個無參構造函數

關于構造函數的一些細節:

  • 您可以顯式為類編寫無參數的構造函數。
  • 如果您沒有為類編寫任何構造函數,那么將自動提供無參數構造函數 (稱為默認構造函數)。
  • 如果為一個類編寫了一個構造函數,則不會自動提供默認的構造函數。
  • 因此:如果您為類編寫了額外的構造函數,那么,則還必須編寫一個無參數構造函數 (供子類調用)。
  • 在示例程序中,類 Video 包含構造函數,因此不會自動提供默認構造函數。所以,Movie 類 super() 函數建議默認使用的構造函數 (會自動調用無參數構造函數) 會導致語法錯誤。

    解決方法是將無參數構造函數顯式放在類中 Video ,如下所示:

    class?Video?{

    ????private?String?title;???//?name?of?video
    ????private?int?length;?????//?number?of?minutes

    ????//?no-argument?constructor
    ????public?Video()?{
    ????????this.title?=?"unknown";
    ????????this.length?=?0;
    ????}

    ????//?constructor
    ????public?Video(String?title,?int?length)?{
    ????????this.title?=?title;
    ????????this.length?=?length;
    ????}

    ????...
    }

    覆蓋方法

    讓我們來實例化 Movie 對象:

    public?class?Tester?{

    ????public?static?void?main(String[]?args)?{
    ????????Video?video?=?new?Video("視頻1",?90);
    ????????Movie?movie?=?new?Movie("悟空傳",?139,?"郭子健",?"5.9");
    ????????System.out.println(video.toString());
    ????????System.out.println(movie.toString());
    ????}
    }

    程序輸出:

    title=視頻1, length=90
    title=悟空傳, length=139

    movie.toString() 是 Movie 類直接繼承自 Video 類,它并沒有使用 Movie 對象具有的新變量,因此并不會打印導演和評分。

    我們需要給 Movie 類添加新的 toString() 的使用方法:

    //?添加到?Movie?類中
    public?String?toString()?{
    ????return?"title:"?+?getTitle()?+?",?length:"?+?getLength()?+?",?director:"?+?getDirector()
    ????????+?",?rating:"?+?getRating();
    }

    現在,Movie 擁有了自己的 toString() 方法,該方法使用了繼承自 Video 的變量和自己定義的變量。

    即使父類有一個 toString() 方法,子類中新定義的 toString() 也會 覆蓋 父類的版本。當子類方法的 簽名 (就是返回值 + 方法名稱 + 參數列表) 與父類相同時,子類的方法就會 覆蓋 父類的方法。

    現在運行程序,Movie 打印出了我們期望的完整信息:

    title=視頻1, length=90
    title:悟空傳, length:139, director:郭子健, rating:5.9

    有些人認為 super 與 this 引用是類似的概念,實際上,這樣比較并不太恰當。這是因為 super 不是一個對象的引用,例如,不能將值 super 賦給另一個對象變量,它只是一個指示編譯器調用父類方法的特殊關鍵字。

    正像前面所看到的那樣,在子類中可以增加字段、增加方法或覆蓋父類的方法,不過,繼承絕對不會刪除任何字段或方法。

    Part 3. 更多細節

    protected 關鍵字

    如果類中創建的變量或者方法使用 protected 描述,則指明了 "就類用戶而言,這是 private 的,但對于任何繼承于此類的導出類或者任何位于同一個 內的類來說,它是可以訪問的"。下面我們就上面的例子來演示:

    public?class?Video?{
    ????protected?String?title;???//?name?of?video
    ????protected?int?length;?????//?number?of?minutes
    ????...
    }

    public?class?Movie?extends?Video?{
    ????...
    ????public?String?toString()?{
    ????????return?"title:"?+?title?+?",?length:"?+?length?+?",?director:"?+?director
    ????????????+?",?rating:"?+?rating;
    ????}
    ????...
    }

    在 protected 修飾之前,如果子類 Movie 要訪問父類 Video 的 title 私有變量只能通過父類暴露出來的 getTitle() 公共方法,現在則可以直接使用。

    向上轉型

    "為新的類提供方法" 并不是繼承技術中最重要的方面,其最重要的方面是用來表現新類和基類之間的關系。這種關系可以用 "新類是現有類的一種類型" 這句話加以概括。

    由于繼承可以確保基類中所有的方法在子類中也同樣有效,所以能夠向基類發送的所有信息也同樣可以向子類發送。例如,如果 Video 類具有一個 play() 方法, 那么 Movie 類也將同樣具備。這意味著我們可以準確地說 Movie 對象也是一種類型的 Video。(體現 is-a 關系)

    這一概念的體現用下面的例子來說明:

    public?class?Video?{
    ????...
    ????public?void?play()?{}
    ????public?static?void?start(Video?video)?{
    ????????//?...
    ????????video.play();
    ????}
    ????...
    }

    //?測試類
    public?class?Tester?{
    ????public?static?void?main(String[]?args)?{
    ????????Movie?movie?=?new?Movie("悟空傳",?139,?"郭子健",?"5.9");
    ????????Video.start(movie);
    ????}
    }

    在示例中,start() 方法可以接受 Video 類型的引用,這是在太有趣了!

    在測試類中,傳遞給 start() 方法的是一個 Movie 引用。鑒于 Java 是一個對類型檢查十分嚴格的語言,接受某種類型 (上例是 Video 類型) 的方法同樣可以接受另外一種類型 (上例是 Movie 類型) 就會顯得很奇怪!

    除非你認識到 Movei 對象也是一種 Video 對象

    在 start() 方法中,程序代碼可以對 Video 和它所有子類起作用,這種將 Movie 引用轉換為 Video 引用的動作,我們稱之為 向上轉型 (這樣稱呼是因為在繼承樹的畫法上,基類在子類的上方...)。

    Object 類

    所有的類均具有父類,除了 Object 類。Java 類層次結構的最頂部就是 Object 類。

    如果類沒有顯式地指明繼承哪一個父類,那么它會自動地繼承自 Object 類。如果一個子類繼承了一個父類,那么父類要么繼承它的父類,要么自動繼承 Object。最終,所有的類都將 Object 作為祖先。

    這意味著 Java 中的所有類都具有一些共同的特征。這些特征在被定義在 Object 中:

    Object 類擁有的方法

    (其中 finalize() 方法在 Java 9 之后棄用了,原因是因為它本身存在一些問題,可能導致性能問題:死鎖、掛起和其他問題...)

    (想看源碼可以打一個 Object,然后按住 Ctrl 不放,然后點擊 Object 就可以進入 JDK 源碼查看了,源碼有十分規范的注釋和結構,你有時甚至會發現一些有趣的東西...)

    Java 之父 Gosling 設計的 Object 類,是對萬事萬物的抽象,是在哲學方向上進行的延伸思考,高度概括了事物的自然行為和社會行為。我們都知道哲學的三大經典問題:我是誰?我從哪里來?我到哪里去?在 Object 類中,這些問題都可以得到隱約的解答:

  • 我是誰? getClass() 說明本質上是誰,而 toString() 是當前的名片;
  • 我從哪里來? Object() 構造方法是生產對象的基本方式,clone() 是繁殖對象的另一種方式;
  • 我到哪里去? finalize() 是在對象銷毀時觸發的方法;(Java 9 之后已移除)
  • 另外,Object 還映射了社會科學領域的一些問題:

  • 世界是否因你而不同? hashCode() 和 equals() 就是判斷與其他元素是否相同的一組方法;
  • 與他人如何協調? wait() 和 notify() 就是對象間通信與協作的一組方法;
  • 理解方法調用

    準確地理解如何在對象上應用方法調用非常重要。下面假設我們要調用 x.f(args),x 是聲明為 C 的一個對象。下面是調用過程的詳細描述:

  • 編譯器查看對象的聲明類型和方法名。需要注意的是:有可能存在多個名字為 f 但參數類型不一樣的方法。例如,可能存在 f(int) 和 f(String)。編譯器將會一一列舉 C 類中所有名為 f 的方法和其父類中所有名為 f 而且可以訪問的方法 (父類中的私有方法不可訪問)。至此,編譯器一直到所有可能被調用的候選方法。
  • 接下來,編譯器要確定方法調用中提供的參數類型。如果在所有名為 f 的方法中存在一個與所提供參數類型完全匹配的方法,就選擇這個方法。這個過程稱為 重載解析 (overloading resolution)。例如,對于調用 x.f("Hello"),編譯期將會挑選 f(String),而不是 f(int)。由于允許類型轉換 (例如,int 可以轉換成 double),所以情況可能會變得很復雜。如果編譯器沒有找到與參數類型匹配的方法,或者發現經過類型轉換后有多個方法與之匹配,編譯器就會報錯。至此,編譯器已經知道需要調用的方法的名字和參數類型。
  • 如果是 private 方法、static 方法、final 方法 (有關 final 修飾符會在下面講到) 或者構造器,那么編譯器將可以明確地知道應該調用哪個方法。這稱為 靜態綁定 (static binding)。與此對應的是,如果要調用的方法依賴于隱式參數的實際類型,那么必須在運行時 動態綁定。在我們的實例中,編譯器會利用動態綁定生成一個調用 f(String) 的指令。
  • 程序運行并且采用動態綁定調用方法時,虛擬機必須調用與 x 所引用對象的實際類型對應的那個方法。假設 x 的實際類型是 D,它是 C 類的子類。如果 D 類定義了方法 f(String),就會調用這個方法;否則,將在 D 類的父類中尋找 f(String),以此類推。
  • 每次調用方法都要完成這樣的搜索,時間開銷相當大。因此,虛擬機預先為每個類計算了一個 方法表 (method table), 其中列出了所有方法的簽名和要調用的實際方法 (存著各個方法的實際入口地址)。這樣一來,在真正調用方法的時候,虛擬機僅查找這個表就行了。(以下是 Video-父類 和 Movie-子類 的方法表結構演示圖)

    例如我們調用上述例子 Movie 類的 play() 方法。

    public?void?play()?{};

    由于 play() 方法沒有參數,因此不必擔心 重載解析 的問題。又不是 private/ static/ final 方法,所以將采用 動態綁定 的方式。

    在運行時,調用 object.play() 的解析過程為:

  • 首先,虛擬機獲取 object 的實際類型的方法表。這可能是 Video、Movie 的方法表,也可能是 Video 類的其他子類的方法表;
  • 接下來,虛擬機查找定義了 play() 簽名的類。此時,虛擬機已經知道應該調用哪個方法了;(這里如果 object 實際類型為 Movie 則調用 Movie.play(),為 Video 則調用 Video.play(),如果沒找到才往父類去找..)
  • 最后,虛擬機調用這個方法。
  • 動態綁定有一個非常重要的特性:無須對現有的代碼進行修改就可以對程序進行擴展。

    假設現在新增一個類 ShortVideo,并且變量 object 有可能引用這個類的對象,我們不需要對包含調用 object.play() 的代碼重新進行編譯。如果 object 恰好引用一個 ShortVideo 類的對象,就會自動地調用 object.play() 方法。

    警告:在覆蓋一個方法時,子類的方法 不能低于 父類方法的 可見性 (public > protected > private)。特別是,如果父類方法是 public,子類方法必須也要聲明為 public。

    final 關鍵字

    有時候,我們可能希望組織人們利用某個類定義子類。不允許擴展 (被繼承) 的類被稱為 final 類。如果在定義類的時候使用了 final 修飾符就表明這個類是 final 類了:

    public?final?class?ShortVideo?extends?Video?{?...?}

    類中的某個特定方法也可以被聲明為 final。如果這樣做,子類就不能覆蓋這個方法 (final 類中的所有方法自動地稱為 final 方法)。例如:

    public?class?Video?{
    ????...
    ????public?final?void?Stop()?{?...?}
    ????...
    }

    如果一個 字段 被聲明為了 final 類型,那么對于 final 字段來說,構造對象之后就不允許改變它們的值了。不過,如果將一個類聲明為 final,只有其中的方法自動地稱為 final,而不包括字段,這一點需要注意。

    將方法或類聲明為 final 的主要原因是:確保它們不會在子類中改變語義。

    JDK 中的例子

    • Calendar 類 (JDK 實現的日歷類) 中的 getTime 和 setTime 方法都聲明為了 final,這就表明 Calendar 類的設計者負責實現 Data 類與日歷狀態之間的轉換,而不允許子類來添亂。
    • 同樣的,String 類也是 final 類 (甚至面試中也經常出現),這意味著不允許任何人定義 String 的子類,換而言之,如果有一個 String 引用,它引用的一定是一個 String 對象,而不可能是其他類的對象。

    內聯

    在早起的 Java 中,有些程序員為了避免動態綁定帶來的系統開銷而使用 final 關鍵字。如果一個方法沒有被覆蓋并且很短,編譯器就能夠對它進行優化處理,這個過程為 內聯 (inlining)。

    例如,內聯調用 e.getName() 會被替換為訪問字段 e.name。

    這是一項很有意義的改進,CPU 在處理當前指令時,分支會擾亂預取指令的策略,所以,CPU 不喜歡分支。然而,如果 getName 在另外一個類中 被覆蓋,那么編譯器就無法知道覆蓋的代碼將會做什么操作,因此也就不能對它進行內聯處理了。

    幸運的是,虛擬機中的 即時編譯器 (JIT) 比傳統編譯器的處理能力強得多。這種編譯器可以準確地知道類之間的繼承關系,并能夠檢測出是否有類確實覆蓋了給定的方法。

    如果方法很短、被頻繁調用而且確實沒有被覆蓋,那么即時編譯器就會將這個方法進行內聯處理。如果虛擬機加載了另外一個子類,而這個子類覆蓋了一個內聯方法,那么優化器將取消對這個方法的內聯。這個過程很慢,不過很少會發生這種情況。

    抽象類

    在類的自下而上的繼承層次結構中,位于上層的類更具有一般性,也更加抽象。從某種角度看,祖先類更具有一般性,人們通常只是將它作為派生其他類的基類,而不是用來構造你想使用的特定的實例。

    考慮一個 Person 類的繼承結構:

    每個人都有一些屬性,如名字。學生與員工都有名字。

    現在,假設需要增加一個 getDescription() 的方法,它返回對一個人簡短的描述,學生類可以返回:一個計算機在讀的學生,員工可以返回 一個在阿里就職的后端工程師 之類的。這在 Student 和 Employee 類中實現很容易,但是在 Person 類中應該提供什么內容呢?除了姓名,Person 類對這個人一無所知。

    有一個更好的方法,就是使用 abstract 關鍵字,把該方法定義為一個 抽象方法,這意味著你并不需要實現這個方法,只需要定義出來就好了:(以下代碼為 Person 類中的抽象定義)

    public?abstract?String?getDescription()?{}

    為了提高程序的清晰度,包含一個或多個抽象方法的類本身必須被聲明為抽象的:

    public?abstract?class?Person?{
    ????...
    ????public?abstract?String?getDescription()?{}
    ????...
    }

    《阿里Java開發規范》強制規定抽象類命名 使用 Abstract 或 Base 開頭,這里只是做演示所以就簡單用 Person 代替啦~

    抽象方法充當著占位方法的角色,它們在子類中被繼承并實現。

    擴展抽象類可以由兩種選擇。一種是在子類中保留抽象類中的部分或所有抽象方法仍未實現,這樣就必須將子類標記為抽象類 (因為還有抽象方法);另一種做法就是實現全部方法,這樣一來,子類就不是抽象的了。

    (即使不包含抽象方法,也可以將類聲明為抽象類)

    抽象類不能實例化,也就是說,如果將一個類聲明為 abstract,就不能創建這個類的實例,例如:new Person(); 就是錯誤的,但可以創建具體子類的對象:Person p = new Student(args);,這里的 p 是一個抽象類型 Person 的變量,它引用了一個非抽象子類 Student 的實例。

    Part 4. 為什么不推薦使用繼承?

    先別急著奇怪和憤懣,剛學習完繼承之后,就告訴說不推薦使用,這是 有原因的!

    在面向對象編程中,有一條非常經典的設計原則:組合優于繼承。使用繼承有什么問題?組合相比繼承有哪些優勢?如何判斷該用組合還是繼承?下面我們就圍繞這三個問題,來詳細講解一下。

    以下內容大部分引用自:https://time.geekbang.org/column/article/169593

    使用繼承有什么問題?

    上面說到,繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關系,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過復雜,也會影響到代碼的可維護性。我們通過一個例子來說明一下。

    假設我們要設計一個關于鳥的類,我們將 “鳥類” 這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

    我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否定的。盡管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。當然,你可能會說,我在鴕鳥這個子類中重寫 (override) fly() 方法,讓它拋出 UnSupportedMethodException 異常不就可以了嗎?具體的代碼實現如下所示:

    public?class?AbstractBird?{
    ??//...省略其他屬性和方法...
    ??public?void?fly()?{?//...?}
    }

    public?class?Ostrich?extends?AbstractBird?{?//鴕鳥
    ??//...省略其他屬性和方法...
    ??public?void?fly()?{
    ????throw?new?UnSupportedMethodException("I?can't?fly.'");
    ??}
    }

    這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對于這些不會飛的鳥來說,我們都需要重寫 fly() 方法,拋出異常。

    這樣的設計,一方面,徒增了編碼的工作量;另一方面,也違背了我們之后要講的最小知識原則 (Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

    你可能又會說,那我們再通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關系如下圖所示:

    從圖中我們可以看出,繼承關系變成了三層。不過,整體上來講,目前的繼承關系還比較簡單,層次比較淺,也算是一種可以接受的設計思路。我們再繼續加點難度。在剛剛這個場景中,我們只關注“鳥會不會飛”,但如果我們關注更多的問題,例如 “鳥會不會叫”、”鳥會不會下單“ 等... 那這個時候,我們又該如何設計類之間的繼承關系呢?

    總之,繼承最大的問題就在于:繼承層次過深、繼承關系過于復雜會影響到代碼的可讀性和可維護性。這也是為什么我們不推薦使用繼承。那剛剛例子中繼承存在的問題,我們又該如何來解決呢?

    組合相比繼承有哪些優勢?

    實際上,我們可以利用組合 (composition)、接口、委托 (delegation) 三個技術手段,一塊兒來解決剛剛繼承存在的問題。

    我們前面講到接口的時候說過,接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口 (相當于定義某一種行為,下方會有代碼說明),只讓會飛的鳥去實現這個接口。對于會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。我們將這個設計思路翻譯成 Java 代碼的話,就是下面這個樣子:

    public?interface?Flyable?{
    ??void?fly();
    }
    public?interface?Tweetable?{
    ??void?tweet();
    }
    public?interface?EggLayable?{
    ??void?layEgg();
    }
    public?class?Ostrich?implements?Tweetable,?EggLayable?{//鴕鳥
    ??//...?省略其他屬性和方法...
    ??@Override
    ??public?void?tweet()?{?//...?}
    ??@Override
    ??public?void?layEgg()?{?//...?}
    }
    public?class?Sparrow?impelents?Flayable,?Tweetable,?EggLayable?{//麻雀
    ??//...?省略其他屬性和方法...
    ??@Override
    ??public?void?fly()?{?//...?}
    ??@Override
    ??public?void?tweet()?{?//...?}
    ??@Override
    ??public?void?layEgg()?{?//...?}
    }

    不過,我們知道,接口只聲明方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layEgg() 方法,并且實現邏輯是一樣的,這就會導致代碼重復的問題。那這個問題又該如何解決呢?

    我們可以針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。然后,通過 組合和委托 技術來消除代碼重復。具體的代碼實現如下所示:

    public?interface?Flyable?{
    ??void?fly();
    }public?class?FlyAbility?implements?Flyable?{
    ??@Override
    ??public?void?fly()?{?//...?}
    }
    //省略Tweetable/TweetAbility/EggLayable/EggLayAbility

    public?class?Ostrich?implements?Tweetable,?EggLayable?{//鴕鳥
    ??private?TweetAbility?tweetAbility?=?new?TweetAbility();?//組合
    ??private?EggLayAbility?eggLayAbility?=?new?EggLayAbility();?//組合
    ??//...?省略其他屬性和方法...
    ??@Override
    ??public?void?tweet()?{
    ????tweetAbility.tweet();?//?委托
    ??}
    ??@Override
    ??public?void?layEgg()?{
    ????eggLayAbility.layEgg();?//?委托
    ??}
    }

    當然啦,也可以使用 JDK 1.8 之后支持的接口默認方法:

    public?interface?Flyable?{
    ????default?void?fly()?{
    ????????//?fly的?的默認實現
    ????}
    }

    我們知道繼承主要有三個作用:表示 is-a 關系,支持多態特性,代碼復用。而這三個作用都可以通過其他技術手段來達成。比如:

    • is-a 關系,我們可以通過組合和接口的 has-a 關系來替代;
    • 多態特性我們可以利用接口來實現;
    • 代碼復用我們可以通過組合和委托來實現;

    所以,從理論上講,通過組合、接口、委托三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關系,特別是一些復雜的繼承關系。

    如何判斷該用組合還是繼承?

    盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。從上面的例子來看,繼承改寫成組合意味著要做更細粒度的類的拆分。這也就意味著,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的復雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。

    如果類之間的繼承結構穩定 (不會輕易改變),繼承層次比較淺 *(比如,最多有兩層繼承關系),繼承關系不復雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關系復雜,我們就盡量使用組合來替代繼承。

    除此之外,還有一些 設計模式 會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了 組合關系,而 模板模式(template pattern)使用了 繼承關系

    前面我們講到繼承可以實現代碼復用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復用父類中的屬性和方法,達到代碼復用的目的。但是,有的時候,從業務含義上,A 類和 B 類并不一定具有繼承關系。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但并不具有繼承關系 (既不是父子關系,也不是兄弟關系)。僅僅為了代碼復用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。如果不熟悉背后設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關的操作,會覺得這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現如下所示:

    public?class?Url?{
    ??//...省略屬性和方法
    }

    public?class?Crawler?{
    ??private?Url?url;?//?組合
    ??public?Crawler()?{
    ????this.url?=?new?Url();
    ??}
    ??//...
    }

    public?class?PageAnalyzer?{
    ??private?Url?url;?//?組合
    ??public?PageAnalyzer()?{
    ????this.url?=?new?Url();
    ??}
    ??//..
    }

    還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支持多態,只能采用繼承來實現。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運行時執行的 encode() 函數。這個時候,我們只能采用繼承來實現了。

    public?class?FeignClient?{?//?Feign?Client框架代碼
    ??//...省略其他代碼...
    ??public?void?encode(String?url)?{?//...?}
    }

    public?void?demofunction(FeignClient?feignClient)?{
    ??//...
    ??feignClient.encode(url);
    ??//...
    }

    public?class?CustomizedFeignClient?extends?FeignClient?{
    ??@Override
    ??public?void?encode(String?url)?{?//...重寫encode的實現...}
    }

    //?調用
    FeignClient?client?=?new?CustomizedFeignClient();
    demofunction(client);

    盡管有些人說,要杜絕繼承,100% 用組合代替繼承,但是我的觀點沒那么極端!之所以 “多用組合少用繼承” 這個口號喊得這么響,只是因為,長期以來,我們過度使用繼承。還是那句話,組合并不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。

    要點回顧

  • 繼承概述 / 單繼承 / is-a 關系 / 類之間的關系 / 層級結構;
  • 繼承的實現 / 覆蓋方法 / protedcted / 向上轉型;
  • Object 類 / 方法調用 / final / 內聯 / 為什么不推薦使用繼承;
  • 練習

    暫無;

    參考資料

  • 《Java 核心技術 卷 I》
  • 《Java 編程思想》
  • 《碼出高效 Java 開發手冊》
  • 設計模式之美 - 為何說要多用組合少用繼承?如何決定該用組合還是繼承?- https://time.geekbang.org/column/article/169593
  • Introduction to Computer Science using Java -?http://programmedlessons.org/Java9/index.html#part02
    • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
    • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

    非常感謝各位人才能 看到這里,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

    創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!

    點擊留言

    總結

    以上是生活随笔為你收集整理的java用继承编写宠物乐园_MoreThanJavaDay 5:面向对象进阶继承详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。