《Java8學(xué)習(xí)筆記》讀書筆記(七)

第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è)訪問修飾符均登場了昆咽,它們是publicprotectedprivate牙甫。如果你一個(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 操作題
  1. 如果使用6.2.5設(shè)計(jì)的ArrayList類收集對(duì)象,想顯示所收集對(duì)象的字符串描述時(shí)曾沈,會(huì)顯示非常麻煩这嚣。嘗試重寫toString()方法,讓客戶端可以方便的顯示所收集對(duì)象的字符串描述塞俱。
  2. 接上題姐帚,請(qǐng)重寫ArrayList類的equals()方法,先比較收集的數(shù)量是否相等障涯,然后對(duì)應(yīng)位置比較各對(duì)象的內(nèi)容是否相等(使用各對(duì)象的equals())罐旗,只有數(shù)量相等且對(duì)應(yīng)位置的各個(gè)對(duì)象的內(nèi)容相等,才判斷兩個(gè)ArrayList對(duì)象是相等的唯蝶。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末九秀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子生棍,更是在濱河造成了極大的恐慌颤霎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涂滴,死亡現(xiàn)場離奇詭異,居然都是意外死亡晴音,警方通過查閱死者的電腦和手機(jī)柔纵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锤躁,“玉大人搁料,你說我怎么就攤上這事∠敌撸” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長氮双。 經(jīng)常有香客問我布隔,道長,這世上最難降的妖魔是什么澎迎? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任庐杨,我火速辦了婚禮选调,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灵份。我一直安慰自己仁堪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布填渠。 她就那樣靜靜地躺著弦聂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪氛什。 梳的紋絲不亂的頭發(fā)上莺葫,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音屉更,去河邊找鬼徙融。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瑰谜,可吹牛的內(nèi)容都是我干的欺冀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼萨脑,長吁一口氣:“原來是場噩夢啊……” “哼隐轩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起渤早,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤职车,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后鹊杖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悴灵,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年骂蓖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了积瞒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡登下,死狀恐怖茫孔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情被芳,我是刑警寧澤缰贝,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站畔濒,受9級(jí)特大地震影響剩晴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜篓冲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一李破、第九天 我趴在偏房一處隱蔽的房頂上張望宠哄。 院中可真熱鬧,春花似錦嗤攻、人聲如沸毛嫉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽承粤。三九已至,卻和暖如春闯团,著一層夾襖步出監(jiān)牢的瞬間辛臊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工房交, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彻舰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓候味,卻偏偏與公主長得像刃唤,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子白群,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容