第6章 繼承與多態(tài)
學(xué)習(xí)目標(biāo)
- 了解繼承的目的
- 了解繼承與多態(tài)的關(guān)系
- 知道如何重寫方法
- 認(rèn)識(shí)java.lang.Object
- 簡介垃圾回收機(jī)制
6.1 何謂繼承
面向?qū)ο笾心治保宇惱^承父類,就擁有了父類的所有非私有屬性和方法,這是為了避免重復(fù)的寫相同的代碼。這在當(dāng)時(shí)可以說是一件創(chuàng)舉引谜,因?yàn)樗蟠筇岣吡舜a的可維護(hù)和可擴(kuò)展的能力课梳,但是站在今天的角度,它也帶來了內(nèi)存的無謂浪費(fèi)與性能的下降等諸多的問題介陶。如何正確判斷使用繼承的時(shí)機(jī)险绘,以及繼承之后如何活用多態(tài)踢京,才是學(xué)習(xí)繼承的重點(diǎn)。
6.1.1 繼承共同的行為
要說明繼承宦棺,最好是舉個(gè)例子來說明瓣距,其中RPG游戲是最容易來說明問題的。
我們現(xiàn)在需要設(shè)定一個(gè)戰(zhàn)士類和一個(gè)魔法師類:
先寫個(gè)戰(zhàn)士類:
public class Fighter{
private String name;//名稱
private int level;//等級(jí)
private int hp;//血量
private int mp;//魔法值
//戰(zhàn)斗方法
public void fight(){
System.out.println("戰(zhàn)士撥出了寶劍代咸!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getMp() {
return mp;
}
public void setMp(int mp) {
this.mp = mp;
}
}
再來一個(gè)魔法師類:
public class Fighter{
private String name;//名稱
private int level;//等級(jí)
private int hp;//血量
private int mp;//魔法值
//戰(zhàn)斗方法
public void fight(){
System.out.println("魔法師揮動(dòng)了他的魔杖蹈丸!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getMp() {
return mp;
}
public void setMp(int mp) {
this.mp = mp;
}
}
等會(huì)兒,我又有不好的感覺了呐芥,這兩類的成員變量都是一樣的逻杖,代碼又是重復(fù)的!
我們可以仔細(xì)想想思瘟,其實(shí)戰(zhàn)士或者魔法師荸百,它們都是游戲中的一個(gè)"角色",所以我們可以寫一個(gè)父類Role(角色)滨攻,放所有相同的部分都放到里面够话,然后再用子類Fighter和Magic繼承Role,子類里不用寫一句代碼铡买,就繼承了父類里的成員變量和方法了。象這樣:
/**
*父類:角色
*/
public class Role{
private String name;//名稱
private int level;//等級(jí)
private int hp;//血量
private int mp;//魔法值
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getMp() {
return mp;
}
public void setMp(int mp) {
this.mp = mp;
}
}
記住霎箍,父類Role僅僅把子類中共同的部分移了進(jìn)來奇钞。
然后是子類Fighter,它要繼承Role:
public class Fighter extends Role{
public void fight(){
System.out.println("戰(zhàn)士撥出了寶劍漂坏!");
}
}
這里有一個(gè)關(guān)鍵字:extends
景埃,這表示Fighter會(huì)擴(kuò)展Role的代碼,意思就是首先Fighter獲得了Role的非私有代碼顶别,同時(shí)Fighter還可以添加新的代碼(比如fight())谷徙。
魔法師類也是一樣的:
public class Magic extends Role{
public void fight(){
System.out.println("魔法師揮動(dòng)著他的魔杖!");
}
}
Magic同樣繼承了Role的代碼驯绎,同時(shí)添加了新的fight()方法完慧。
說明:在圖6.1中,為UML的類圖剩失,每個(gè)框都有三格屈尼,最上格為類名册着;中間格為屬性名,前面的減號(hào)代表private脾歧。冒號(hào)(:)后面是數(shù)據(jù)類型的名稱甲捏;最下格為方法名稱,加號(hào)代表public鞭执∷径伲空心的三角代表繼承關(guān)系,三角指向的是父類兄纺。
圖6.1 類圖
我們寫段代碼測試一下:
/**
* 角色游戲測試
* @author mouyong
*/
public class RoleTest {
public static void main(String[] args){
/*****************戰(zhàn)士測試**********************/
Fighter f1=new Fighter();
f1.setName("戰(zhàn)士小中");
f1.setLevel(1);
f1.setHp(100);
f1.setMp(0);
System.out.println("戰(zhàn)士測試輸出:");
System.out.println("姓名:"+f1.getName());
System.out.println("等級(jí):"+f1.getLevel());
System.out.println("血量:"+f1.getHp());
System.out.println("魔法值:"+f1.getMp());
/******************魔法師測試********************/
Magic m1=new Magic();
m1.setName("大魔法師默然");
m1.setLevel(120);
m1.setHp(100);
m1.setMp(100000);
System.out.println("\n\n\n魔法師測試輸出:");
System.out.println("姓名:"+m1.getName());
System.out.println("等級(jí):"+m1.getLevel());
System.out.println("血量:"+m1.getHp());
System.out.println("魔法值:"+m1.getMp());
}
}
我們可以看到大溜,在Fighter和Magic類并沒有定義姓名,等級(jí)囤热,血量和魔法值這些屬性猎提,也沒有定義它們的讀取和設(shè)置方法,可是我們?nèi)匀豢梢允褂胒1和m1兩個(gè)對(duì)象使用這些屬性和方法旁蔼,并得出正確的結(jié)果锨苏。這就是繼承的力量,不需要重復(fù)寫同樣的代碼棺聊,就可以使用它們伞租。
輸出結(jié)果:
圖6.2 角色游戲測試輸出界面
6.1.2 多態(tài)與"是一個(gè)"
繼承可以讓我們避免類間重復(fù)的代碼定義,同時(shí)限佩,它還帶來很更多的"智能"葵诈。
在3.1.4我們講過類型轉(zhuǎn)換。我們說祟同,當(dāng)類型之間兼容的時(shí)候作喘,我們可以進(jìn)行兩種類型轉(zhuǎn)換,一種是自動(dòng)類型轉(zhuǎn)換晕城,由編譯自動(dòng)幫我們完成泞坦,一種是強(qiáng)制類型轉(zhuǎn)換,由我們強(qiáng)制聲明完成砖顷。當(dāng)一個(gè)類繼承了另一個(gè)類的時(shí)候贰锁,我們說父子類之間就是兼容的,這個(gè)時(shí)候我們就可以進(jìn)行類型轉(zhuǎn)換滤蝠。
下面的代碼我相信大家能看懂豌熄,并且知道是可以編譯通過的:
Fighter f1=new Fighter();
Magic m1=new Magic();
上面的代碼并沒有進(jìn)行類型轉(zhuǎn)換。那么我們接著看下面的代碼:
Role role1=new Fighter();
Role role2=new Magic();
如果你把上面的代碼進(jìn)行編譯物咳,你會(huì)發(fā)現(xiàn)锣险,它們能通過編譯!這是因?yàn)楫?dāng)一個(gè)類的實(shí)例對(duì)象賦值給自己的父類變量時(shí),編譯器會(huì)自動(dòng)進(jìn)行類型轉(zhuǎn)換囱持,將子類當(dāng)做父類看待夯接,也就是編譯器認(rèn)為"戰(zhàn)士是一個(gè)角色","魔法師也是一個(gè)角色"纷妆。我們知道這兩句話是正確的盔几,這就是我說的,父類和子類如果用我們?nèi)祟惖脑拋肀磉_(dá)就是"子類(戰(zhàn)士)是一個(gè)父類(角色)"的關(guān)系掩幢。當(dāng)有"是一個(gè)"的關(guān)系時(shí)逊拍,編譯器就會(huì)進(jìn)行自動(dòng)類型轉(zhuǎn)換(默然說話:換個(gè)說法:子轉(zhuǎn)父,自動(dòng)轉(zhuǎn)际邻!)芯丧。
看完自動(dòng)轉(zhuǎn)換,我們?cè)賮砜吹谌N情況:
Fight f1=new Role();
Magic m1=new Role();
在這個(gè)情況下世曾,我們看到了缨恒,我們把一個(gè)父類的對(duì)象賦值給了一個(gè)子類的變量,這會(huì)發(fā)生什么呢轮听?編譯報(bào)錯(cuò)骗露!因?yàn)榫幾g器認(rèn)為"角色是一個(gè)戰(zhàn)士"和"角色是一個(gè)魔法師"并不是正確的,所以它報(bào)錯(cuò)了血巍。(默然說話:你可以這樣理解萧锉,每個(gè)人總想裝爹,但是爹卻是不愿意裝兒子的述寡。也許這樣可以幫助你記住這個(gè)規(guī)則)柿隙。
再來看一個(gè)在現(xiàn)實(shí)代碼中會(huì)存在的情況:
Role r1=new Fighter();
Fighter f1=r1;
第一句代碼我們已經(jīng)解釋過了,它是可以成功通過編譯的(子轉(zhuǎn)父鲫凶,自動(dòng)轉(zhuǎn))禀崖,但是第二句代碼呢?它是不能通過編譯的螟炫。這時(shí)你肯定會(huì)覺得奇怪波附,這個(gè)角色對(duì)象就是一個(gè)戰(zhàn)士呀,為何不行呢不恭?因?yàn)榫幾g器是不會(huì)結(jié)合上一句代碼來看第二句代碼的叶雹,所以在編譯器看來财饥,第二句是有可能出錯(cuò)的(父轉(zhuǎn)子换吧,不愿意),所以它不會(huì)自動(dòng)轉(zhuǎn)換钥星,那么如果我們一定要完成這個(gè)轉(zhuǎn)換呢沾瓦?我們可以改成這樣:
Role r1=new Fighter();
Fighter f1=(Fighter)r1;
大家已經(jīng)看到了,第二句的語法就是我們?cè)诘谌绿岬降?強(qiáng)制類型轉(zhuǎn)換"的語法,相當(dāng)于我們告訴編譯器:"伙計(jì)贯莺,你放心风喇,出了問題我負(fù)責(zé)!"缕探。于是編譯器就會(huì)去嘗試完成類型轉(zhuǎn)換魂莫。(默然說話:你可以記下這句口訣:子轉(zhuǎn)父,不安全爹耗,需強(qiáng)制)
強(qiáng)制轉(zhuǎn)換在任意的父類轉(zhuǎn)子類時(shí)均可以進(jìn)行耙考,編譯都能通過,但是并不保證能成功執(zhí)行潭兽,比如下面的代碼:
Role r1=new Magic();
Fighter f1=(Fighter)r1;
這兩句代碼均是可以通過編譯的倦始,雖然我們能發(fā)現(xiàn)第二句代碼是有問題的,因?yàn)閞1其實(shí)不是戰(zhàn)士山卦,是一個(gè)魔法師鞋邑。但是因?yàn)槲覀兟暶髁藦?qiáng)制類型轉(zhuǎn)換,于是編譯器本著"你說的账蓉,你負(fù)責(zé)枚碗!"的態(tài)度閉上了錯(cuò)誤提醒的嘴,于是我們需要承擔(dān)的后果就是剔猿,執(zhí)行報(bào)錯(cuò)视译。
圖6.3 類型轉(zhuǎn)換失敗:不能將Magic轉(zhuǎn)為Fighter
在執(zhí)行的時(shí)候我們會(huì)看到紅色的報(bào)錯(cuò)信息(默然說話:額归敬,那個(gè)綠色的字是我PS上去的酷含,不要誤以為你的報(bào)錯(cuò)信息里會(huì)有那行綠色的字哦{尷尬臉})。
總結(jié)一下:父轉(zhuǎn)子汪茧,自動(dòng)轉(zhuǎn)椅亚,子轉(zhuǎn)父,不安全舱污,需強(qiáng)制呀舔。如果強(qiáng)制轉(zhuǎn)換時(shí)類型真的不對(duì),會(huì)出現(xiàn)ClassCastException
(類轉(zhuǎn)換異常)的運(yùn)行時(shí)異常拋出扩灯。
前面花了很多的篇幅來講"是一個(gè)"的原理媚赖,講自動(dòng)轉(zhuǎn)換與強(qiáng)制轉(zhuǎn)換的原則并非只是在玩語法的游戲,而是為"多態(tài)"的實(shí)現(xiàn)鋪平理論的道路珠插。只有在了解了自動(dòng)轉(zhuǎn)換與強(qiáng)制轉(zhuǎn)換的原則之后惧磺,我們才有可能寫出更靈活的代碼。
例如捻撑,有這樣一道題目是做為游戲一定要做的磨隘,就是顯示角色的血量缤底,魔法值。我們很可能會(huì)這樣來完成:
public void showBlood(Fighter f){
System.out.println("姓名:"+f.getName+"血量:"+f.getHp()+"魔法值:"+f.getMp());
}
public void showBlood(Magic m){
System.out.println("姓名:"+m.getName+"血量:"+m.getHp()+"魔法值:"+m.getMp());
}
不錯(cuò)不錯(cuò)番捂,現(xiàn)學(xué)現(xiàn)用呀个唧。前面才講過方法重載,我們這里就用上了设预,真的很棒哦徙歼!不過,別高興得太早鳖枕,我們來設(shè)想一個(gè)很實(shí)際的問題:我們這里只有兩個(gè)角色鲁沥,而一個(gè)實(shí)際的游戲中很可能有幾百個(gè)角色,按我們現(xiàn)在的思路耕魄,我們就得重載幾百個(gè)方法來顯示不同角色的血量画恰?寫幾百個(gè)方法倒也罷了,因?yàn)楫吘棺罱K我們的程序里肯定會(huì)有上千個(gè)方法吸奴,問題是這幾百個(gè)方法內(nèi)的代碼非常相似允扇,幾乎是重復(fù)的,這完全違背我們"任何代碼只寫一遍"的原則呀则奥。
我們來想想"戰(zhàn)士是一個(gè)角色"和"魔法師是一個(gè)角色"這兩句話考润,還有"父轉(zhuǎn)子,自動(dòng)轉(zhuǎn)"读处『危可以只寫下面的一個(gè)方法:
public void showBlood(Role r){
System.out.println("姓名:"+r.getName+"血量:"+r.getHp()+"魔法值:"+r.getMp());
}
因?yàn)镽ole是所有角色的父類,所以罚舱,我們可以把任何子類作為參數(shù)傳遞給這個(gè)方法井辜,而這個(gè)方法就可以輸出任何角色的姓名,血量和魔法值管闷,這包括目前還不存在的幾百個(gè)角色粥脚,唯一的要求,就是它們需要繼承自Role包个。這就是"多態(tài)"的寫法嗅剖。下面是具體實(shí)現(xiàn)的完整代碼:
/**
* 測試多態(tài)方法showBlood豁生,通過設(shè)置傳入的參數(shù)為父類璧榄,可以方便的適應(yīng)多變的角色供嚎。
* @author mouyong
*/
public class Game {
public void showBlood(Role r){
System.out.println("姓名:"+r.getName()+"血量:"+r.getHp()+"魔法值:"+r.getMp());
}
public static void main(String[] args){
Game test=new Game();
Fighter f1=new Fighter();
f1.setName("戰(zhàn)士小中");
f1.setLevel(1);
f1.setHp(100);
f1.setMp(0);
Magic m1=new Magic();
m1.setName("大魔法師默然");
m1.setLevel(120);
m1.setHp(100);
m1.setMp(100000);
//顯示戰(zhàn)士小中的血量
test.showBlood(f1);
//顯示魔法師的血量
test.showBlood(m1);
}
}
下面是運(yùn)行結(jié)果:
圖6.4 多態(tài)方法運(yùn)行結(jié)果
多態(tài)的意思,就是"一個(gè)方法糯而,多種實(shí)現(xiàn)"天通。按字面意思,前面學(xué)過的方法重載也是多態(tài)實(shí)現(xiàn)的一種方式歧蒋,這里講到的利用父類參數(shù)的例子土砂,也是多態(tài)實(shí)現(xiàn)的典型例子。后面我們還會(huì)接著講方法重寫谜洽,它是多態(tài)實(shí)現(xiàn)的第三種方式萝映。
6.1.3 方法重寫
我們接下來完成游戲中的另一個(gè)功能,完成游戲中任意角色的攻擊調(diào)用阐虚。根據(jù)剛剛才學(xué)習(xí)過的思路序臂,我想我們可以寫這樣一個(gè)方法:
public void attack(Role r){
r.fight();
}
然后我們得到了一個(gè)編譯器的報(bào)錯(cuò)信息:Role中找不到fight()方法!是的实束,fight()方法被定義在戰(zhàn)士和魔法師兩個(gè)子類中奥秆,Role中并沒有這個(gè)方法,所以我們不可能使用Role來調(diào)用的咸灿。但我們可以觀察到另一個(gè)特點(diǎn)构订,無論是戰(zhàn)士,還是魔法師避矢,fight()方法的聲明都是這樣的:
public void fight()
也就是說悼瘾,方法聲明是一樣的,只是方法的操作代碼不一樣审胸。所以亥宿,其實(shí)我們這可以把這個(gè)方法提升到父類方法中,象這樣砂沛。
public class Role {
//省略前面成員變量的聲明
//聲明戰(zhàn)斗方法烫扼,讓子類重寫,方便多態(tài)使用
public void fight(){
//此處無代碼
}
//省略setter和getter方法的定義
}
由于所有的攻擊都是子類才會(huì)知道的碍庵,所以我們讓父類的這個(gè)方法為空方法映企,然后在子類中重新定義它的執(zhí)行代碼,
在繼承父類之后静浴,在子類中將父類的方法重新進(jìn)行定義卑吭,我們稱為方法重寫(Override
)。
由于Role定義了fight()方法(雖然方法體一行代碼也沒有)马绝,編譯器就不會(huì)找不到fight()方法了豆赏,此時(shí)就可以繼承利用我們前面所學(xué)的多態(tài)了。
/**
* 測試多態(tài)方法showFight富稻,通過設(shè)置傳入的參數(shù)為父類掷邦,可以方便的適應(yīng)多變的角色。
* @author mouyong
*/
public class Game {
//省略前面顯示血量的方法定義….
//顯示戰(zhàn)斗的方法定義椭赋,使用父類參數(shù)實(shí)現(xiàn)多態(tài)
public void showFight(Role r){
r.fight();
}
public static void main(String[] args){
Game test=new Game();
Fighter f1=new Fighter();
f1.setName("戰(zhàn)士小中");
f1.setLevel(1);
f1.setHp(100);
f1.setMp(0);
Magic m1=new Magic();
m1.setName("大魔法師默然");
m1.setLevel(120);
m1.setHp(100);
m1.setMp(100000);
//顯示戰(zhàn)士小中的戰(zhàn)斗
test.showFight(f1);
//顯示魔法師的戰(zhàn)斗
test.showFight(m1);
}
}
程序執(zhí)行結(jié)果也表明Java非常的智能抚岗,你傳給它 Fighter,它就調(diào)用Fighter的fight()方法哪怔,你傳給它Magic宣蔚,它就調(diào)用Magic的方法向抢,結(jié)果如下:
圖6.5 方法重寫測試,智能完成子類重寫方法的調(diào)用
子類重寫父類一個(gè)方法時(shí)胚委,必須要注意到方法的方法名挟鸠,參數(shù)和返回值必須一模一樣。這是一個(gè)鎖碎的工作亩冬,特別是針對(duì)我們這邊非英語國家的學(xué)生來說艘希,真是一個(gè)地獄般的考驗(yàn)與修煉(默然說話:耶!我來自地獄硅急,我居然活著出來了覆享!),這種鎖碎的工作营袜,我們程序員一定要養(yǎng)成習(xí)慣撒顿,重復(fù)鎖碎的活計(jì),交給機(jī)器去辦荚板。自從JDK5加入了注解(Annotation)之后核蘸,這個(gè)檢查是不是做了正確的方法重寫的任務(wù),總算可以給機(jī)器去完成了啸驯。
/**
* 戰(zhàn)士,加入了方法重寫的注解@Override
* @author mouyong
*/
public class Fighter extends Role {
//戰(zhàn)士戰(zhàn)斗的方法
//@Override注解表示讓編譯器檢查此方法是否為方法重寫
@Override
public void fight(){
System.out.println("戰(zhàn)士撥出了寶劍客扎!");
}
}
@Override
這個(gè)注解表示讓編譯器檢查這個(gè)方法是不是一個(gè)父類方法的重寫,如果不是罚斗,則給出報(bào)錯(cuò)信息提示徙鱼。(默然說話:這報(bào)錯(cuò)信息明顯不是一個(gè)中國人翻譯的,完全不明顯它要表達(dá)什么针姿,我懷疑這也是由機(jī)器來翻譯的袱吆!正確的翻譯應(yīng)該是"此方法沒有重寫或者父類的方法")
圖6.6 錯(cuò)誤重寫引發(fā)的報(bào)錯(cuò)信息(天坑,這是哪國人的翻譯距淫?=嗜蕖)
如果要重寫父類的某個(gè)方法,加上@Override注解榕暇,寫錯(cuò)方法名蓬衡,機(jī)器就會(huì)告訴你了。關(guān)于注解彤枢,我們?cè)诘?8章詳細(xì)說明狰晚。
6.1.4 抽象方法、抽象類
一方面缴啡,Role類中的fight方法就象這樣空著不寫壁晒,不免讓人覺得奇怪。(默然說話:在實(shí)際當(dāng)中业栅,其實(shí)有很多的時(shí)候都會(huì)有空著不寫的方法存在的秒咐,這是一個(gè)避免不了的事實(shí)谬晕。)另一方面,由于沒有提示携取,我們真的很難保證一次就寫對(duì)這個(gè)方法的定義攒钳。(默然說話:不要說我們這些非英語國家的人民,就算是英語國的人民們也深受折磨歹茶。名字稍復(fù)雜,免不了進(jìn)行反復(fù)核對(duì)你弦,即使你使用了@Override惊豺,它也僅只能告訴你有沒有錯(cuò),卻不能告訴你錯(cuò)在哪里)為了解決這一問題禽作,Java引入了抽象方法的概念尸昧。
如果某個(gè)方法的確不知道應(yīng)該寫什么,Java允許你不寫一對(duì)大括號(hào)({})旷偿,直接分號(hào)結(jié)束它就好了烹俗,唯一的代價(jià)是,你需要在返回值前面加上關(guān)鍵字abstract
(抽象)萍程,以聲明它是一個(gè)抽象方法幢妄。
還要付出的一個(gè)代價(jià)是,你的類也要在關(guān)鍵字class
前面加上abstract
關(guān)鍵字茫负。以聲明它是一個(gè)抽象類蕉鸳。
//含有抽象方法的類必須聲明為抽象類,不能被實(shí)例化(new)
public abstract class Role {
//省略成員變量的聲明
//省略setter與getter方法定義
//聲明抽象戰(zhàn)斗方法忍法,沒有方法體潮尝,直接分號(hào)結(jié)束。
//抽象方法必須讓子類重寫饿序,否則報(bào)錯(cuò)
public abstract void fight();
}
類中如果有方法被聲明為抽象方法勉失,則說明這個(gè)方法沒有可執(zhí)行的代碼,是不完整的原探,帶有不完整方法的類也不應(yīng)該進(jìn)行實(shí)例化(new)乱凿,這也就是當(dāng)一個(gè)類聲明了抽象方法后,這個(gè)類本身也必須聲明為抽象的原因咽弦。如果你硬要實(shí)例化(new)一個(gè)對(duì)象告匠,那等待你的自然就是編譯器的報(bào)錯(cuò)信息。
圖6.7 實(shí)例化一個(gè)抽象類的結(jié)果:報(bào)錯(cuò)信息
如果一個(gè)子類繼承了一個(gè)抽象類离唬,那這個(gè)子類就必須要實(shí)現(xiàn)這個(gè)抽象類聲明的所有抽象方法(默然說話:是的后专,必須實(shí)現(xiàn)所有的抽象方法,一個(gè)都不能少输莺!)戚哎,這個(gè)時(shí)候你有兩個(gè)選擇裸诽,一個(gè)是繼續(xù)聲明方法為抽象方法(默然說話:額,我不覺得這個(gè)可以選型凳,因?yàn)橥瑫r(shí)你就要把你的類也弄成抽象的丈冬,而抽象類又不能new,你寫一個(gè)抽象類繼承另一個(gè)抽象類搞毛線甘畅?)埂蕊,另一個(gè)是就是重寫這個(gè)抽象方法。如果你沒有疏唾,比如你只重寫了部分抽象方法蓄氧,并沒有全部都實(shí)現(xiàn),那你也會(huì)收到一個(gè)編譯器的報(bào)錯(cuò)信息槐脏。
圖6.8 未重寫(圖中叫"未覆蓋")抽象方法的報(bào)錯(cuò)信息
(默然說話:耶喉童!我看到了方法的名字,現(xiàn)在我知道哪個(gè)方法沒有重寫了顿天!另外堂氯,我還發(fā)現(xiàn),現(xiàn)在的IDE工具都可以幫助我進(jìn)行重寫牌废,這樣我就不用再浪費(fèi)時(shí)間去核對(duì)這該死的方法名了咽白!)
圖6.9 現(xiàn)在的IDE都提供了幫助我們改正錯(cuò)誤的辦法,只要輕輕一點(diǎn)鸟缕!
6.2 繼承語法細(xì)節(jié)
前面簡單介紹了繼承的語法局扶,下面來具體對(duì)一些細(xì)節(jié)做一些說明。
6.2.1 protected成員
前面我們寫了顯示血量的方法叁扫,這個(gè)方法其實(shí)蠻麻煩的三妈,因?yàn)槲覀€(gè)人覺得,血量等等信息應(yīng)該是由對(duì)象自身來告訴我們莫绣,而不是應(yīng)該在另外的方法中去依次獲得的畴蒲。所以,我們可以為戰(zhàn)士和魔法師兩個(gè)類分別添加toString()方法对室,如下模燥。
public class Magic extends Role {
//省略其他代碼
//toString方法專為輸出信息而設(shè)置
public String toString(){
return String.format("姓名:%s 血量:%d 魔法值:%d", this.getName(),this.getHp(),this.getMp());
}
}
public class Fighter extends Role {
//省略戰(zhàn)士戰(zhàn)斗的方法
//toString方法專為輸出信息而設(shè)置
public String toString(){
return String.format("姓名:%s 血量:%d 魔法值:%d", this.getName(),this.getHp(),this.getMp());
}
}
這樣修改之后,我們的測試類就可以很簡捷的寫成這樣:
public void showBlood(Role r){
System.out.println(r);
}
但是每次都要寫getName()這樣來獲得成員變量的值真的好麻煩呀掩宜,能不能直接使用成員變量的名字呢蔫骂?目前不行,因?yàn)檫@些成員變量都被設(shè)為private牺汤,如果改為public又不是我們想要的辽旋,我們只是想在子類里可以直接訪問這些成員變量,并不想讓所有的類都可以輕易訪問它們。Java為我們提供了第三個(gè)關(guān)鍵字:protected
补胚,它可以限制其他的類不能訪問码耐,但是子類可以直接訪問父類的protected成員。(默然說話:對(duì)的溶其,和private與public一樣骚腥,protected不僅可以修飾成員變量,同樣也可以修飾成員方法瓶逃。)象這樣束铭。
package cn.speakermore.ch06;
/**
* 用于講解類的繼承
* 父類:角色
* @author mouyong
*/
public abstract class Role {
protected String name;//名稱
protected int level;//等級(jí)
protected int hp;//血量
protected int mp;//魔法值
//略。厢绝。契沫。。
}
加了protected的類成員代芜,同一個(gè)包中的類可以訪問埠褪,不同包下的子類也可以訪問∨ɡ現(xiàn)在我們可以這樣來寫Fighter類了挤庇。
package cn.speakermore.ch06;
/**
* 戰(zhàn)士
* @author mouyong
*/
public class Fighter extends Role {
//……
public String toString(){
return String.format("姓名:%s 血量:%d 魔法值:%d", this.name,this.hp,this.mp);
}
}
當(dāng)然,Magic也可以同樣進(jìn)行修改了贷掖,這里就不列出代碼了嫡秕。
提示:基于程序可讀性,以及充分利用IDE的提示功能苹威,強(qiáng)烈建議使用this.成員的形式書寫代碼
關(guān)鍵字 | 類內(nèi)部 | 相同包 | 不同包 |
---|---|---|---|
public | 可訪問 | 可訪問 | 可訪問 |
protected | 可訪問 | 可訪問 | 子類可訪問 |
不寫關(guān)鍵字(默認(rèn)) | 可訪問 | 可訪問 | 不可訪問 |
private | 可訪問 | 不可訪問 | 不可訪問 |
Java的三個(gè)訪問修飾符均登場了昆咽,它們是public
、protected
和private
牙甫。如果你一個(gè)都沒有寫掷酗,那類的成員就擁有包訪問權(quán)限,這個(gè)權(quán)限我們稱為默認(rèn)權(quán)限窟哺。同一個(gè)包內(nèi)的類均可以訪問默認(rèn)權(quán)限的類成員泻轰。表6.1列出了他們的權(quán)限范圍:
表6.1 訪問修飾符與訪問權(quán)限
關(guān)鍵字 | 類內(nèi)部 | 相同包 | 不同包 |
---|---|---|---|
public | 可訪問 | 可訪問 | 可訪問 |
protected | 可訪問 | 可訪問 | 子類可訪問 |
不寫關(guān)鍵字(默認(rèn)) | 可訪問 | 可訪問 | 不可訪問 |
private | 可訪問 | 不可訪問 | 不可訪問 |
提示:此張表看上去很復(fù)雜,也不是很好背且轨,可以比較簡單地記住它們的使用規(guī)則浮声,大部分情況下會(huì)使用public,它可以無限制訪問旋奢,不愿意給訪問的就寫private泳挥,通常成員變量都是private的,只想給子類訪問的就寫protected至朗。
6.2.2 方法重寫的細(xì)節(jié)
在前面屉符,我們?cè)贔ighter和Magic重寫了toString()方法(默然說話:等會(huì)兒!toString()方法在Role里可沒有!這種說法不對(duì)筑煮!)辛蚊,我們注意到,它們的代碼又是一樣的真仲,那只要是一樣的袋马,是不是可以直接寫在父類里呢?我們來試試秸应。Role里添加toString()虑凛,象這樣:
package cn.speakermore.ch06;
public abstract class Role {
/**
* 在Role中重寫toString()
* 此方法添加在Role類的最后,前面的代碼省略
* @return
*/
@Override
public String toString(){
return String.format("姓名:%s 血量:%d 魔法值:%d", this.name,this.hp,this.mp);
}
}
(默然說話:天呀软啼,它居然加了@Override注解桑谍!居然沒有錯(cuò)!)
然后刪掉Fighter與Magic里的toString方法定義祸挪,運(yùn)行測試類锣披,看看是什么結(jié)果?
圖6.10 運(yùn)行結(jié)果與前面一樣贿条,沒有變化
(默然說話:Java真的好智能雹仿,這都能對(duì)!)我們發(fā)現(xiàn)運(yùn)行的結(jié)果和前面是一樣的整以!又一次把重復(fù)的代碼變得只寫一遍胧辽,感覺真的很好。
不過公黑,我總覺得應(yīng)該再做點(diǎn)什么邑商。在這個(gè)角色信息輸出中,似乎應(yīng)該要顯示出角色的類型凡蚜,不然人家取角色名的時(shí)候沒有加角色的類型人断,我們就不知道他是一個(gè)什么樣的角色了。對(duì)朝蜘!就這樣辦恶迈。
看來我們還是得重新為Fighter重寫toString()方法。不過芹务,這次重寫與前面不一樣蝉绷,因?yàn)镽ole中的toString()已經(jīng)寫好了角色基本信息了,所以我們只要在子類的toString()里獲得父類的toString()方法返回字符串枣抱,再連接上角色類型信息就可以了熔吗。問題來了,如何在子類里指定調(diào)用父類的方法呢佳晶?我們可以使用super
關(guān)鍵字桅狠,象這樣:
package cn.speakermore.ch06;
public class Fighter extends Role {
//省略前面的代碼
@Override
public String toString(){
//super表示父類對(duì)象
return "戰(zhàn)士:["+super.toString()+"]";
}
}
戰(zhàn)士寫完,魔法師也一樣:
package cn.speakermore.ch06;
public class Magic extends Role {
@Override
public String toString(){
return "魔法師:["+super.toString()+"]";
}
}
來看看輸出結(jié)果:
圖6.11 修改toString()后的的執(zhí)行結(jié)果
耶!成功的在父類的字符串前加上了角色的名稱中跌!
super
的意思就是"我爹"咨堤。指當(dāng)前對(duì)象的父類對(duì)象(默然說話:對(duì)的,是一個(gè)對(duì)象漩符,不是父類一喘。所以super關(guān)鍵字擁有所有對(duì)象的特點(diǎn),比如嗜暴,只能調(diào)用非private修飾的成員變量或方法凸克。)
方法重寫要注意一個(gè)問題,就是方法重寫的訪問修飾符只能擴(kuò)大闷沥,不能縮小萎战。所以,如果聲明為public舆逃,就只能寫為public了蚂维。
圖6.12 重寫不能縮小訪問修飾權(quán)限
關(guān)與重寫,有個(gè)小細(xì)節(jié)必須提及路狮。就是關(guān)于前面提到的虫啥,關(guān)于"方法重寫要求方法的返回值,方法名稱览祖,參數(shù)列表完全一致"孝鹊,在JDK5之后炊琉,你可以聲明返回值為原來返回值的子類展蒂。例如,我們有兩個(gè)類苔咪,Animal是父類锰悼,Cat是子類。我們?cè)谑褂盟鼈冏龇祷刂禃r(shí)团赏,有一個(gè)方法定義如下:
public Animal getSome(){}箕般。
在JDK5之前,如果我重寫這個(gè)方法如下:
public Cat getSome(){}
是會(huì)報(bào)錯(cuò)的舔清,但是JDK5之后卻不報(bào)錯(cuò)了丝里。
提示:static方法不存在重寫,因?yàn)閟tatic方法均為類方法体谒,是公有成員杯聚,所以如果子類中定義了相同返回值、方法名抒痒、參數(shù)列表的方法時(shí)幌绍,也僅只屬于子類,并非方法重寫。
6.2.3 再看構(gòu)造方法
如果類有繼承關(guān)系傀广,則在實(shí)例化子類對(duì)象的時(shí)候颁独,會(huì)先實(shí)例化父類對(duì)象。也就是說伪冰,會(huì)先執(zhí)行父類的初始化過程誓酒,然后再執(zhí)行子類的初始化過程。
由于構(gòu)造方法是可以重載的贮聂,所以子類也可以指定調(diào)用父類的某個(gè)重載的構(gòu)造方法丰捷,如果子類沒有指定,則默認(rèn)調(diào)用無參構(gòu)造方法(默然說話:這個(gè)時(shí)候寂汇,如果你的父類沒有無參構(gòu)造方法病往,那就麻煩了,子類無法實(shí)例化了骄瓣。所以如果你進(jìn)行了構(gòu)造方法的重載停巷,請(qǐng)務(wù)必寫上無參的構(gòu)造方法,即使打一對(duì)空的大括號(hào)也行榕栏,這可以防止很多Java的高級(jí)特性(如反射機(jī)制)無法進(jìn)行的問題)畔勤。
如果想要在子類中指定調(diào)用父類的構(gòu)造方法,可以使用super()
的語法扒磁。要注意的是庆揪,super()
只能寫在子類構(gòu)造方法中,而且必須是構(gòu)造方法中的第一行妨托。你可以在super()
中添加入?yún)?shù)缸榛,這樣Java就會(huì)智能的識(shí)別對(duì)應(yīng)的父類重載的構(gòu)造方法進(jìn)行調(diào)用了。來看例子:
首先兰伤,我們編寫了一個(gè)父類Father内颗,它有兩個(gè)構(gòu)造方法,默認(rèn)的敦腔,和帶一個(gè)整型參數(shù)的:
package cn.speakermore.ch06;
/**
* 構(gòu)造方法調(diào)用順序的教學(xué)類,
* 父類均澳,擁有兩個(gè)構(gòu)造方法
* @author mouyong
*/
public class Father {
public Father(){
System.out.println("這是Father無參構(gòu)造方法");
}
public Father(int a){
System.out.println("這是Father有參構(gòu)造方法,它傳入了"+a);
}
}
然后我們?cè)倬帉憙蓚€(gè)子類符衔,Son和Son2找前。其中Son用來測試默認(rèn)情況下的調(diào)用順序:
package cn.speakermore.ch06;
/**
* 用于測試默認(rèn)構(gòu)造方法調(diào)用的測試類
* 這是一個(gè)子類
* @author mouyong
*/
public class Son extends Father {
public Son(){
//這里沒有使用super(),但是編譯器會(huì)默認(rèn)添加調(diào)用父類的無參構(gòu)造方法
//super();
System.out.println("這是Son的無參構(gòu)造函數(shù)");
}
}
而Son2,是用來測試指定調(diào)用父類一個(gè)參的構(gòu)造方法的(使用super(3)
這條語句來指定):
package cn.speakermore.ch06;
/**
* 用于測試使用super()調(diào)用指定的父類構(gòu)造方法的子類判族,
* 另一個(gè)子類
* @author mouyong
*/
public class Son2 extends Father {
public Son2(){
//通過傳遞一個(gè)整型數(shù)躺盛,指定調(diào)用父類中帶一個(gè)整形參數(shù)的構(gòu)造方法
super(3);
System.out.println("這是Son2的無參構(gòu)造方法");
}
}
最后,使用一個(gè)測試類五嫂,對(duì)它進(jìn)行測試:
package cn.speakermore.ch06;
/**
* 父子類構(gòu)造方法調(diào)用的測試
* @author mouyong
*/
public class FatherAndSonTest {
public static void main(String[] args){
//測試默認(rèn)情況下颗品,構(gòu)造方法的調(diào)用順序
new Son();
System.out.println("============漂亮的分割線================");
//測試在子類中指定調(diào)用父類某個(gè)構(gòu)造方法的調(diào)用順序
new Son2();
}
}
執(zhí)行的結(jié)果如下圖:
圖6.13 繼承下的初始化代碼執(zhí)行順序及指定父類的構(gòu)造方法
我們可以看到肯尺,第一個(gè)new Son()調(diào)用了Father的無參構(gòu)造方法,而第二個(gè)new Son2()躯枢,由于使用了super(3)则吟,指定調(diào)用了Father的有參構(gòu)造方法,并收到了參數(shù)3锄蹂。
注意:由于this()和super()都要求寫在構(gòu)造方法的第一行氓仲,所以一個(gè)構(gòu)造方法中,寫了this()就不可能再寫super()得糜,同樣敬扛,寫了super()就不可能再寫this()。
6.2.4 再看final關(guān)鍵字
第三章告訴我們朝抖,可以在方法變量前添加final啥箭,讓變量的值不能再被修改,第五章又告訴我們治宣,還可以在類的成員變量前添加final急侥,讓成員變量也不能再次被修改。這里侮邀,我們要知道坏怪,在class的前面,也可以添加final绊茧,讓這個(gè)類成為太監(jiān)铝宵。(默然說話:理論上,太監(jiān)都不會(huì)再有后代了华畏。)
Java里最有名的"太監(jiān)"類鹏秋,就是我們經(jīng)常使用的String。
圖6.14 Java APIs中String的文檔描述
如果打算繼承final類唯绍,則會(huì)發(fā)生編譯錯(cuò)誤拼岳,如圖:
圖6.15 無法從最終(final)String進(jìn)行繼承
除了可以用于類的前面枝誊,final還可以用于方法的前面况芒,用來表示方法不能被子類重寫。Java中最著名的Object類里就有這樣的方法叶撒。
圖6.16 無法重寫的wait()方法
提示:Java SE API中會(huì)聲明為final的類或方法绝骚,通常都與JVM對(duì)象或操作系統(tǒng)資源管理有密切關(guān)系。所以都不希望用戶重寫這些方法祠够,以免出現(xiàn)不可預(yù)料的情況压汪,甚至破壞JVM的安全性。比如這里例舉的wait()方法,還有notify()方法等等。
圖6.17 錯(cuò)誤:不能重寫(圖中叫"覆蓋")final方法
6.2.5 java.lang.Object
在Java中霞玄,子類只能繼承一個(gè)父類雪隧,如果定義類時(shí)沒有用到extends關(guān)鍵字來指定任何父類亲铡,則會(huì)自動(dòng)繼承java.lang.Object(默然說話:現(xiàn)在知道我前面為什么說Object是"著名的"了吧国觉?Object是一切Java類的父類称开,有時(shí)候也被稱為Java類的根類八毯,因?yàn)樗蠮ava類的最頂層父類一定是Object皮获。不過它也是最可憐的焙蚓,Object沒有父類。)洒宝。
再根據(jù)我們前面說過的類對(duì)象的類型轉(zhuǎn)換規(guī)律购公,所以我們可以得出:任何一個(gè)類都可以賦值給Object類型的變量(子轉(zhuǎn)父,自動(dòng)轉(zhuǎn)):
Object o1="默然說話";
Object o2=new Date();
這樣做的好處是明顯的雁歌,壞處也是明顯的宏浩。好處就是,當(dāng)我們?cè)诰幋a的時(shí)候靠瞎,如果我們要處理的數(shù)據(jù)绘闷,它的類型要求是多種類型,這時(shí)我們就可以聲明一個(gè)Object[]類型來收集它們较坛,并做統(tǒng)一處理印蔗。Java的集合就是利用了這一點(diǎn),很輕松解決了不同數(shù)據(jù)類型的數(shù)據(jù)放在一起的難題丑勤。它的源代碼看起來大概是這樣的:
package cn.speakermore.ch06;
import java.util.Arrays;
/**
* 一個(gè)模仿Java的ArrayList功能的類
* @author mouyong
*/
public class ArrayList {
//因?yàn)樵试S集合可以任意混裝各種類型的對(duì)象华嘹,所以使用Object數(shù)組
private Object[] list;
//目前l(fā)ist數(shù)組的下標(biāo),這個(gè)下標(biāo)還沒有裝東西法竞,可以賦值耙厚。相當(dāng)于集合的長度
private int next;
/**
* 指定集合的初始長度的構(gòu)造方法
* @param capacity 一個(gè)數(shù)字,指定集合的初始長度
*/
public ArrayList(int capacity){
list=new Object[capacity];
}
/**
* 默認(rèn)構(gòu)造方法岔霸,指定了數(shù)組初始長度為16
*/
public ArrayList(){
this(16);
}
/**
* 添加對(duì)象到集合里
* @param o 被添加到集合里的對(duì)象薛躬,可以是任意對(duì)象,所以定義為Object類型
*/
public void add(Object o){
if(next==list.length){
//如果next剛好是集合的長度呆细,說明集合已經(jīng)滿了型宝,自動(dòng)擴(kuò)容到原來的2倍
list=Arrays.copyOf(list, next*2);
}
//把對(duì)象添加到數(shù)組中
list[next]=o;
//下標(biāo)移動(dòng)到下一個(gè)位置,準(zhǔn)備接收下一個(gè)元素
next++;
}
/**
* 獲得指定位置的對(duì)象
* @param index 整數(shù)絮爷,指定的集合下標(biāo)趴酣,從0開始,不應(yīng)該超過集合的最大長度坑夯。
* @return 對(duì)象岖寞,因?yàn)椴恢兰现兴b對(duì)象的具體類型,所以也被定義為Object
*/
public Object get(int index){
return list[index];
}
/**
* 獲得集合的長度
* @return 整數(shù)柜蜈,集合的長度
*/
public int size(){
return next;
}
}
自定義的ArrayList類仗谆,它使用了一個(gè)Object[]數(shù)組裝對(duì)象指巡。如果在創(chuàng)建對(duì)象時(shí)沒有指定長度,則默認(rèn)使用16隶垮。
可以通過add()方法來裝入任意對(duì)象厌处。如果原長度不夠,則自動(dòng)擴(kuò)容到原來的2倍岁疼。如果要取出對(duì)象阔涉,則使用get()方法,傳入下標(biāo)來獲取捷绒。如果想要知道有多少個(gè)對(duì)象裝在里面瑰排,則可調(diào)用 size()方法。下面是一個(gè)使用的例子暖侨。
package cn.speakermore.ch06;
import java.util.Scanner;
/**
* 測試自定義ArrayList的測試類
* @author mouyong
*/
public class ArrayListTest {
public static void main(String[] args){
//實(shí)例化自定義集合對(duì)象
ArrayList infos=new ArrayList();
//準(zhǔn)備鍵盤輸入
Scanner input=new Scanner(System.in);
//設(shè)置循環(huán)終止變量
String isQuit="";
do{
System.out.println("請(qǐng)輸入姓名:");
String name=input.nextLine();
infos.add(name);//將字符串放入集合中
System.out.println("請(qǐng)輸入年齡:");
int age=input.nextInt();
infos.add(age);//將整數(shù)放入集合中
System.out.println("是否繼續(xù)椭住?(y/n)");
isQuit=input.next();
//為解決字符輸入的bug而多寫的接受語句(想知道bug是什么樣,可以刪除此句)
input.nextLine();
}while("y".equalsIgnoreCase(isQuit));
//循環(huán)輸出集合中所有的數(shù)據(jù)
for(int i=0;i<infos.size();){
System.out.println("姓名:"+infos.get(i++));
System.out.println("年齡:"+infos.get(i++));
}
}
}
下面是具體執(zhí)行的結(jié)果:
run:
請(qǐng)輸入姓名:
默然說話
請(qǐng)輸入年齡:
44
是否繼續(xù)字逗?(y/n)
y
請(qǐng)輸入姓名:
狂獅中中
請(qǐng)輸入年齡:
10
是否繼續(xù)京郑?(y/n)
n
姓名:默然說話
年齡:44
姓名:狂獅中中
年齡:10
成功構(gòu)建 (總時(shí)間: 31 秒)
java.lang.Object是所有類的頂層父類,所以任意子類均可重寫其定義的非final方法葫掉,在現(xiàn)實(shí)中些举,我們也是這樣做的。有一些方法是經(jīng)常會(huì)被重寫的俭厚。
1. 重寫toString()
在前面的例子中户魏,我們已經(jīng)重寫過toString()方法了,它是Object經(jīng)常被重寫的一個(gè)方法挪挤,主要的作用就是用來方便我們顯示一些字符串內(nèi)容(默然說話:前面的游戲已經(jīng)大量應(yīng)用嘍叼丑。)
在Object中toString()的方法聲明是這樣的:
public String toString(){
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
現(xiàn)在還不好解釋以上代碼的具體含義,它輸出了一個(gè)類名扛门,后跟"@"鸠信,接著是十六進(jìn)制的數(shù)字(默然說話:我常告訴學(xué)生,這一串十六進(jìn)制數(shù)字與內(nèi)存有關(guān)论寨,并不是內(nèi)存地址星立,但的確是根據(jù)內(nèi)存地址換算出來的。)政基。如果你沒有重寫過toString()贞铣,那么用下面這句代碼,就會(huì)得到這樣的一個(gè)輸出沮明。
ArrayList infos=new ArrayList();
System.out.println(infos);
圖6.18 System.out.println(infos)的輸出結(jié)果
注意:如果你嘗試在你的電腦上運(yùn)行,那么后面的十六進(jìn)制數(shù)字會(huì)和我的不一樣窍奋。
2.重寫equals()
在第四章談過荐健,如果想要比較兩個(gè)對(duì)象內(nèi)容相等酱畅,不能使用==,而是要通過equals()
方法江场。而equals()
也是屬于Object類的一個(gè)方法纺酸,其源代碼是這樣的:
public boolean equals(Object obj){
return this==obj;
}
如果你能看懂,其實(shí)應(yīng)該看出來了址否,Object的equals()方法定義也是用的==餐蔬,所以,如果你不重寫equals()方法佑附,你想要的比較兩個(gè)對(duì)象內(nèi)容相等的奇跡也是不會(huì)出現(xiàn)的樊诺。如何定義eqauls()方法呢?這還真沒有統(tǒng)一的寫法音同,不過有一個(gè)模式可以借鑒词爬,如下面的代碼:
package cn.speakermore.ch06;
import java.util.Objects;
/**
* 示范equals方法重寫
* @author mouyong
*/
public class Student {
private Integer id;
@Override
public boolean equals(Object obj){
//首先,比較"我是不是我"
if(this==obj){
return true;
}
//其次权均,證明類型是不是匹配
if(!(obj instanceof Student)){
return false;
}
//排除前面兩種情況顿膨,進(jìn)入自定義部分
Student stu=(Student)obj;
//下面這句代碼的意思,我們定義了一個(gè)規(guī)則:只要id相同叽赊,我們就認(rèn)為是同一個(gè)學(xué)生
return Objects.equals(stu.getId(), this.getId());
}
/**
* @return the id
*/
public Integer getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Integer id) {
this.id = id;
}
}
上面代碼的注釋就在說明這個(gè)模式恋沃,第一步首先驗(yàn)證對(duì)象的內(nèi)存地址相不相同,之后再驗(yàn)證對(duì)象是不是同一種類型必指,最后是自定義規(guī)則芽唇,這部分就是要由你來決定如何寫的。也就是說取劫,在具體的類中匆笤,你們是如何規(guī)定"對(duì)象的內(nèi)容相同"。在這個(gè)例子里谱邪,我們規(guī)定"如果對(duì)象的id是相同的炮捧,我們就認(rèn)為兩個(gè)對(duì)象是相同的"。
此外惦银,為了完成類型比較咆课,我們使用了instanceof
關(guān)鍵字,它是一個(gè)比較運(yùn)算符扯俱,在左邊要寫一個(gè)對(duì)象變量名书蚪,在右邊要寫類的名稱,instanceof
完成比較左邊的對(duì)象與右邊類是否兼容迅栅。如果不兼容殊校,直接報(bào)語法錯(cuò)。
另外要注意的是读存,instanceof關(guān)鍵字為true的情況并非類名稱與對(duì)象名完全一致为流,類為父類也是會(huì)返回true的呕屎。
最后,通常我們重寫了eqauls()方法之后敬察,同時(shí)也會(huì)重寫hashCode()秀睛。等到第9章時(shí)我們?cè)賮碛懻摗?/p>
6.2.6 關(guān)于垃圾收集
創(chuàng)建對(duì)象就會(huì)占據(jù)內(nèi)存,這是一個(gè)常識(shí)莲祸。如果程序執(zhí)行流程中出現(xiàn)了無法使用的對(duì)象蹂安,這個(gè)對(duì)象就只是"占著茅坑不拉屎"的垃圾,它占用了內(nèi)存锐帜,卻無法使用田盈,浪費(fèi)了這些內(nèi)存。
放在以前抹估,程序員是要自己來做這件很"臟"卻經(jīng)常很難搞定的事情(默然說話:哦缠黍,"偷雞不成蝕把米"就是指這類"臟"活了吧。垃圾沒清干凈药蜻,倒留下一堆bug可真是老前輩們的"家常便飯"瓷式。其實(shí),我們經(jīng)常聽老前輩們傳說C語言如何如何難學(xué)语泽,特別是指針贸典。其實(shí)指針一點(diǎn)都不難學(xué),難的是如何確定一塊內(nèi)存已經(jīng)是垃圾了踱卵,何時(shí)釋放內(nèi)存才是正確的廊驼。這個(gè)過程中經(jīng)常寫出bug,把程序搞崩潰惋砂。這才是C語言真正的地獄模式妒挎。),于是Java提供了垃圾回收機(jī)制(Garbage Collection, 簡稱GC)西饵,專門用來處理這些垃圾酝掩。只要是程序里沒有任何一個(gè)變量引用到的對(duì)象,就會(huì)被GC認(rèn)定為垃圾對(duì)象眷柔。在CPU有空的時(shí)候期虾,或者是內(nèi)存已經(jīng)占滿的時(shí)候,GC就會(huì)自動(dòng)開始工作(這就是多線程運(yùn)作的方式驯嘱,我們?cè)诘?1章說明)镶苞。
實(shí)際要說明垃圾回收的原理是很困難的,因?yàn)樗乃惴ň秃軓?fù)雜鞠评,不同的需求還會(huì)導(dǎo)致有不同的算法茂蚓。所以作為我們來說,只要知道"JVM會(huì)幫助我們進(jìn)行內(nèi)存管理,它的名稱叫垃圾回收煌贴,簡稱GC御板,耶锥忿,太棒了牛郑!",就足夠了敬鬓。細(xì)節(jié)讓JVM工程師幫我們搞定吧淹朋。
那到底哪些是垃圾呢?下面的例子將說明這個(gè)問題钉答,先來看代碼:
Object o1=new Object();
Object o2=new Object();
o1=o2;
我們需要弄清楚的是础芍,在第一行的代碼中進(jìn)了三步操作,第一步聲明了o1變量內(nèi)存数尿,第二步創(chuàng)建了一塊內(nèi)存放Object對(duì)象仑性,第三步是賦值操作,把Object對(duì)象的內(nèi)存地址放到了o1變量中右蹦。第二行代碼也是一樣:o2變量得到了第二次new出來的Object對(duì)象的地址诊杆。這時(shí),兩個(gè)new出的對(duì)象都分別由o1和o2引用何陆,所以它們目前都不是垃圾晨汹。
圖6.19 兩個(gè)對(duì)象不是垃圾
接下來是第三行代碼,把o2的值(第二個(gè)Object對(duì)象的地址)賦值給了o1贷盲。此時(shí)o1原來的值就會(huì)被覆蓋淘这,而o1和o2兩個(gè)變量都在引用第二個(gè)對(duì)象了。第一個(gè)Object對(duì)象就沒有任何變量在引用它巩剖,它就成為了垃圾铝穷,GC就會(huì)自動(dòng)找到這樣的垃圾并予以回收。
圖6.20 第一個(gè)對(duì)象成為垃圾
6.2.7 再看抽象類
寫程序常有些看似不合理但又非得完成的需求佳魔。舉個(gè)例子曙聂,現(xiàn)在老板叫你開發(fā)一個(gè)猜數(shù)字的游戲,隨機(jī)產(chǎn)生一個(gè)1000-9999的四位數(shù)吃引,用戶輸入的數(shù)字與隨機(jī)產(chǎn)生的數(shù)字相比筹陵,如果相同就顯示"猜對(duì)了",如果不同主繼續(xù)讓用戶輸入數(shù)字镊尺,一共猜12次朦佩。
這個(gè)程序有什么難的?相信現(xiàn)在的你可以寫出來:
package cn.speakermore.ch06;
import java.util.Random;
import java.util.Scanner;
/**
* 猜數(shù)游戲:計(jì)算機(jī)產(chǎn)生一個(gè)四位數(shù)(1000-9999),由用戶來猜庐氮。<br />
* <br />
* 如果沒猜中语稠,給出"大了"或"小了"的提示,同時(shí)還給出"猜中了x個(gè)數(shù)"的提示<br />
* 最多可以猜12次。<br />
* 如果猜中了仙畦,給出猜中的提示
* @author mouyong
*/
public class Guess {
public static void main(String[] args){
Scanner input=new Scanner(System.in);
Random random=new Random();
Integer guess=random.nextInt(9000)+1000;
//用來存放電腦想出來的四位數(shù)中的每一個(gè)位置上的數(shù)字
int[] numberComputer=new int[4];
int clientInput=0;
Integer tempComputer=guess,tempClient=clientInput;
int i=0;
while(tempComputer!=0){
//將電腦想出來的四位數(shù)分為四個(gè)數(shù)字放到數(shù)組里
numberComputer[i]=tempComputer%10;
tempComputer=tempComputer/10;
i++;
}
System.out.println("我現(xiàn)在想好了一個(gè)1000-9999之間的數(shù)输涕,你可以猜12次");
for(i=0;i<12;i++){
System.out.println("第"+(i+1)+"次請(qǐng)輸入一個(gè)數(shù):");
clientInput=input.nextInt();
//如果猜對(duì)了,則結(jié)束游戲
if(clientInput==guess){
System.out.println("恭喜慨畸!你猜對(duì)了莱坎!");
System.exit(0);
}
//告訴用戶猜的數(shù)是大是小
if(clientInput>guess){
System.out.println("大了");
}else{
System.out.println("小了");
}
//告訴用戶猜中了幾個(gè)數(shù)
int count=0;
for(int j=0;j<numberComputer.length;j++){
if(numberComputer[j]==clientInput%10){
count++;
}
clientInput=clientInput/10;
}
if(count!=0){
System.out.println("你猜的數(shù)有"+count+"個(gè)");
}else{
System.out.println("你一個(gè)都沒有猜中!");
}
}
System.out.println("很遺憾寸士,沒有猜中檐什!");
}
}
我們可以做了一個(gè)挺復(fù)雜,富有挑戰(zhàn)且真的很有趣的猜數(shù)游戲哦H蹩ā(默然說話:哦乃正,這個(gè)例子來自于一次與兒子去于密室逃脫時(shí)的一個(gè)迷題。)你興沖沖的把程序交給老板婶博,準(zhǔn)備迎來一如既往的表揚(yáng)時(shí)瓮具,老板卻皺著眉頭說:"這個(gè),我們似乎不應(yīng)該在文本的狀態(tài)下執(zhí)行這個(gè)游戲呀凡人。"名党,你一楞,隨即機(jī)智的問道:"那會(huì)怎么來執(zhí)行這個(gè)程序呢划栓?"兑巾,老板一臉看到未來的迷茫樣子:"這是個(gè)好問題,不過我們還沒有完全決定忠荞,可能用窗口程序蒋歌,其實(shí)網(wǎng)頁或者app也不錯(cuò),難說我們需要造一臺(tái)專用的游戲機(jī)委煤,通過九宮按鈕直接輸入數(shù)字堂油?下周開會(huì)討論一下吧。"碧绞,于是你舒了口氣府框,說:"好吧,那我下周討論完了再寫吧讥邻。"迫靖,老板用不容置疑的口氣說:"不行!"兴使。你只好無奈的點(diǎn)點(diǎn)頭系宜,退出老板的門時(shí),你有沒有感覺到一萬只草泥馬歡快地在你的心臟里跳舞呢发魄?
這可不是一個(gè)段子(默然說話:嗯盹牧,當(dāng)然俩垃,我似乎把它寫成了段子)。在團(tuán)隊(duì)合作汰寓、多部門開發(fā)程序時(shí)口柳,有許多時(shí)候,有一定順序完成的工作必須要同時(shí)開工有滑,因?yàn)槔习迨遣豢赡荛e養(yǎng)你3個(gè)月等上一個(gè)工序完成之后跃闹,再你完成你的工作。(默然說話:對(duì)的俺孙,如果要等3個(gè)月辣卒,那直接不請(qǐng)你掷贾,讓人家直接全做完就好了睛榄。)雖然需求沒有決定,但你卻要把你的程序完成的例子太多了想帅。
有些不合理的需求场靴,本身確實(shí)不合理,但有些看似不合理的需求港准,其實(shí)可以通過設(shè)計(jì)來解決旨剥。比如上面的例子,雖然用戶輸入浅缸,顯示結(jié)果的環(huán)境未定轨帜,但你負(fù)責(zé)的部分(猜數(shù)游戲的邏輯)還是可以先操作的。我們可以這樣完成:
public abstract class GuessNumber {
public void go(){
Random random=new Random();
Integer guess=random.nextInt(9000)+1000;
//用來存放電腦想出來的四位數(shù)中的每一個(gè)位置上的數(shù)字
int[] numberComputer=new int[4];
int clientInput=0;
Integer tempComputer=guess,tempClient=clientInput;
int i=0;
while(tempComputer!=0){
//將電腦想出來的四位數(shù)分為四個(gè)數(shù)字放到數(shù)組里
numberComputer[i]=tempComputer%10;
tempComputer=tempComputer/10;
i++;
}
//所有輸出消息均替換為抽象方法衩椒,以便在將來不用修改這部分代碼
print("我現(xiàn)在想好了一個(gè)1000-9999之間的數(shù)蚌父,你可以猜12次");
for(i=0;i<12;i++){
print("第"+(i+1)+"次請(qǐng)輸入一個(gè)數(shù):");
//用戶輸入替換為抽象方法,以便在將來保證不用修改這部分代碼
clientInput=clientInput();
//如果猜對(duì)了毛萌,則結(jié)束游戲
if(clientInput==guess){
print("恭喜苟弛!你猜對(duì)了!");
System.exit(0);
}
//告訴用戶猜的數(shù)是大是小
if(clientInput>guess){
print("大了");
}else{
print("小了");
}
//告訴用戶猜中了幾個(gè)數(shù)
int count=0;
for(int j=0;j<numberComputer.length;j++){
if(numberComputer[j]==clientInput%10){
count++;
}
clientInput=clientInput/10;
}
if(count!=0){
print("你猜的數(shù)有"+count+"個(gè)");
}else{
print("你一個(gè)都沒有猜中阁将!");
}
}
print("很遺憾膏秫,沒有猜中!");
}
public abstract void print(String msg);
public abstract Integer clientInput();
}
你可以看出做盅,我們把不確定的部分(用戶的輸入與消息的輸出)替換為抽象方法缤削,這樣既解決了老板沒決定,不知道如何輸入和輸出的問題吹榴,又解決了我們寫的代碼將來也許會(huì)面臨的大量修改的問題亭敢。
等到下周開會(huì)決定了,你只需要再寫個(gè)子類腊尚,繼承GuessNumber吨拗,重寫兩個(gè)抽象方法即可。實(shí)際上你應(yīng)該已經(jīng)發(fā)現(xiàn)了,由于這兩個(gè)抽象方法劝篷,咱們的猜數(shù)代碼可以利用繼承反復(fù)重用了哨鸭。下個(gè)月開會(huì)研究,由于猜數(shù)游戲大受歡迎娇妓,我們需要進(jìn)行"全平臺(tái)"商業(yè)化像鸡,此時(shí)你只需要再寫幾個(gè)子類,繼承GuessNumber哈恰,對(duì)兩個(gè)方法做不同的重寫就夠了只估,省下的時(shí)間,為你和公司帶來了豐厚的回報(bào)着绷。你可以買更大的房子蛔钙,更漂亮的車,生更多的孩子了荠医!這就是設(shè)計(jì)的力量吁脱!
提示:設(shè)計(jì)上的經(jīng)驗(yàn),我們稱為設(shè)計(jì)模式彬向,上面的例子我們使用了"模板方法"模式兼贡。如果對(duì)其他設(shè)計(jì)模式感興趣,可以上網(wǎng)查找相關(guān)"設(shè)計(jì)模式"的資料
(默然說話:在去查找之前……先擦擦你的口水娃胆,紙弄濕了不要緊遍希,鍵盤要是濕了,說不準(zhǔn)會(huì)電你的)
6.3 重點(diǎn)復(fù)習(xí)
- 面向?qū)ο笾欣锓常宇惱^承父類凿蒜,充分進(jìn)行代碼重用是對(duì)的,但是不要為了代碼重用就濫用繼承招驴。如何正確使用繼承篙程,如果更好的活用多態(tài),才是學(xué)習(xí)繼承時(shí)的重點(diǎn)(默然說話:這似乎能講的東西太多了别厘,所以這里只說明一個(gè)你在學(xué)習(xí)過程重點(diǎn)需要關(guān)注的地方虱饿,后面我們還會(huì)具體的舉很多很多的例子來說明的,總之二十多年的編程經(jīng)驗(yàn)告訴我触趴,優(yōu)秀程序的的樣子都是長一樣的氮发,那就大家常說的六個(gè)字:"易維護(hù),易擴(kuò)展"冗懦,如果你覺得這太高大上爽冕,不接地氣,我換六個(gè)平易近人且吸引眼球的另外六個(gè)字告訴你:"少干活披蕉,多拿錢"颈畸!要知道乌奇,在寫這些文字之前,一般人我都不告訴他們的眯娱。)
- 如果出現(xiàn)代碼的反復(fù)書寫礁苗,就應(yīng)引警覺,此時(shí)可考慮的改進(jìn)之一徙缴,就是把相同的程序代碼提升為父類(默然說話:其實(shí)這是第三步试伙,第一步應(yīng)該考慮把重復(fù)代碼寫到一個(gè)獨(dú)立的方法中,第二步應(yīng)該考慮使用方法重載于样,第三步才考慮父類疏叨。)。
- 在Java中穿剖,繼承使用
extends
關(guān)鍵字蚤蔓,所有非私有屬性均會(huì)被繼承。但是私有屬性如果父類提供了公有方法携御,也可以使用昌粤。 - Java為了保持程序不出現(xiàn)"倫理道德"的爭議,只允許單繼承啄刹,即一個(gè)類有且只有一個(gè)父類(默然說話:Object例外,它沒有父類)
- 還記得父子類的類型轉(zhuǎn)換口訣么凄贩?"子轉(zhuǎn)父誓军,自動(dòng)轉(zhuǎn);父轉(zhuǎn)子,強(qiáng)制轉(zhuǎn)"疲扎。
-
abstract
表示抽象方法昵时,它使用在類和方法定義的前面。抽象方法不能寫方法體(默然說話:方法體就是那對(duì)大括號(hào)椒丧,還記得么壹甥?),子類必須重寫父類的抽象方法壶熏,否則報(bào)錯(cuò)句柠;抽象類不能實(shí)例化(默然說話:實(shí)例化就是new,new就是實(shí)例化棒假,記得了么溯职?),只能由子類來實(shí)際執(zhí)行它的功能代碼帽哑。另外谜酒,有抽象方法的類必須聲明為抽象類,否則也報(bào)錯(cuò)妻枕。 - 被聲明為
portected
的成員僻族,相同包中的類可以直接存取粘驰,不同包中的類可以在繼承后的子類直接存取。 - Java中有
public
述么、protected
晴氨、private
三個(gè)權(quán)限關(guān)鍵字,但卻有四種權(quán)限碉输,因?yàn)槟J(rèn)權(quán)限就是不寫關(guān)鍵字的時(shí)候籽前。 - 如果想在子類中指定調(diào)用父類的某個(gè)方法,可以使用super關(guān)鍵字敷钾。
- 重寫方法時(shí)要注意枝哄,在JDK5之后,方法重寫時(shí)可以返回被重寫方法返回類型的子類阻荒。
-
final
可以用于類挠锥、方法和屬性的前面,用于類的前面表示類不能被繼承(默然說話:太監(jiān)類侨赡,記得嗎蓖租?),用于方法前羊壹,表示方法不能被重寫蓖宦,用于屬性前,則意味著屬性不能被第二次賦值(默然說話:就是我們常說的常量了呢)油猫。 - 如果定義類時(shí)沒有指定任何父類稠茂,并不意味著它沒有父類,因?yàn)镴VM會(huì)自動(dòng)讓這個(gè)類繼承Object情妖。
- 對(duì)于在程序中沒有被變量引用的對(duì)象睬关,JVM會(huì)進(jìn)行垃圾收集(GC),這是非常重要的毡证,因?yàn)檫@能提高我們對(duì)內(nèi)存的使用电爹,不至于浪費(fèi)內(nèi)存。
6.4 課后練習(xí)
6.4.1 選擇題
1.如果有以下的程序片段:
class Father{
void service(){
System.out.println("父類的服務(wù)");
}
}
class Children extends Father{
@Override
void service(){
System.out.println("子類的服務(wù)");
}
}
public class Main{
public static void main(String[] args){
Children child=new Children();
child.service();
}
}
以下描述正確的是()
A. 編譯失敗
B.顯示"父類的服務(wù)"
C.顯示"子類的服務(wù)"
D.先顯示"父類的服務(wù)"料睛,后顯示"子類的服務(wù)"
2.接上題丐箩,如果main()中改為:
Father father=new Father();
father.service();
以下描述正確的是()
A. 編譯失敗
B.顯示"父類的服務(wù)"
C.顯示"子類的服務(wù)"
D.先顯示"父類的服務(wù)",后顯示"子類的服務(wù)"
3.如果有以下的程序片段:
class Test{
String ToString (){
return "某個(gè)類"
}
}
public class Main{
public static void main(String[] args){
Test test=new Test();
System.out.println(test);
}
}
以下描述正確的是()秦效。
A. 編譯失敗
B.顯示"某個(gè)類"
C.顯示"Test@XXXX",XXXX為十六進(jìn)制數(shù)
D.發(fā)生ClassCastException
4.如果有以下的程序片段:
class Test{
int hashCode (){
return 99
}
}
public class Main{
public static void main(String[] args){
Test test=new Test();
System.out.println(test.hashCode());
}
}
以下描述正確的是()雏蛮。
A. 編譯失敗
B.顯示"99"
C.顯示"0"
D.發(fā)生ClassCastException
5.如果有以下的程序片段:
class Test{
@Override
String ToString (){
return "某個(gè)類"
}
}
public class Main{
public static void main(String[] args){
Test test=new Test();
System.out.println(test);
}
}
以下描述正確的是()。
A. 編譯失敗
B.顯示"某個(gè)類"
C.顯示"Test@XXXX",XXXX為十六進(jìn)制數(shù)
D.發(fā)生ClassCastException
6.如果有以下的程序片段:
class Father{
abstract void service();
}
class Children extends Father{
@Override
void service(){
System.out.println("子類的服務(wù)");
}
}
public class Main{
public static void main(String[] args){
Father father=new Children();
child.service();
}
}
以下描述正確的是()
A. 編譯失敗
B.顯示"子類的服務(wù)"
C.執(zhí)行時(shí)發(fā)生ClassCastException
D.移除@Override可編譯成功
7.如果有以下的程序片段:
class Father{
protected int x;
Father(int x){
this.x=x;
}
}
class Children extends Father{
Children(){
this.x=x;
}
}
以下描述正確的是()
A. new Children(10)后阱州,對(duì)象成員x值為10
B.new Children(10)后挑秉,對(duì)象成員x值為0
C.Children中無法存取x,編譯失敗
D.Children中無法調(diào)用父類構(gòu)造方法苔货,編譯失敗
8.如果有以下的程序片段:
public class StringChild extends String{
public StringChild(String str){
super(str);
}
}
以下描述正確的是()
A. String s=new StringChild("測試")可通過編譯
B.StringChild s=new StringChild("測試")可通過編譯
C.因無法調(diào)用super()犀概,編譯失敗
D.因無法繼承String立哑,編譯失敗
9.如果有以下的程序片段:
class Father{
Father(){
this(10);
System.out.println("Father()");
}
Father(int x){
System.out.println("Father(x)");
}
}
class Children extends Father{
Children(){
super(10);
System.out.println("Children()");
}
Children(int y){
System.out.println("Children(y)");
}
}
以下描述正確的是()
A. new Children()顯示"Father(x)"、"Children()"
B.new Children(10)顯示"Children(y)"
C.new Father()顯示"Father(x)"姻灶、"Father()"
D.編譯失敗
10.如果有以下的程序片段:
class Father{
Father(){
System.out.println("Father()");
this(10);
}
Father(int x){
System.out.println("Father(x)");
}
}
class Children extends Father{
Children(){
super(10);
System.out.println("Children()");
}
Children(int y){
System.out.println("Children(y)");
}
}
以下描述正確的是()
A. new Children()顯示"Father(x)"铛绰、"Children()"
B.new Children(10)顯示"Father()"、"Father(x)"产喉、Children(y)
C.new Father()顯示"Father(x)"捂掰、"Father()"
D.編譯失敗
6.4.2 操作題
- 如果使用6.2.5設(shè)計(jì)的ArrayList類收集對(duì)象,想顯示所收集對(duì)象的字符串描述時(shí)曾沈,會(huì)顯示非常麻煩这嚣。嘗試重寫toString()方法,讓客戶端可以方便的顯示所收集對(duì)象的字符串描述塞俱。
- 接上題姐帚,請(qǐng)重寫ArrayList類的equals()方法,先比較收集的數(shù)量是否相等障涯,然后對(duì)應(yīng)位置比較各對(duì)象的內(nèi)容是否相等(使用各對(duì)象的equals())罐旗,只有數(shù)量相等且對(duì)應(yīng)位置的各個(gè)對(duì)象的內(nèi)容相等,才判斷兩個(gè)ArrayList對(duì)象是相等的唯蝶。