更新內(nèi)容:方法覆蓋
本以為覆蓋的主體在多態(tài)一章贰谣,結(jié)果寫完之后才發(fā)現(xiàn)實際上本書對覆蓋的講解幾乎沒有娜搂。。吱抚。特此補充百宇。
學(xué)習(xí)內(nèi)容
- 兩種代碼復(fù)用機制 - 組合 & 繼承
- 以及中庸之道 - 代理
- 組合 和 繼承
- 如何選擇使用
- 如何結(jié)合使用
- 向上轉(zhuǎn)型
- final 關(guān)鍵字
- 方法覆蓋
1. 組合
1.1 組合的概念
? 在新的類中產(chǎn)生現(xiàn)有類的對象,由于新的類是由現(xiàn)有類的對象所組成秘豹,所以這種方法成為組合恳谎。
1.1.1 組合的寫法及樣例
只需要在當(dāng)前類中聲明另一個類的對象作為其成員變量即可,如下:
class B {
//...
}
public class A {
private B b;
//...
//使用 b 時憋肖,需要進行初始化 比如 b = new B();
}
下面給出具體例子:
class Game {
private String name;
Game(String name){
this.name = name;
}
void play(){
System.out.println("play game : " + name);
}
}
public class Person {
private Game game;//持有對象的引用
void doSomething(){
game = new Game("超級瑪麗");
game.play();//調(diào)用包含對象 Game 的方法
}
public static void main(String[] args){
Person person = new person();
person.doSomething();
}
}
//輸出結(jié)果為 : play game : 超級瑪麗
1.2 組合的特性
優(yōu)點:
- 耦合度低因痛,包含對象的內(nèi)部細節(jié)改變不會影響當(dāng)前對象的使用
- 可以動態(tài)綁定,當(dāng)前對象可以在運行時動態(tài)的綁定所包含的對象(也就是后面提到的 使用實例初始化)
缺點:
- 容易產(chǎn)生過多對象
1.3 組合 - 引用的初始化
通過在以下 4 種位置進行初始化:
- 在定義對象的地方進行初始化
這也為這總是能夠在構(gòu)造器被調(diào)用之前被初始化
- 在類的構(gòu)造器中初始化
- 惰性初始化 - 就在正要使用這些對象之前進行初始化岸更。
不必要生成對象時鸵膏,這種方式可以減少額外的負擔(dān)
- 使用實例初始化
2. 繼承
2.1 繼承的概念
? 按照現(xiàn)有類的類型來創(chuàng)建新類,無需改變現(xiàn)有類的形式怎炊,采用現(xiàn)有類的形式向其中添加新代碼谭企,這種方法叫繼承。
? 簡單來說评肆,繼承就是子類繼承父類的特征和行為债查,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法瓜挽,使得子類具有父類相同的行為盹廷。
繼承的重點不在于“為新的類提供方法”,而是表現(xiàn)新類和基類的之間的關(guān)系--”新類是現(xiàn)有類的一種類型“久橙。
2.1.1 類的繼承語法
通過關(guān)鍵字 extends 申明一個類是從另一個類繼承而來的俄占,形式如下:
class 父類{
...
}
class 子類 extends 父類{
...
}
2.1.2 繼承示例
class Animal {
private String name;
private int id;
public Animal(String myName, int myid) {
name = myName;
id = myid;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好管怠!我是" + id + "號" + name + ".");
}
}
//子類 Mouse 繼承 Animal
public class Mouse extends Animal {
public Mouse(String myName, int myid) {
super(myName, myid);
}
public static void main(String[] args){
Mouse mouse = new Mouse("jerry",1);
mouse.sleep();//sleep 方法繼承自父類 Animal 的 sleep 方法
}
}
//結(jié)果為 : jerry正在睡
2.2 繼承的特性
- 子類擁有父類非 private 的屬性缸榄、方法
- 子類可以擁有自己的屬性和方法渤弛,即對父類進行擴展
- 子類可以用自己的方式實現(xiàn)父類的方法(方法覆蓋)
- 單繼承,即一個子類只能繼承一個父類
- 多重繼承甚带,即一個子類繼承一個父類她肯,而這個父類還可以繼承它的父類
- 提高了類之間的耦合性 -- 繼承的缺點
- 破壞封裝,為了復(fù)用代碼可能使子類具有不該具有的功能 -- 結(jié)成的缺點
2.3 繼承 - 基類的初始化
? 對于基類和導(dǎo)出類(或者說父類和子類)鹰贵,從外部看辕宏,導(dǎo)出類就像是一個與基類具有相同接口的新類,附加上一些額外的方法和屬性砾莱。但是瑞筐!繼承不只是復(fù)制基類的接口。當(dāng)創(chuàng)建了一個導(dǎo)出類的對象時腊瑟,該對象隱含了一個基類的對象聚假。
隱含的基類對象和我們使用基類直接創(chuàng)建的對象是一樣的,二者區(qū)別在于闰非,基類的子對象是被包裝在導(dǎo)出類對象的內(nèi)部的膘格,而后者來自于外部。
因此财松,在創(chuàng)建導(dǎo)出類對象時瘪贱,需要考慮基類對象的正確初始化。
2.3.1 初始化過程
只有一種方式能保證對基類對象進行初始化
-
在導(dǎo)出類的構(gòu)造器種調(diào)用基類構(gòu)造器來執(zhí)行初始化辆毡。
Java 會自動在導(dǎo)出類的構(gòu)造器種插入對基類構(gòu)造器(默認(rèn)/無參構(gòu)造)的調(diào)用
初始化過程總結(jié)如下:
- 從基類向外"擴散"
所有基類在導(dǎo)出類構(gòu)造器可以訪問它之前菜秦,就已經(jīng)完成了初始化
- 帶參數(shù)的構(gòu)造器,使用關(guān)鍵字 super 顯式編寫舶掖。
示例如下:
class Game{
Game(int i){
System.out.println("Game constructor with param i");
}
Game(){
System.out.println("Game constructor");
}
}
public class Chess extends Game{
Chess(int i){
super(i);
System.out.println("Chess constructor with param i");
}
Chess(){
System.out.println("Chess constructor");
}
public static void main(String[] args){
Chess chess1 = new Chess();//輸出 (1)
Chess chess2 = new Chess(11);//輸出 (2)
}
}
// (1)
/*
Game constructor
Chess constructor
*/
// (2)
/*
Game constructor with param i
Chess constructor with param i
*/
2.4 初始化與類的加載
主要是涉及到 static 以及 繼承時的問題
Java 中每個類的編譯代碼存在于它自己的獨立文件中球昨。該文件只在創(chuàng)建類的第一個對象之時被加載,但是訪問其中 static 域或者 static 方法時眨攘,也會發(fā)生加載主慰。
構(gòu)造器也是 static 方法,盡管沒有顯式寫出 static 關(guān)鍵字。因此可以更準(zhǔn)確地說,類是在其任何 static 成員被訪問時被加載喇嘱。
所有 static 對象和 static 代碼段都會在加載時依照程序中的順序而一次初始化。當(dāng)然 static 成員只會被加載一次鲁森。
2.4.1 繼承與初始化
本節(jié)重點強調(diào)一下包括繼承在內(nèi)的初始化全過程。
加載順序:
- 從 X.main() 入口開始狡蝶,加載器開始啟動并找出 X 類的編譯代碼(在X.class 文件中)咸这。
- 對它加載的過程中佳吞,如果它有基類(由關(guān)鍵詞 extends 得知),就會去加載這個基類(編譯器強制要求)棉安。
- 如果該基類上層還有基類底扳,那么就加載上層的基類。如此類推贡耽。
- 接下來衷模,執(zhí)行 根基類的 static 初始化,然后是它的導(dǎo)出類蒲赂。以此類推阱冶,直到全部導(dǎo)出類都加載完畢
對象創(chuàng)建:
- 相關(guān)類加載完畢之后,開始創(chuàng)建對象滥嘴。首先木蹬,對象中所有基本類型被設(shè)為默認(rèn)值,對象引用設(shè)為 null
- 然后若皱,調(diào)用基類的構(gòu)造器(編譯器自動調(diào)用或者開發(fā)者顯式 super 調(diào)用)
- 基類構(gòu)造器完成之后镊叁,當(dāng)前導(dǎo)出類的實例變量按照次序進行初始化。
- 最后執(zhí)行當(dāng)前導(dǎo)出類的構(gòu)造器走触。
小結(jié):
類的加載順序: 類內(nèi)部靜態(tài)塊的執(zhí)行先于類內(nèi)部靜態(tài)屬性的初始化晦譬,會發(fā)生在類被第一次加載初始化的時候;類內(nèi)部屬性的初始化先于構(gòu)造函數(shù)會發(fā)生在一個類的對象被實例化的時候互广。
類的初始化順序:父類 static > 子類 static(其中類內(nèi)部靜態(tài)塊 > 類靜態(tài)屬性) >父類 非 static > 子類非 static(其中類內(nèi)部屬性 > 類構(gòu)造函數(shù))
2.5 方法覆蓋
原書中敛腌,對方法覆蓋并沒有明確的定義,就只是提到用 @Override 注解防止本意是覆蓋方法而不留心進行了重載的情況惫皱,所以這里補充一下方法覆蓋的相關(guān)內(nèi)容像樊。
方法覆蓋是父類和子類之間的一種多態(tài),子類必須擁有父類方法的實現(xiàn)旅敷。
作用:
解決子類繼承父類之后凶硅,父類的方法不滿足子類的具體特征,此時在子類中重新定義該方法扫皱,并重寫方法體足绅。
規(guī)則:
- 覆蓋方法必須和父類中被覆蓋方法具有相同的方法名稱、輸入?yún)?shù)和返回值類型
- 覆蓋方法不能使用比父類中被覆蓋方法更嚴(yán)格的訪問權(quán)限
- 覆蓋方法**不能比父類中被覆蓋方法拋出更多的異常 **
- 不能覆蓋父類 static 方法
多態(tài)與方法覆蓋:
多態(tài)是方法覆蓋的基礎(chǔ)韩脑,而方法覆蓋又是一種多態(tài)的表現(xiàn)形式氢妈。(另一種是重載)
多態(tài)的內(nèi)容在下一章,此處先把結(jié)論放出來段多。
- 多態(tài)也就是動態(tài)綁定首量,運行時根據(jù)對象的類型進行綁定并調(diào)用相應(yīng)方法,這為覆蓋提供了前提。
- 同時加缘,為了讓多態(tài)表現(xiàn)出“允許不同類的對象對同一消息做出響應(yīng)”鸭叙,我們需要讓子類覆蓋父類的方法,否則無論是子類還是父類對象拣宏,最后調(diào)用的都是父類的方法沈贝。
方法重載與方法覆蓋:
二者都是多態(tài)的表現(xiàn)形式,但是二者并沒什么直接的關(guān)聯(lián)
- 重載是針對同一個類中的重名方法而言 -- 每個方法的參數(shù)表不同
- 覆蓋是針對父類和子類兩個類來說的勋乾,子類重新定義父類中的方法 -- 覆蓋的方法名字宋下、類型和參數(shù)表都相同。
3. 代理
Java 并沒有提供對代理的直接支持
3.1 代理的概念
? 代理是繼承和組合之間的中庸之道辑莫,將一個成員對象置于所要構(gòu)造的類中(就像組合)学歧,但與此同時在新類暴露該成員對象的所有方法(就像繼承)。
? 換句話說各吨,就是將基類對象作為代理類的成員枝笨,而代理類有對應(yīng)于基類的所有方法,這些方法內(nèi)部使用基類對象成員調(diào)用基類的方法
3.2 代理的示例
CarControls.java
public class CarControls{
void up(int velocity){
//...
}
void down(int velocity){
//...
}
void left(int velocity){
//...
}
void right(int velocity){
//...
}
}
Car.java
public class Car{
private String name;
private CarControls controls = new CarControls();
void up(int velocity){
controls.up()
}
void down(int velocity){
controls.down()
}
void left(int velocity){
controls.left()
}
void right(int velocity){
controls.right()
}
}
以上代碼揭蜒,Car 包含了 CarControls伺帘,與此同時 CarControls 的所有方法暴漏在 Car 中,也就是說現(xiàn)在我們可以直接對 Car 進行操作(比如讓它 up())忌锯,而 Car 會在內(nèi)部通過 CarControls 對這個操作進行實現(xiàn)伪嫁。
4. 組合 & 繼承
4.1 結(jié)合使用
emmm..例子看書吧。
4.1.1 確保正確清理
前面幾章也提到過偶垮,在 Java 中张咳,我們依賴?yán)厥掌髟诒匾獣r釋放內(nèi)存,但是這個我們并不知道它在什么時候被調(diào)用或者它是否被調(diào)用似舵。
因此在某些情況下脚猾,我們需要執(zhí)行一些必須的清理活動時,就必須顯式地編寫一個特殊方法來進行處理砚哗,并確保這個方法能夠執(zhí)行龙助。
除了內(nèi)存以外,不依賴?yán)厥掌髯鋈魏问轮虢妗H绻枰謇矸莾?nèi)存的東西提鸟,編寫自己的清理方法,注意不要使用 finalize()仅淑。
通常我們會在 try-catch-finally 中的 finally 子句中進行清理工作称勋。
4.1.2 名稱屏蔽
如果 Java 的基類擁有某個已被多次重載的方法,那么在導(dǎo)出類中重新定義該方法名稱并不會屏蔽其在基類中的任何版本(與 C++ 不同)涯竟。
因此赡鲜,無論是在導(dǎo)出類或者它的基類中對方法進行定義空厌,重載機制都可以正常工作。
換個說法银酬,就是子類可以重載父類的方法
重載的定義是:同一類中嘲更,方法名相同,參數(shù)列表不同的一組方法
嗯揩瞪?好像哪里不對赋朦?這不是說了是"同一類"么?子類和父類是不同的類啊壮韭。
別急別急北发,此處可以理解為纹因,子類繼承了父類的方法喷屋,那么子類就包含了父類中的方法,此時在子類添加一個同名不同參的方法瞭恰,那么不就滿足重載了么屯曹。
4.2 不同的使用場景
首先,組合和繼承都允許在新的類中放置子對象惊畏,組合是顯式地這么做恶耽,而繼承是隱式的。那么二者有什么區(qū)別颜启?適用于什么場景呢偷俭?
組合
- 通常用于在新類中使用現(xiàn)有類的功能而非它的接口
即在新類中嵌入某個對象,讓其實現(xiàn)所需要的功能缰盏,但新類的用戶看到的知識為新類定義的接口涌萤,而非所嵌入對象的接口。此種情況下口猜,嵌入的對象權(quán)限聲明為 private负溪。
- 表示一種 has - a 的關(guān)系
繼承
- 通常用于 在現(xiàn)有類的基礎(chǔ)上,開發(fā)一個它的特殊版本
- 表示一種 is - a 的關(guān)系
(子類) is a (父類) -- cat is an animal.
5. 向上轉(zhuǎn)型
? 前面我們提到了济炎,繼承的重點在于突出 "新類是現(xiàn)有類的一種類型" 這一關(guān)系川抡,由于繼承確保基類中所有的方法在導(dǎo)出類中同樣有效须尚,所以能夠向基類發(fā)送的所有信息同樣也可以向?qū)С鲱惏l(fā)送崖堤。這一說法就很有趣了,我們是不是有就可以將一個 導(dǎo)出類對象的引用 作為 基類對象的引用 來使用呢耐床?
? 事實上倘感,當(dāng)然是可以的。Java 中將 導(dǎo)出類轉(zhuǎn)型成基類 的動作 稱為向上轉(zhuǎn)型咙咽。
5.1 為什么稱之為向上轉(zhuǎn)型
歷史原因使然
- 傳統(tǒng)的類繼承圖的繪制方法是:將根置于頁面的頂端老玛,然后逐漸向下。于是將導(dǎo)出類轉(zhuǎn)型成基類時,繼承圖上是上向移動的蜡豹,因此一般稱為向上轉(zhuǎn)型麸粮。
向上轉(zhuǎn)型的安全性
- 向上轉(zhuǎn)型是安全的,因為這個過程中唯一可能發(fā)生的問題是丟失方法镜廉,而不是獲取沒有的方法
向下轉(zhuǎn)型:有向上轉(zhuǎn)型弄诲,也有與之相對的 向下轉(zhuǎn)型,不過此處沒涉及到娇唯,就留到之后再說了齐遵。
5.2 再論組合與繼承
盡量多使用組合,盡量少使用繼承
一個清晰的判斷方法:是否需要從新類向基類進行轉(zhuǎn)型
- 如果需要塔插,那么繼承是必要的梗摇。
- 如果不需要,則慎重考慮是否使用繼承想许。
6. final 關(guān)鍵字
通常 final 指的是”這是無法改變的“伶授,不想做改變可能處于兩個理由:設(shè)計或效率。
final 可能有三種使用情況:數(shù)據(jù)流纹、方法 和 類糜烹。
6.1 final 數(shù)據(jù)
一句話概括:final 使數(shù)據(jù)恒定不變。
- 對于 基本數(shù)據(jù)類型:使 數(shù)值恒定不變漱凝,即常量
- 對于 對象引用:使 引用恒定不變
注意疮蹦!
引用恒定不變指的是 無法再把它改為指向另一個對象。
但是茸炒!對象本身仍可以被修改愕乎。
編譯期常量
- 帶有恒定初始值的 static final 基本數(shù)據(jù)類型
- 占據(jù)一段不能改變的存儲空間
6.1.1 空白 final
Java 允許生成 空白final,空白final 指的是 被聲明為 final 但是為給定初值的域扣典。但是無論如何妆毕,空白final 使用前必須被初始化(必須在域的定義處或者每個構(gòu)造器中用表達式對 final 進行賦值)
下面用一個例子展示 空白final 的靈活性:
public class Person{
private final String type; // 空白 final
public Person(String type){
type = type
}
public static void main(String[] args){
Person person = new Person("white");
// Person person = new Person("black");
//...
}
}
上述代碼,通過傳入的參數(shù)贮尖,進行 final 字段的初始化笛粘。
6.1.2 final 參數(shù)
Java 允許在參數(shù)列表中將參數(shù)指明為 final
- 對于對象引用:無法在方法中更改參數(shù)引用所指向的對象。
- 對于基本類型:可以讀參數(shù)湿硝,但不能修改參數(shù)
這一特性主要用來向匿名內(nèi)部類傳遞數(shù)據(jù)薪前,具體在原書第 10 章。
6.2 final 方法
final 方法的原因:
- [設(shè)計] 把方法鎖定关斜,保證繼承中方法的行為保持不變示括,并且不會被覆蓋。
- [效率] 將方法指明為 final痢畜,編譯器會將該方法的所有調(diào)用轉(zhuǎn)為內(nèi)聯(lián)調(diào)用
- JVM 會根據(jù)情況優(yōu)化內(nèi)聯(lián)
我們應(yīng)當(dāng)讓編譯器和 JVM 去處理效率問題垛膝,只有在明確禁止覆蓋時鳍侣,才將方法設(shè)置為 final
關(guān)于 private:
- 所有 private 方法都隱式指定為 final
- private 方法無法被覆蓋。
6.3 final 類
final 類之后 該類無法被繼承吼拥。
通常處于某種考慮下倚聚,使得該類不允許被繼承,對該類的設(shè)計不再做任何改動凿可,或者處于安全考慮惑折,不希望它有子類。
總結(jié)
這一章主體介紹的是類的復(fù)用枯跑,附帶著介紹了 向上轉(zhuǎn)型以及 final 關(guān)鍵字惨驶。核心還是在于面向?qū)ο缶幊痰膹?fù)用思想,具體實現(xiàn)是次要的敛助。
向上轉(zhuǎn)型不僅僅這一章介紹的這么一點粗卜,它對下一章中的多態(tài)至關(guān)重要,此處也只是拋磚引玉辜腺。
果然我還是不知道應(yīng)該總結(jié)什么休建。
就這樣吧乍恐,共勉评疗。