java用继承编写宠物乐园_MoreThanJavaDay 5:面向对象进阶继承详解
- 「MoreThanJava」 宣揚(yáng)的是 「學(xué)習(xí),不止 CODE」,本系列 Java 基礎(chǔ)教程是自己在結(jié)合各方面的知識之后,對 Java 基礎(chǔ)的一個(gè)總回顧,旨在 「幫助新朋友快速高質(zhì)量的學(xué)習(xí)」。
- 當(dāng)然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯(cuò)」 的朋友,歡迎 「關(guān)注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支持是我前進(jìn)的最大的動(dòng)力!
Part 1. 繼承概述
上一篇文章 中我們簡單介紹了繼承的作用,它允許創(chuàng)建 具有邏輯等級結(jié)構(gòu)的類體系,形成一個(gè)繼承樹。
Animal 繼承樹繼承使您可以基于現(xiàn)有類定義新類。新類與現(xiàn)有類相似,但是可能具有其他實(shí)例變量和方法。這使編程更加容易,因?yàn)槟梢?在現(xiàn)有的類上構(gòu)建,而不必從頭開始。
繼承是現(xiàn)代軟件取得巨大成功的部分原因。程序員能夠在先前的工作基礎(chǔ)上繼續(xù)發(fā)展并不斷改進(jìn)和升級現(xiàn)有軟件。
面向?qū)ο笾?#xff0c;寫代碼的一些問題
如果你有一個(gè)類的源代碼,你可以復(fù)制代碼并改變它變成你想要的樣子。在面向?qū)ο缶幊讨?#xff0c;就是這樣子做的。但至少有兩個(gè)問題:
? 很難保持僅僅有條。
假設(shè)您已經(jīng)有了幾十個(gè)需要的類,并且需要基于原始類創(chuàng)造新的一些類,再基于新的類創(chuàng)造出更新的類,最終您將獲得數(shù)十個(gè)源文件,這些源文件都是通過其他已更改的源文件的另外版本。
假設(shè)現(xiàn)在在一個(gè)源文件中發(fā)現(xiàn)了錯(cuò)誤,一些基于它的源文件需要進(jìn)行修復(fù),但是對于其他源文件來說,并不需要!沒有細(xì)致的寫代碼的計(jì)劃,您最終會(huì)陷入混亂....
? 需要學(xué)習(xí)原始代碼。
假設(shè)您有一個(gè)復(fù)雜的類,基本上可以完成所需的工作,但是您需要進(jìn)行一些小的修改。如果您修改了源代碼,即使是進(jìn)行了很小的更改,也可能會(huì)破壞某些內(nèi)容。因此,您必須研究原始代碼以確保所做的更改正確,這可能并不容易。
Java 的自動(dòng)繼承機(jī)制極大地緩解了這兩個(gè)問題。
單繼承
用于作為新類模板的類稱為 父類 (或超類或基類),基于父類創(chuàng)建的類稱為 子類 (或派生類)。
就像上圖中演示的那樣,箭頭從子類指向父類。(在上圖中,云表示類,而矩形表示對象,這樣的表示的方法來自于 Grady Booch 寫的《面向?qū)ο蟮姆治龊驮O(shè)計(jì)》一書。而在官方的 UML-統(tǒng)一建模語言 中,類和對象都用矩形表示,請注意這一點(diǎn))
在 Java 中,子類僅從一個(gè)父類繼承特征,這被稱為 單繼承 (與人類不同)。
有些語言允許"孩子"從多個(gè)"父母"那里繼承,這被稱為 多繼承。但由于具有多重繼承,有時(shí)很難說出哪個(gè)父母為孩子貢獻(xiàn)了哪些特征 (跟人類一樣..)。
Java 通過使用單繼承避免了這些問題。(意思 Java 只允許單繼承)
is-a 關(guān)系
上圖顯示了一個(gè)父類 (Video 視頻類),一個(gè)子類 (Movie 電影類)。它們之間的實(shí)線表示 "is-a" 的關(guān)系:電影是視頻。
注意,繼承是在類之間,而不是在對象之間。 (上圖兩朵云都代表類)
父類是構(gòu)造對象時(shí)使用的藍(lán)圖,子類用于構(gòu)造看起來像父對象的對象,但具有附加功能的對象。
類之間的關(guān)系簡述
簡單地說,類和類之間的關(guān)系有三種:is-a、has-a 和 use-a。
- is-a 關(guān)系也叫繼承或泛化,比如學(xué)生和人的關(guān)系、手機(jī)和電子產(chǎn)品的關(guān)系都屬于繼承關(guān)系;
- has-a 關(guān)系通常稱之為關(guān)聯(lián),比如部門和員工的關(guān)系、汽車和引擎的關(guān)系都屬于關(guān)聯(lián)關(guān)系;關(guān)聯(lián)關(guān)系如果是整體和部分的關(guān)聯(lián),那么我們稱之為 聚合關(guān)系;如果整體進(jìn)一步負(fù)責(zé)了部分的生命周期 (整體和部分是不可分割的,同時(shí)同在也同時(shí)消亡),那么這種就是最強(qiáng)的關(guān)聯(lián)關(guān)系,我們稱之為 合成 關(guān)系。
- use-a 關(guān)系通常稱之為依賴,比如司機(jī)有一個(gè)駕駛的行為 (方法),其中 (的參數(shù)) 使用到了汽車,那么司機(jī)和汽車的關(guān)系就是依賴關(guān)系。
利用類之間的這些關(guān)系,我們可以在已有類的基礎(chǔ)上來完成某些操作,也可以在已有類的基礎(chǔ)上創(chuàng)建新的類,這些都是實(shí)現(xiàn)代碼復(fù)用的重要手段。復(fù)用現(xiàn)有的代碼不僅可以減少開發(fā)的工作量,也有利于代碼的管理和維護(hù),這是我們在日常工作中都會(huì)使用到的技術(shù)手段。
層級結(jié)構(gòu)
上圖顯示了一個(gè)父類和一個(gè)子類的 層次結(jié)構(gòu),以及從每個(gè)類構(gòu)造的一些對象。這些對象用矩形表示,以表達(dá)它們比設(shè)計(jì)的類更真實(shí)。
在層次結(jié)構(gòu)中,每個(gè)類最多有一個(gè)父類,但可能有幾個(gè)子類。層次結(jié)構(gòu)頂部的類沒有父級。此類稱為層次結(jié)構(gòu)的 根。
另外,一個(gè)類可以是另一個(gè)子類的父類,也可以是父類的子類。就像人類一樣,一個(gè)人是某些人類的孩子,也是其他人類的父母。(但在 Java 中,一個(gè)孩子只有一個(gè)父母)
Part 2. 繼承的實(shí)現(xiàn)
從父類派生子類的語法是使用 extend 關(guān)鍵字:
class?ChildClass?extend?ParentClass?{????//?子類的新成員和構(gòu)造函數(shù)....
}
父類的成員 (變量和方法) 通過繼承包含在子類中。其他成員將在其類定義中添加到子類。
視頻觀影 App 示例
Java 編程是通過創(chuàng)建類層次結(jié)構(gòu)并從中實(shí)例化對象來完成的。您可以擴(kuò)展自己的類或擴(kuò)展已經(jīng)存在的類。Java 開發(fā)工具包 (JDK) 為您提供了豐富的基類集合,您可以根據(jù)需要擴(kuò)展這些基類。
(如果某些類已經(jīng)使用 final 修飾,則無法繼承)
下面演示了一個(gè)使用 Video 類作為基類的視頻觀影 App 的程序設(shè)計(jì):
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;?}
}
這兩個(gè)類均已定義:Video 類可用于構(gòu)造視頻類型的對象,現(xiàn)在 Movie 類可用于構(gòu)造電影類型的對象。
Movie 類具有在 Video 中定義的成員變量和公共方法。
使用父類的構(gòu)造函數(shù)
查看上方的示例,在 Movie 類的初始化構(gòu)造函數(shù)中有一條 super(title, length); 的語句,是 "調(diào)用父類 Video 中帶有 title、length 參數(shù)的構(gòu)造器" 的簡寫形式。
由于 Movie 類的構(gòu)造器不能訪問 Video 類的私有字段,所以必須通過一個(gè)構(gòu)造器來初始化這些私有字段。可以利用特殊的 super 語法調(diào)用這個(gè)構(gòu)造器。
重要說明:super() 必須是子類構(gòu)造函數(shù)中的第一條語句。 (這意味子類構(gòu)造器總是會(huì)先調(diào)用父類的構(gòu)造器) 這件事經(jīng)常被忽略,導(dǎo)致的結(jié)果就是一些神秘的編譯器錯(cuò)誤消息。
如果子類的構(gòu)造器沒有顯式地調(diào)用父類的構(gòu)造器,將自動(dòng)地調(diào)用父類的無參構(gòu)造器。如果父類沒有無參數(shù)的構(gòu)造器,并且在子類的構(gòu)造器中又沒有顯式地調(diào)用父類的其他構(gòu)造器,Java 編譯器就會(huì)報(bào)告一個(gè)錯(cuò)誤。(在我們的例子中 Video 缺少無參數(shù)的構(gòu)造函數(shù),故?上面圖片代碼會(huì)報(bào)錯(cuò))
創(chuàng)建一個(gè)無參構(gòu)造函數(shù)
關(guān)于構(gòu)造函數(shù)的一些細(xì)節(jié):
在示例程序中,類 Video 包含構(gòu)造函數(shù),因此不會(huì)自動(dòng)提供默認(rèn)構(gòu)造函數(shù)。所以,Movie 類 super() 函數(shù)建議默認(rèn)使用的構(gòu)造函數(shù) (會(huì)自動(dòng)調(diào)用無參數(shù)構(gòu)造函數(shù)) 會(huì)導(dǎo)致語法錯(cuò)誤。
解決方法是將無參數(shù)構(gòu)造函數(shù)顯式放在類中 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;
????}
????...
}
覆蓋方法
讓我們來實(shí)例化 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=90title=悟空傳, length=139
movie.toString() 是 Movie 類直接繼承自 Video 類,它并沒有使用 Movie 對象具有的新變量,因此并不會(huì)打印導(dǎo)演和評分。
我們需要給 Movie 類添加新的 toString() 的使用方法:
//?添加到?Movie?類中public?String?toString()?{
????return?"title:"?+?getTitle()?+?",?length:"?+?getLength()?+?",?director:"?+?getDirector()
????????+?",?rating:"?+?getRating();
}
現(xiàn)在,Movie 擁有了自己的 toString() 方法,該方法使用了繼承自 Video 的變量和自己定義的變量。
即使父類有一個(gè) toString() 方法,子類中新定義的 toString() 也會(huì) 覆蓋 父類的版本。當(dāng)子類方法的 簽名 (就是返回值 + 方法名稱 + 參數(shù)列表) 與父類相同時(shí),子類的方法就會(huì) 覆蓋 父類的方法。
現(xiàn)在運(yùn)行程序,Movie 打印出了我們期望的完整信息:
title=視頻1, length=90title:悟空傳, length:139, director:郭子健, rating:5.9
“
有些人認(rèn)為 super 與 this 引用是類似的概念,實(shí)際上,這樣比較并不太恰當(dāng)。這是因?yàn)?super 不是一個(gè)對象的引用,例如,不能將值 super 賦給另一個(gè)對象變量,它只是一個(gè)指示編譯器調(diào)用父類方法的特殊關(guān)鍵字。
正像前面所看到的那樣,在子類中可以增加字段、增加方法或覆蓋父類的方法,不過,繼承絕對不會(huì)刪除任何字段或方法。
Part 3. 更多細(xì)節(jié)
protected 關(guān)鍵字
如果類中創(chuàng)建的變量或者方法使用 protected 描述,則指明了 "就類用戶而言,這是 private 的,但對于任何繼承于此類的導(dǎo)出類或者任何位于同一個(gè) 包 內(nèi)的類來說,它是可以訪問的"。下面我們就上面的例子來演示:
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() 公共方法,現(xiàn)在則可以直接使用。
向上轉(zhuǎn)型
"為新的類提供方法" 并不是繼承技術(shù)中最重要的方面,其最重要的方面是用來表現(xiàn)新類和基類之間的關(guān)系。這種關(guān)系可以用 "新類是現(xiàn)有類的一種類型" 這句話加以概括。
由于繼承可以確保基類中所有的方法在子類中也同樣有效,所以能夠向基類發(fā)送的所有信息也同樣可以向子類發(fā)送。例如,如果 Video 類具有一個(gè) play() 方法, 那么 Movie 類也將同樣具備。這意味著我們可以準(zhǔn)確地說 Movie 對象也是一種類型的 Video。(體現(xiàn) is-a 關(guān)系)
這一概念的體現(xiàn)用下面的例子來說明:
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() 方法的是一個(gè) Movie 引用。鑒于 Java 是一個(gè)對類型檢查十分嚴(yán)格的語言,接受某種類型 (上例是 Video 類型) 的方法同樣可以接受另外一種類型 (上例是 Movie 類型) 就會(huì)顯得很奇怪!
除非你認(rèn)識到 Movei 對象也是一種 Video 對象。
在 start() 方法中,程序代碼可以對 Video 和它所有子類起作用,這種將 Movie 引用轉(zhuǎn)換為 Video 引用的動(dòng)作,我們稱之為 向上轉(zhuǎn)型 (這樣稱呼是因?yàn)樵诶^承樹的畫法上,基類在子類的上方...)。
Object 類
所有的類均具有父類,除了 Object 類。Java 類層次結(jié)構(gòu)的最頂部就是 Object 類。
如果類沒有顯式地指明繼承哪一個(gè)父類,那么它會(huì)自動(dòng)地繼承自 Object 類。如果一個(gè)子類繼承了一個(gè)父類,那么父類要么繼承它的父類,要么自動(dòng)繼承 Object。最終,所有的類都將 Object 作為祖先。
這意味著 Java 中的所有類都具有一些共同的特征。這些特征在被定義在 Object 中:
Object 類擁有的方法(其中 finalize() 方法在 Java 9 之后棄用了,原因是因?yàn)樗旧泶嬖谝恍﹩栴},可能導(dǎo)致性能問題:死鎖、掛起和其他問題...)
(想看源碼可以打一個(gè) Object,然后按住 Ctrl 不放,然后點(diǎn)擊 Object 就可以進(jìn)入 JDK 源碼查看了,源碼有十分規(guī)范的注釋和結(jié)構(gòu),你有時(shí)甚至?xí)l(fā)現(xiàn)一些有趣的東西...)
Java 之父 Gosling 設(shè)計(jì)的 Object 類,是對萬事萬物的抽象,是在哲學(xué)方向上進(jìn)行的延伸思考,高度概括了事物的自然行為和社會(huì)行為。我們都知道哲學(xué)的三大經(jīng)典問題:我是誰?我從哪里來?我到哪里去?在 Object 類中,這些問題都可以得到隱約的解答:
另外,Object 還映射了社會(huì)科學(xué)領(lǐng)域的一些問題:
理解方法調(diào)用
準(zhǔn)確地理解如何在對象上應(yīng)用方法調(diào)用非常重要。下面假設(shè)我們要調(diào)用 x.f(args),x 是聲明為 C 的一個(gè)對象。下面是調(diào)用過程的詳細(xì)描述:
每次調(diào)用方法都要完成這樣的搜索,時(shí)間開銷相當(dāng)大。因此,虛擬機(jī)預(yù)先為每個(gè)類計(jì)算了一個(gè) 方法表 (method table), 其中列出了所有方法的簽名和要調(diào)用的實(shí)際方法 (存著各個(gè)方法的實(shí)際入口地址)。這樣一來,在真正調(diào)用方法的時(shí)候,虛擬機(jī)僅查找這個(gè)表就行了。(以下是 Video-父類 和 Movie-子類 的方法表結(jié)構(gòu)演示圖)
例如我們調(diào)用上述例子 Movie 類的 play() 方法。
public?void?play()?{};由于 play() 方法沒有參數(shù),因此不必?fù)?dān)心 重載解析 的問題。又不是 private/ static/ final 方法,所以將采用 動(dòng)態(tài)綁定 的方式。
在運(yùn)行時(shí),調(diào)用 object.play() 的解析過程為:
動(dòng)態(tài)綁定有一個(gè)非常重要的特性:無須對現(xiàn)有的代碼進(jìn)行修改就可以對程序進(jìn)行擴(kuò)展。
假設(shè)現(xiàn)在新增一個(gè)類 ShortVideo,并且變量 object 有可能引用這個(gè)類的對象,我們不需要對包含調(diào)用 object.play() 的代碼重新進(jìn)行編譯。如果 object 恰好引用一個(gè) ShortVideo 類的對象,就會(huì)自動(dòng)地調(diào)用 object.play() 方法。
“警告:在覆蓋一個(gè)方法時(shí),子類的方法 不能低于 父類方法的 可見性 (public > protected > private)。特別是,如果父類方法是 public,子類方法必須也要聲明為 public。
final 關(guān)鍵字
有時(shí)候,我們可能希望組織人們利用某個(gè)類定義子類。不允許擴(kuò)展 (被繼承) 的類被稱為 final 類。如果在定義類的時(shí)候使用了 final 修飾符就表明這個(gè)類是 final 類了:
public?final?class?ShortVideo?extends?Video?{?...?}類中的某個(gè)特定方法也可以被聲明為 final。如果這樣做,子類就不能覆蓋這個(gè)方法 (final 類中的所有方法自動(dòng)地稱為 final 方法)。例如:
public?class?Video?{????...
????public?final?void?Stop()?{?...?}
????...
}
如果一個(gè) 字段 被聲明為了 final 類型,那么對于 final 字段來說,構(gòu)造對象之后就不允許改變它們的值了。不過,如果將一個(gè)類聲明為 final,只有其中的方法自動(dòng)地稱為 final,而不包括字段,這一點(diǎn)需要注意。
將方法或類聲明為 final 的主要原因是:確保它們不會(huì)在子類中改變語義。
JDK 中的例子
- Calendar 類 (JDK 實(shí)現(xiàn)的日歷類) 中的 getTime 和 setTime 方法都聲明為了 final,這就表明 Calendar 類的設(shè)計(jì)者負(fù)責(zé)實(shí)現(xiàn) Data 類與日歷狀態(tài)之間的轉(zhuǎn)換,而不允許子類來添亂。
- 同樣的,String 類也是 final 類 (甚至面試中也經(jīng)常出現(xiàn)),這意味著不允許任何人定義 String 的子類,換而言之,如果有一個(gè) String 引用,它引用的一定是一個(gè) String 對象,而不可能是其他類的對象。
內(nèi)聯(lián)
在早起的 Java 中,有些程序員為了避免動(dòng)態(tài)綁定帶來的系統(tǒng)開銷而使用 final 關(guān)鍵字。如果一個(gè)方法沒有被覆蓋并且很短,編譯器就能夠?qū)λM(jìn)行優(yōu)化處理,這個(gè)過程為 內(nèi)聯(lián) (inlining)。
例如,內(nèi)聯(lián)調(diào)用 e.getName() 會(huì)被替換為訪問字段 e.name。
這是一項(xiàng)很有意義的改進(jìn),CPU 在處理當(dāng)前指令時(shí),分支會(huì)擾亂預(yù)取指令的策略,所以,CPU 不喜歡分支。然而,如果 getName 在另外一個(gè)類中 被覆蓋,那么編譯器就無法知道覆蓋的代碼將會(huì)做什么操作,因此也就不能對它進(jìn)行內(nèi)聯(lián)處理了。
幸運(yùn)的是,虛擬機(jī)中的 即時(shí)編譯器 (JIT) 比傳統(tǒng)編譯器的處理能力強(qiáng)得多。這種編譯器可以準(zhǔn)確地知道類之間的繼承關(guān)系,并能夠檢測出是否有類確實(shí)覆蓋了給定的方法。
如果方法很短、被頻繁調(diào)用而且確實(shí)沒有被覆蓋,那么即時(shí)編譯器就會(huì)將這個(gè)方法進(jìn)行內(nèi)聯(lián)處理。如果虛擬機(jī)加載了另外一個(gè)子類,而這個(gè)子類覆蓋了一個(gè)內(nèi)聯(lián)方法,那么優(yōu)化器將取消對這個(gè)方法的內(nèi)聯(lián)。這個(gè)過程很慢,不過很少會(huì)發(fā)生這種情況。
抽象類
在類的自下而上的繼承層次結(jié)構(gòu)中,位于上層的類更具有一般性,也更加抽象。從某種角度看,祖先類更具有一般性,人們通常只是將它作為派生其他類的基類,而不是用來構(gòu)造你想使用的特定的實(shí)例。
考慮一個(gè) Person 類的繼承結(jié)構(gòu):
每個(gè)人都有一些屬性,如名字。學(xué)生與員工都有名字。
現(xiàn)在,假設(shè)需要增加一個(gè) getDescription() 的方法,它返回對一個(gè)人簡短的描述,學(xué)生類可以返回:一個(gè)計(jì)算機(jī)在讀的學(xué)生,員工可以返回 一個(gè)在阿里就職的后端工程師 之類的。這在 Student 和 Employee 類中實(shí)現(xiàn)很容易,但是在 Person 類中應(yīng)該提供什么內(nèi)容呢?除了姓名,Person 類對這個(gè)人一無所知。
有一個(gè)更好的方法,就是使用 abstract 關(guān)鍵字,把該方法定義為一個(gè) 抽象方法,這意味著你并不需要實(shí)現(xiàn)這個(gè)方法,只需要定義出來就好了:(以下代碼為 Person 類中的抽象定義)
public?abstract?String?getDescription()?{}為了提高程序的清晰度,包含一個(gè)或多個(gè)抽象方法的類本身必須被聲明為抽象的:
public?abstract?class?Person?{????...
????public?abstract?String?getDescription()?{}
????...
}
“
《阿里Java開發(fā)規(guī)范》強(qiáng)制規(guī)定抽象類命名 使用 Abstract 或 Base 開頭,這里只是做演示所以就簡單用 Person 代替啦~
抽象方法充當(dāng)著占位方法的角色,它們在子類中被繼承并實(shí)現(xiàn)。
擴(kuò)展抽象類可以由兩種選擇。一種是在子類中保留抽象類中的部分或所有抽象方法仍未實(shí)現(xiàn),這樣就必須將子類標(biāo)記為抽象類 (因?yàn)檫€有抽象方法);另一種做法就是實(shí)現(xiàn)全部方法,這樣一來,子類就不是抽象的了。
(即使不包含抽象方法,也可以將類聲明為抽象類)
抽象類不能實(shí)例化,也就是說,如果將一個(gè)類聲明為 abstract,就不能創(chuàng)建這個(gè)類的實(shí)例,例如:new Person(); 就是錯(cuò)誤的,但可以創(chuàng)建具體子類的對象:Person p = new Student(args);,這里的 p 是一個(gè)抽象類型 Person 的變量,它引用了一個(gè)非抽象子類 Student 的實(shí)例。
Part 4. 為什么不推薦使用繼承?
先別急著奇怪和憤懣,剛學(xué)習(xí)完繼承之后,就告訴說不推薦使用,這是 有原因的!
在面向?qū)ο缶幊讨?#xff0c;有一條非常經(jīng)典的設(shè)計(jì)原則:組合優(yōu)于繼承。使用繼承有什么問題?組合相比繼承有哪些優(yōu)勢?如何判斷該用組合還是繼承?下面我們就圍繞這三個(gè)問題,來詳細(xì)講解一下。
“以下內(nèi)容大部分引用自:https://time.geekbang.org/column/article/169593
使用繼承有什么問題?
上面說到,繼承是面向?qū)ο蟮乃拇筇匦灾?#xff0c;用來表示類之間的 is-a 關(guān)系,可以解決代碼復(fù)用的問題。雖然繼承有諸多作用,但繼承層次過深、過復(fù)雜,也會(huì)影響到代碼的可維護(hù)性。我們通過一個(gè)例子來說明一下。
假設(shè)我們要設(shè)計(jì)一個(gè)關(guān)于鳥的類,我們將 “鳥類” 這樣一個(gè)抽象的事物概念,定義為一個(gè)抽象類 AbstractBird。所有更細(xì)分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個(gè)抽象類。
我們知道,大部分鳥都會(huì)飛,那我們可不可以在 AbstractBird 抽象類中,定義一個(gè) fly() 方法呢?答案是否定的。盡管大部分鳥都會(huì)飛,但也有特例,比如鴕鳥就不會(huì)飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現(xiàn)實(shí)世界中事物的認(rèn)識。當(dāng)然,你可能會(huì)說,我在鴕鳥這個(gè)子類中重寫 (override) fly() 方法,讓它拋出 UnSupportedMethodException 異常不就可以了嗎?具體的代碼實(shí)現(xiàn)如下所示:
public?class?AbstractBird?{??//...省略其他屬性和方法...
??public?void?fly()?{?//...?}
}
public?class?Ostrich?extends?AbstractBird?{?//鴕鳥
??//...省略其他屬性和方法...
??public?void?fly()?{
????throw?new?UnSupportedMethodException("I?can't?fly.'");
??}
}
這種設(shè)計(jì)思路雖然可以解決問題,但不夠優(yōu)美。因?yàn)槌锁r鳥之外,不會(huì)飛的鳥還有很多,比如企鵝。對于這些不會(huì)飛的鳥來說,我們都需要重寫 fly() 方法,拋出異常。
這樣的設(shè)計(jì),一方面,徒增了編碼的工作量;另一方面,也違背了我們之后要講的最小知識原則 (Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。
你可能又會(huì)說,那我們再通過 AbstractBird 類派生出兩個(gè)更加細(xì)分的抽象類:會(huì)飛的鳥類 AbstractFlyableBird 和不會(huì)飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會(huì)飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會(huì)飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關(guān)系如下圖所示:
從圖中我們可以看出,繼承關(guān)系變成了三層。不過,整體上來講,目前的繼承關(guān)系還比較簡單,層次比較淺,也算是一種可以接受的設(shè)計(jì)思路。我們再繼續(xù)加點(diǎn)難度。在剛剛這個(gè)場景中,我們只關(guān)注“鳥會(huì)不會(huì)飛”,但如果我們關(guān)注更多的問題,例如 “鳥會(huì)不會(huì)叫”、”鳥會(huì)不會(huì)下單“ 等... 那這個(gè)時(shí)候,我們又該如何設(shè)計(jì)類之間的繼承關(guān)系呢?
總之,繼承最大的問題就在于:繼承層次過深、繼承關(guān)系過于復(fù)雜會(huì)影響到代碼的可讀性和可維護(hù)性。這也是為什么我們不推薦使用繼承。那剛剛例子中繼承存在的問題,我們又該如何來解決呢?
組合相比繼承有哪些優(yōu)勢?
實(shí)際上,我們可以利用組合 (composition)、接口、委托 (delegation) 三個(gè)技術(shù)手段,一塊兒來解決剛剛繼承存在的問題。
我們前面講到接口的時(shí)候說過,接口表示具有某種行為特性。針對“會(huì)飛”這樣一個(gè)行為特性,我們可以定義一個(gè) Flyable 接口 (相當(dāng)于定義某一種行為,下方會(huì)有代碼說明),只讓會(huì)飛的鳥去實(shí)現(xiàn)這個(gè)接口。對于會(huì)叫、會(huì)下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。我們將這個(gè)設(shè)計(jì)思路翻譯成 Java 代碼的話,就是下面這個(gè)樣子:
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()?{?//...?}
}
不過,我們知道,接口只聲明方法,不定義實(shí)現(xiàn)。也就是說,每個(gè)會(huì)下蛋的鳥都要實(shí)現(xiàn)一遍 layEgg() 方法,并且實(shí)現(xiàn)邏輯是一樣的,這就會(huì)導(dǎo)致代碼重復(fù)的問題。那這個(gè)問題又該如何解決呢?
我們可以針對三個(gè)接口再定義三個(gè)實(shí)現(xiàn)類,它們分別是:實(shí)現(xiàn)了 fly() 方法的 FlyAbility 類、實(shí)現(xiàn)了 tweet() 方法的 TweetAbility 類、實(shí)現(xiàn)了 layEgg() 方法的 EggLayAbility 類。然后,通過 組合和委托 技術(shù)來消除代碼重復(fù)。具體的代碼實(shí)現(xiàn)如下所示:
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();?//?委托
??}
}
當(dāng)然啦,也可以使用 JDK 1.8 之后支持的接口默認(rèn)方法:
public?interface?Flyable?{????default?void?fly()?{
????????//?fly的?的默認(rèn)實(shí)現(xiàn)
????}
}
我們知道繼承主要有三個(gè)作用:表示 is-a 關(guān)系,支持多態(tài)特性,代碼復(fù)用。而這三個(gè)作用都可以通過其他技術(shù)手段來達(dá)成。比如:
- is-a 關(guān)系,我們可以通過組合和接口的 has-a 關(guān)系來替代;
- 多態(tài)特性我們可以利用接口來實(shí)現(xiàn);
- 代碼復(fù)用我們可以通過組合和委托來實(shí)現(xiàn);
所以,從理論上講,通過組合、接口、委托三個(gè)技術(shù)手段,我們完全可以替換掉繼承,在項(xiàng)目中不用或者少用繼承關(guān)系,特別是一些復(fù)雜的繼承關(guān)系。
如何判斷該用組合還是繼承?
盡管我們鼓勵(lì)多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。從上面的例子來看,繼承改寫成組合意味著要做更細(xì)粒度的類的拆分。這也就意味著,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的復(fù)雜程度和維護(hù)成本。所以,在實(shí)際的項(xiàng)目開發(fā)中,我們還是要根據(jù)具體的情況,來具體選擇該用繼承還是組合。
如果類之間的繼承結(jié)構(gòu)穩(wěn)定 (不會(huì)輕易改變),繼承層次比較淺 *(比如,最多有兩層繼承關(guān)系),繼承關(guān)系不復(fù)雜,我們就可以大膽地使用繼承。反之,系統(tǒng)越不穩(wěn)定,繼承層次很深,繼承關(guān)系復(fù)雜,我們就盡量使用組合來替代繼承。
除此之外,還有一些 設(shè)計(jì)模式 會(huì)固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了 組合關(guān)系,而 模板模式(template pattern)使用了 繼承關(guān)系。
前面我們講到繼承可以實(shí)現(xiàn)代碼復(fù)用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復(fù)用父類中的屬性和方法,達(dá)到代碼復(fù)用的目的。但是,有的時(shí)候,從業(yè)務(wù)含義上,A 類和 B 類并不一定具有繼承關(guān)系。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但并不具有繼承關(guān)系 (既不是父子關(guān)系,也不是兄弟關(guān)系)。僅僅為了代碼復(fù)用,生硬地抽象出一個(gè)父類出來,會(huì)影響到代碼的可讀性。如果不熟悉背后設(shè)計(jì)思路的同事,發(fā)現(xiàn) Crawler 類和 PageAnalyzer 類繼承同一個(gè)父類,而父類中定義的卻只是 URL 相關(guān)的操作,會(huì)覺得這個(gè)代碼寫得莫名其妙,理解不了。這個(gè)時(shí)候,使用組合就更加合理、更加靈活。具體的代碼實(shí)現(xiàn)如下所示:
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();
??}
??//..
}
還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個(gè)函數(shù)的入?yún)㈩愋?#xff0c;而入?yún)⒂址墙涌?#xff0c;為了支持多態(tài),只能采用繼承來實(shí)現(xiàn)。比如下面這樣一段代碼,其中 FeignClient 是一個(gè)外部類,我們沒有權(quán)限去修改這部分代碼,但是我們希望能重寫這個(gè)類在運(yùn)行時(shí)執(zhí)行的 encode() 函數(shù)。這個(gè)時(shí)候,我們只能采用繼承來實(shí)現(xiàn)了。
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的實(shí)現(xiàn)...}
}
//?調(diào)用
FeignClient?client?=?new?CustomizedFeignClient();
demofunction(client);
盡管有些人說,要杜絕繼承,100% 用組合代替繼承,但是我的觀點(diǎn)沒那么極端!之所以 “多用組合少用繼承” 這個(gè)口號喊得這么響,只是因?yàn)?#xff0c;長期以來,我們過度使用繼承。還是那句話,組合并不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發(fā)揮它們各自的優(yōu)勢,在不同的場合下,恰當(dāng)?shù)剡x擇使用繼承還是組合,這才是我們所追求的境界。
要點(diǎn)回顧
練習(xí)
暫無;
參考資料
- 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學(xué)習(xí),不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
- 個(gè)人公眾號 :wmyskxz,個(gè)人獨(dú)立域名博客:wmyskxz.com,堅(jiān)持原創(chuàng)輸出,下方掃碼關(guān)注,2020,與您共同成長!
非常感謝各位人才能 看到這里,如果覺得本篇文章寫得不錯(cuò),覺得 「我沒有三顆心臟」有點(diǎn)東西 的話,求點(diǎn)贊,求關(guān)注,求分享,求留言!
創(chuàng)作不易,各位的支持和認(rèn)可,就是我創(chuàng)作的最大動(dòng)力,我們下篇文章見!
點(diǎn)擊留言
總結(jié)
以上是生活随笔為你收集整理的java用继承编写宠物乐园_MoreThanJavaDay 5:面向对象进阶继承详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ie网络集合代理无法启动_网络故障诊断7
- 下一篇: python 模拟登陆智联_Python