《Java8學習筆記》讀書筆記(6)

第6章 繼承與多態

學習目標

  • 瞭解繼承的目的
  • 瞭解繼承與多態的關係
  • 知道如何重寫方法
  • 認識java.lang.Object
  • 簡介垃圾回收機制

6.1 何謂繼承

面向對象中,子類繼承父類,就擁有了父類的所有非私有屬性和方法,這是爲了避免重複的寫相同的代碼。這在當時可以說是一件創舉,因爲它大大提高了代碼的可維護和可擴展的能力,但是站在今天的角度,它也帶來了內存的無謂浪費與性能的下降等諸多的問題。如何正確判斷使用繼承的時機,以及繼承之後如何活用多態,纔是學習繼承的重點。

6.1.1 繼承共同的行爲

要說明繼承,最好是舉個例子來說明,其中RPG遊戲是最容易來說明問題的。
我們現在需要設定一個戰士類和一個魔法師類:
先寫個戰士類:

public class Fighter{
    private String name;//名稱
    private int level;//等級
    private int hp;//血量
    private int mp;//魔法值
     //戰鬥方法
    public void fight(){
        System.out.println("戰士撥出了寶劍!");
    }

    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;
    }

}

再來一個魔法師類:

public class Fighter{
    private String name;//名稱
    private int level;//等級
    private int hp;//血量
    private int mp;//魔法值
    //戰鬥方法
    public void fight(){
        System.out.println("魔法師揮動了他的魔杖!");
    }

    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(角色),放所有相同的部分都放到裏面,然後再用子類Fighter和Magic繼承Role,子類裏不用寫一句代碼,就繼承了父類裏的成員變量和方法了。象這樣:

/**
*父類:角色
*/
public class Role{
    private String name;//名稱
    private int level;//等級
    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僅僅把子類中共同的部分移了進來。
然後是子類Fighter,它要繼承Role:

public class Fighter extends Role{
    public void fight(){
        System.out.println("戰士撥出了寶劍!");
    }
}

這裏有一個關鍵字:extends,這表示Fighter會擴展Role的代碼,意思就是首先Fighter獲得了Role的非私有代碼,同時Fighter還可以添加新的代碼(比如fight())。
魔法師類也是一樣的:

public class Magic extends Role{
    public void fight(){
        System.out.println("魔法師揮動着他的魔杖!");
    }
}

Magic同樣繼承了Role的代碼,同時添加了新的fight()方法。

說明:在圖6.1中,爲UML的類圖,每個框都有三格,最上格爲類名;中間格爲屬性名,前面的減號代表private。冒號(:)後面是數據類型的名稱;最下格爲方法名稱,加號代表public。空心的三角代表繼承關係,三角指向的是父類。


圖6.1 類圖
我們寫段代碼測試一下:

/**
 *      角色遊戲測試
 * @author mouyong
 */
public class RoleTest {
    public static void main(String[] args){
        /*****************戰士測試**********************/
        Fighter f1=new Fighter();
        f1.setName("戰士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        System.out.println("戰士測試輸出:");
        System.out.println("姓名:"+f1.getName());
        System.out.println("等級:"+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("等級:"+m1.getLevel());
        System.out.println("血量:"+m1.getHp());
        System.out.println("魔法值:"+m1.getMp());
    }
}

我們可以看到,在Fighter和Magic類並沒有定義姓名,等級,血量和魔法值這些屬性,也沒有定義它們的讀取和設置方法,可是我們仍然可以使用f1和m1兩個對象使用這些屬性和方法,並得出正確的結果。這就是繼承的力量,不需要重複寫同樣的代碼,就可以使用它們。
輸出結果:

圖6.2 角色遊戲測試輸出界面

6.1.2 多態與”是一個”

繼承可以讓我們避免類間重複的代碼定義,同時,它還帶來很更多的”智能”。
在3.1.4我們講過類型轉換。我們說,當類型之間兼容的時候,我們可以進行兩種類型轉換,一種是自動類型轉換,由編譯自動幫我們完成,一種是強制類型轉換,由我們強制聲明完成。當一個類繼承了另一個類的時候,我們說父子類之間就是兼容的,這個時候我們就可以進行類型轉換。
下面的代碼我相信大家能看懂,並且知道是可以編譯通過的:

Fighter f1=new Fighter();
Magic m1=new Magic();

上面的代碼並沒有進行類型轉換。那麼我們接着看下面的代碼:

Role role1=new Fighter();
Role role2=new Magic();

如果你把上面的代碼進行編譯,你會發現,它們能通過編譯!這是因爲當一個類的實例對象賦值給自己的父類變量時,編譯器會自動進行類型轉換,將子類當做父類看待,也就是編譯器認爲”戰士是一個角色”,”魔法師也是一個角色”。我們知道這兩句話是正確的,這就是我說的,父類和子類如果用我們人類的話來表達就是”子類(戰士)是一個父類(角色)”的關係。當有”是一個”的關係時,編譯器就會進行自動類型轉換(默然說話:換個說法:子轉父,自動轉!)。
看完自動轉換,我們再來看第三種情況:

Fight f1=new Role();
Magic m1=new Role();

在這個情況下,我們看到了,我們把一個父類的對象賦值給了一個子類的變量,這會發生什麼呢?編譯報錯!因爲編譯器認爲”角色是一個戰士”和”角色是一個魔法師”並不是正確的,所以它報錯了。(默然說話:你可以這樣理解,每個人總想裝爹,但是爹卻是不願意裝兒子的。也許這樣可以幫助你記住這個規則)。
再來看一個在現實代碼中會存在的情況:

Role r1=new Fighter();
Fighter f1=r1;

第一句代碼我們已經解釋過了,它是可以成功通過編譯的(子轉父,自動轉),但是第二句代碼呢?它是不能通過編譯的。這時你肯定會覺得奇怪,這個角色對象就是一個戰士呀,爲何不行呢?因爲編譯器是不會結合上一句代碼來看第二句代碼的,所以在編譯器看來,第二句是有可能出錯的(父轉子,不願意),所以它不會自動轉換,那麼如果我們一定要完成這個轉換呢?我們可以改成這樣:

Role r1=new Fighter();
Fighter f1=(Fighter)r1;

大家已經看到了,第二句的語法就是我們在第三章提到的”強制類型轉換”的語法,相當於我們告訴編譯器:”夥計,你放心,出了問題我負責!”。於是編譯器就會去嘗試完成類型轉換。(默然說話:你可以記下這句口訣:子轉父,不安全,需強制
強制轉換在任意的父類轉子類時均可以進行,編譯都能通過,但是並不保證能成功執行,比如下面的代碼:

Role r1=new Magic();
Fighter f1=(Fighter)r1;

這兩句代碼均是可以通過編譯的,雖然我們能發現第二句代碼是有問題的,因爲r1其實不是戰士,是一個魔法師。但是因爲我們聲明瞭強制類型轉換,於是編譯器本着”你說的,你負責!”的態度閉上了錯誤提醒的嘴,於是我們需要承擔的後果就是,執行報錯。

圖6.3 類型轉換失敗:不能將Magic轉爲Fighter
在執行的時候我們會看到紅色的報錯信息(默然說話:額,那個綠色的字是我PS上去的,不要誤以爲你的報錯信息裏會有那行綠色的字哦{尷尬臉})。
總結一下:父轉子,自動轉,子轉父,不安全,需強制。如果強制轉換時類型真的不對,會出現ClassCastException(類轉換異常)的運行時異常拋出。
前面花了很多的篇幅來講”是一個”的原理,講自動轉換與強制轉換的原則並非只是在玩語法的遊戲,而是爲”多態”的實現鋪平理論的道路。只有在瞭解了自動轉換與強制轉換的原則之後,我們纔有可能寫出更靈活的代碼。
例如,有這樣一道題目是做爲遊戲一定要做的,就是顯示角色的血量,魔法值。我們很可能會這樣來完成:

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());
}

不錯不錯,現學現用呀。前面纔講過方法重載,我們這裏就用上了,真的很棒哦!不過,別高興得太早,我們來設想一個很實際的問題:我們這裏只有兩個角色,而一個實際的遊戲中很可能有幾百個角色,按我們現在的思路,我們就得重載幾百個方法來顯示不同角色的血量?寫幾百個方法倒也罷了,因爲畢竟最終我們的程序裏肯定會有上千個方法,問題是這幾百個方法內的代碼非常相似,幾乎是重複的,這完全違揹我們”任何代碼只寫一遍”的原則呀。
我們來想想”戰士是一個角色”和”魔法師是一個角色”這兩句話,還有”父轉子,自動轉”。可以只寫下面的一個方法:

public void showBlood(Role r){
    System.out.println("姓名:"+r.getName+"血量:"+r.getHp()+"魔法值:"+r.getMp());
}

因爲Role是所有角色的父類,所以,我們可以把任何子類作爲參數傳遞給這個方法,而這個方法就可以輸出任何角色的姓名,血量和魔法值,這包括目前還不存在的幾百個角色,唯一的要求,就是它們需要繼承自Role。這就是”多態”的寫法。下面是具體實現的完整代碼:

/**
 * 測試多態方法showBlood,通過設置傳入的參數爲父類,可以方便的適應多變的角色。
 * @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("戰士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        Magic m1=new Magic();
        m1.setName("大魔法師默然");
        m1.setLevel(120);
        m1.setHp(100);
        m1.setMp(100000);
        //顯示戰士小中的血量
        test.showBlood(f1);
        //顯示魔法師的血量
        test.showBlood(m1);


    }
}

下面是運行結果:

圖6.4 多態方法運行結果
多態的意思,就是”一個方法,多種實現”。按字面意思,前面學過的方法重載也是多態實現的一種方式,這裏講到的利用父類參數的例子,也是多態實現的典型例子。後面我們還會接着講方法重寫,它是多態實現的第三種方式。

6.1.3 方法重寫

我們接下來完成遊戲中的另一個功能,完成遊戲中任意角色的攻擊調用。根據剛剛纔學習過的思路,我想我們可以寫這樣一個方法:

public void attack(Role r){
    r.fight();
}

然後我們得到了一個編譯器的報錯信息:Role中找不到fight()方法!是的,fight()方法被定義在戰士和魔法師兩個子類中,Role中並沒有這個方法,所以我們不可能使用Role來調用的。但我們可以觀察到另一個特點,無論是戰士,還是魔法師,fight()方法的聲明都是這樣的:

public void fight()

也就是說,方法聲明是一樣的,只是方法的操作代碼不一樣。所以,其實我們這可以把這個方法提升到父類方法中,象這樣。

public class Role {
    //省略前面成員變量的聲明
    //聲明戰鬥方法,讓子類重寫,方便多態使用
    public void fight(){
        //此處無代碼
    }
    //省略setter和getter方法的定義
}

由於所有的攻擊都是子類纔會知道的,所以我們讓父類的這個方法爲空方法,然後在子類中重新定義它的執行代碼,
在繼承父類之後,在子類中將父類的方法重新進行定義,我們稱爲方法重寫(Override)。
由於Role定義了fight()方法(雖然方法體一行代碼也沒有),編譯器就不會找不到fight()方法了,此時就可以繼承利用我們前面所學的多態了。

/**
 * 測試多態方法showFight,通過設置傳入的參數爲父類,可以方便的適應多變的角色。
 * @author mouyong
 */
public class Game {
    //省略前面顯示血量的方法定義….

    //顯示戰鬥的方法定義,使用父類參數實現多態
    public void showFight(Role r){
        r.fight();
    }

    public static void main(String[] args){
        Game test=new Game();
        Fighter f1=new Fighter();
        f1.setName("戰士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        Magic m1=new Magic();
        m1.setName("大魔法師默然");
        m1.setLevel(120);
        m1.setHp(100);
        m1.setMp(100000);


        //顯示戰士小中的戰鬥
        test.showFight(f1);
        //顯示魔法師的戰鬥
        test.showFight(m1);
    }
}

程序執行結果也表明Java非常的智能,你傳給它 Fighter,它就調用Fighter的fight()方法,你傳給它Magic,它就調用Magic的方法,結果如下:

圖6.5 方法重寫測試,智能完成子類重寫方法的調用
子類重寫父類一個方法時,必須要注意到方法的方法名,參數和返回值必須一模一樣。這是一個鎖碎的工作,特別是針對我們這邊非英語國家的學生來說,真是一個地獄般的考驗與修煉(默然說話:耶!我來自地獄,我居然活着出來了!),這種鎖碎的工作,我們程序員一定要養成習慣,重複鎖碎的活計,交給機器去辦。自從JDK5加入了註解(Annotation)之後,這個檢查是不是做了正確的方法重寫的任務,總算可以給機器去完成了。

/**
 * 戰士,加入了方法重寫的註解@Override
 * @author mouyong
 */
public class Fighter extends Role {
    //戰士戰鬥的方法
    //@Override註解表示讓編譯器檢查此方法是否爲方法重寫
    @Override
    public void fight(){
        System.out.println("戰士撥出了寶劍!");
    }
}

@Override這個註解表示讓編譯器檢查這個方法是不是一個父類方法的重寫,如果不是,則給出報錯信息提示。(默然說話:這報錯信息明顯不是一箇中國人翻譯的,完全不明顯它要表達什麼,我懷疑這也是由機器來翻譯的!正確的翻譯應該是”此方法沒有重寫或者父類的方法”

圖6.6 錯誤重寫引發的報錯信息(天坑,這是哪國人的翻譯?!)
如果要重寫父類的某個方法,加上@Override註解,寫錯方法名,機器就會告訴你了。關於註解,我們在第18章詳細說明。

6.1.4 抽象方法、抽象類

一方面,Role類中的fight方法就象這樣空着不寫,不免讓人覺得奇怪。(默然說話:在實際當中,其實有很多的時候都會有空着不寫的方法存在的,這是一個避免不了的事實。)另一方面,由於沒有提示,我們真的很難保證一次就寫對這個方法的定義。(默然說話:不要說我們這些非英語國家的人民,就算是英語國的人民們也深受折磨。名字稍複雜,免不了進行反覆覈對,即使你使用了@Override,它也僅只能告訴你有沒有錯,卻不能告訴你錯在哪裏)爲了解決這一問題,Java引入了抽象方法的概念。
如果某個方法的確不知道應該寫什麼,Java允許你不寫一對大括號({}),直接分號結束它就好了,唯一的代價是,你需要在返回值前面加上關鍵字abstract(抽象),以聲明它是一個抽象方法。
還要付出的一個代價是,你的類也要在關鍵字class前面加上abstract關鍵字。以聲明它是一個抽象類。
//含有抽象方法的類必須聲明爲抽象類,不能被實例化(new)

public abstract class Role {
    //省略成員變量的聲明
    //省略setter與getter方法定義

    //聲明抽象戰鬥方法,沒有方法體,直接分號結束。
//抽象方法必須讓子類重寫,否則報錯
    public abstract void fight();
}

類中如果有方法被聲明爲抽象方法,則說明這個方法沒有可執行的代碼,是不完整的,帶有不完整方法的類也不應該進行實例化(new),這也就是當一個類聲明瞭抽象方法後,這個類本身也必須聲明爲抽象的原因。如果你硬要實例化(new)一個對象,那等待你的自然就是編譯器的報錯信息。

圖6.7 實例化一個抽象類的結果:報錯信息
如果一個子類繼承了一個抽象類,那這個子類就必須要實現這個抽象類聲明的所有抽象方法(默然說話:是的,必須實現所有的抽象方法,一個都不能少!),這個時候你有兩個選擇,一個是繼續聲明方法爲抽象方法(默然說話:額,我不覺得這個可以選,因爲同時你就要把你的類也弄成抽象的,而抽象類又不能new,你寫一個抽象類繼承另一個抽象類搞毛線?),另一個是就是重寫這個抽象方法。如果你沒有,比如你只重寫了部分抽象方法,並沒有全部都實現,那你也會收到一個編譯器的報錯信息。

圖6.8 未重寫(圖中叫”未覆蓋”)抽象方法的報錯信息
默然說話:耶!我看到了方法的名字,現在我知道哪個方法沒有重寫了!另外,我還發現,現在的IDE工具都可以幫助我進行重寫,這樣我就不用再浪費時間去核對這該死的方法名了!

圖6.9 現在的IDE都提供了幫助我們改正錯誤的辦法,只要輕輕一點!

6.2 繼承語法細節

前面簡單介紹了繼承的語法,下面來具體對一些細節做一些說明。

6.2.1 protected成員

前面我們寫了顯示血量的方法,這個方法其實蠻麻煩的,因爲我個人覺得,血量等等信息應該是由對象自身來告訴我們,而不是應該在另外的方法中去依次獲得的。所以,我們可以爲戰士和魔法師兩個類分別添加toString()方法,如下。

public class Magic extends Role {

    //省略其他代碼
    //toString方法專爲輸出信息而設置
     public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.getName(),this.getHp(),this.getMp());
    }
}
public class Fighter extends Role {
    //省略戰士戰鬥的方法

    //toString方法專爲輸出信息而設置
    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()這樣來獲得成員變量的值真的好麻煩呀,能不能直接使用成員變量的名字呢?目前不行,因爲這些成員變量都被設爲private,如果改爲public又不是我們想要的,我們只是想在子類裏可以直接訪問這些成員變量,並不想讓所有的類都可以輕易訪問它們。Java爲我們提供了第三個關鍵字:protected,它可以限制其他的類不能訪問,但是子類可以直接訪問父類的protected成員。(默然說話:對的,和private與public一樣,protected不僅可以修飾成員變量,同樣也可以修飾成員方法。)象這樣。

package cn.speakermore.ch06;

/**
 * 用於講解類的繼承
 * 父類:角色
 * @author mouyong
 */
public abstract class Role {
    protected String name;//名稱
    protected int level;//等級
    protected int hp;//血量
    protected int mp;//魔法值
   //略。。。。
}

加了protected的類成員,同一個包中的類可以訪問,不同包下的子類也可以訪問。現在我們可以這樣來寫Fighter類了。

package cn.speakermore.ch06;

/**
 * 戰士
 * @author mouyong
 */
public class Fighter extends Role {
    //……
    public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.name,this.hp,this.mp);
    }
}

當然,Magic也可以同樣進行修改了,這裏就不列出代碼了。

提示:基於程序可讀性,以及充分利用IDE的提示功能,強烈建議使用this.成員的形式書寫代碼

Java的三個訪問修飾符均登場了,它們是publicprotectedprivate。如果你一個都沒有寫,那類的成員就擁有包訪問權限,這個權限我們稱爲默認權限。同一個包內的類均可以訪問默認權限的類成員。表6.1列出了他們的權限範圍:
表6.1 訪問修飾符與訪問權限

關鍵字 類內部 相同包 不同包
public 可訪問 可訪問 可訪問
protected 可訪問 可訪問 子類可訪問
不寫關鍵字(默認) 可訪問 可訪問 不可訪問
private 可訪問 不可訪問 不可訪問

提示:此張表看上去很複雜,也不是很好背,可以比較簡單地記住它們的使用規則,大部分情況下會使用public,它可以無限制訪問,不願意給訪問的就寫private,通常成員變量都是private的,只想給子類訪問的就寫protected。

6.2.2 方法重寫的細節

在前面,我們在Fighter和Magic重寫了toString()方法(默然說話:等會兒!toString()方法在Role裏可沒有!這種說法不對!),我們注意到,它們的代碼又是一樣的,那只要是一樣的,是不是可以直接寫在父類裏呢?我們來試試。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註解!居然沒有錯!
然後刪掉Fighter與Magic裏的toString方法定義,運行測試類,看看是什麼結果?

圖6.10 運行結果與前面一樣,沒有變化
默然說話:Java真的好智能,這都能對!)我們發現運行的結果和前面是一樣的!又一次把重複的代碼變得只寫一遍,感覺真的很好。
不過,我總覺得應該再做點什麼。在這個角色信息輸出中,似乎應該要顯示出角色的類型,不然人家取角色名的時候沒有加角色的類型,我們就不知道他是一個什麼樣的角色了。對!就這樣辦。
看來我們還是得重新爲Fighter重寫toString()方法。不過,這次重寫與前面不一樣,因爲Role中的toString()已經寫好了角色基本信息了,所以我們只要在子類的toString()裏獲得父類的toString()方法返回字符串,再連接上角色類型信息就可以了。問題來了,如何在子類裏指定調用父類的方法呢?我們可以使用super關鍵字,象這樣:

package cn.speakermore.ch06;

public class Fighter extends Role {
    //省略前面的代碼
    @Override
    public String toString(){
        //super表示父類對象
        return "戰士:["+super.toString()+"]";
    }
}
戰士寫完,魔法師也一樣:
package cn.speakermore.ch06;

public class Magic extends Role {


    @Override
    public String toString(){
        return "魔法師:["+super.toString()+"]";
    }
}

來看看輸出結果:

圖6.11 修改toString()後的的執行結果
耶!成功的在父類的字符串前加上了角色的名稱!
super的意思就是”我爹”。指當前對象的父類對象(默然說話:對的,是一個對象,不是父類。所以super關鍵字擁有所有對象的特點,比如,只能調用非private修飾的成員變量或方法。
方法重寫要注意一個問題,就是方法重寫的訪問修飾符只能擴大,不能縮小。所以,如果聲明爲public,就只能寫爲public了。

圖6.12 重寫不能縮小訪問修飾權限
關與重寫,有個小細節必須提及。就是關於前面提到的,關於”方法重寫要求方法的返回值,方法名稱,參數列表完全一致”,在JDK5之後,你可以聲明返回值爲原來返回值的子類。例如,我們有兩個類,Animal是父類,Cat是子類。我們在使用它們做返回值時,有一個方法定義如下:

public Animal getSome(){}。

在JDK5之前,如果我重寫這個方法如下:

public Cat getSome(){}

是會報錯的,但是JDK5之後卻不報錯了。

提示:static方法不存在重寫,因爲static方法均爲類方法,是公有成員,所以如果子類中定義了相同返回值、方法名、參數列表的方法時,也僅只屬於子類,並非方法重寫。

6.2.3 再看構造方法

如果類有繼承關係,則在實例化子類對象的時候,會先實例化父類對象。也就是說,會先執行父類的初始化過程,然後再執行子類的初始化過程。
由於構造方法是可以重載的,所以子類也可以指定調用父類的某個重載的構造方法,如果子類沒有指定,則默認調用無參構造方法(默然說話:這個時候,如果你的父類沒有無參構造方法,那就麻煩了,子類無法實例化了。所以如果你進行了構造方法的重載,請務必寫上無參的構造方法,即使打一對空的大括號也行,這可以防止很多Java的高級特性(如反射機制)無法進行的問題)。
如果想要在子類中指定調用父類的構造方法,可以使用super()的語法。要注意的是,super()只能寫在子類構造方法中,而且必須是構造方法中的第一行。你可以在super()中添加入參數,這樣Java就會智能的識別對應的父類重載的構造方法進行調用了。來看例子:
首先,我們編寫了一個父類Father,它有兩個構造方法,默認的,和帶一個整型參數的:

package cn.speakermore.ch06;

/**
 * 構造方法調用順序的教學類,
 * 父類,擁有兩個構造方法
 * @author mouyong
 */
public class Father {
    public Father(){
        System.out.println("這是Father無參構造方法");
    }
    public Father(int a){
        System.out.println("這是Father有參構造方法,它傳入了"+a);
    }

}

然後我們再編寫兩個子類,Son和Son2。其中Son用來測試默認情況下的調用順序:

package cn.speakermore.ch06;

/**
 * 用於測試默認構造方法調用的測試類
 * 這是一個子類
 * @author mouyong
 */
public class Son extends Father {
    public Son(){
        //這裏沒有使用super(),但是編譯器會默認添加調用父類的無參構造方法
        //super();
        System.out.println("這是Son的無參構造函數");
    }
}

而Son2,是用來測試指定調用父類一個參的構造方法的(使用super(3)這條語句來指定):

package cn.speakermore.ch06;

/**
 * 用於測試使用super()調用指定的父類構造方法的子類,
 * 另一個子類
 * @author mouyong
 */
public class Son2 extends Father {
    public Son2(){
        //通過傳遞一個整型數,指定調用父類中帶一個整形參數的構造方法
        super(3);
        System.out.println("這是Son2的無參構造方法");
    }
}

最後,使用一個測試類,對它進行測試:

package cn.speakermore.ch06;

/**
 * 父子類構造方法調用的測試
 * @author mouyong
 */
public class FatherAndSonTest {
    public static void main(String[] args){
        //測試默認情況下,構造方法的調用順序
        new Son();
        System.out.println("============漂亮的分割線================");
        //測試在子類中指定調用父類某個構造方法的調用順序
        new Son2();
    }
}

執行的結果如下圖:

圖6.13 繼承下的初始化代碼執行順序及指定父類的構造方法
我們可以看到,第一個new Son()調用了Father的無參構造方法,而第二個new Son2(),由於使用了super(3),指定調用了Father的有參構造方法,並收到了參數3。

注意:由於this()和super()都要求寫在構造方法的第一行,所以一個構造方法中,寫了this()就不可能再寫super(),同樣,寫了super()就不可能再寫this()。

6.2.4 再看final關鍵字

第三章告訴我們,可以在方法變量前添加final,讓變量的值不能再被修改,第五章又告訴我們,還可以在類的成員變量前添加final,讓成員變量也不能再次被修改。這裏,我們要知道,在class的前面,也可以添加final,讓這個類成爲太監。(默然說話:理論上,太監都不會再有後代了。
Java裏最有名的”太監”類,就是我們經常使用的String。

圖6.14 Java APIs中String的文檔描述
如果打算繼承final類,則會發生編譯錯誤,如圖:

圖6.15 無法從最終(final)String進行繼承
除了可以用於類的前面,final還可以用於方法的前面,用來表示方法不能被子類重寫。Java中最著名的Object類裏就有這樣的方法。

圖6.16 無法重寫的wait()方法

提示:Java SE API中會聲明爲final的類或方法,通常都與JVM對象或操作系統資源管理有密切關係。所以都不希望用戶重寫這些方法,以免出現不可預料的情況,甚至破壞JVM的安全性。比如這裏例舉的wait()方法,還有notify()方法等等。


圖6.17 錯誤:不能重寫(圖中叫”覆蓋”)final方法

6.2.5 java.lang.Object

在Java中,子類只能繼承一個父類,如果定義類時沒有用到extends關鍵字來指定任何父類,則會自動繼承java.lang.Object(默然說話:現在知道我前面爲什麼說Object是”著名的”了吧?Object是一切Java類的父類,有時候也被稱爲Java類的根類,因爲所有Java類的最頂層父類一定是Object。不過它也是最可憐的,Object沒有父類。)。
再根據我們前面說過的類對象的類型轉換規律,所以我們可以得出:任何一個類都可以賦值給Object類型的變量(子轉父,自動轉):

Object o1="默然說話";
Object o2=new Date();

這樣做的好處是明顯的,壞處也是明顯的。好處就是,當我們在編碼的時候,如果我們要處理的數據,它的類型要求是多種類型,這時我們就可以聲明一個Object[]類型來收集它們,並做統一處理。Java的集合就是利用了這一點,很輕鬆解決了不同數據類型的數據放在一起的難題。它的源代碼看起來大概是這樣的:

package cn.speakermore.ch06;

import java.util.Arrays;

/**
 * 一個模仿Java的ArrayList功能的類
 * @author mouyong
 */
public class ArrayList {
    //因爲允許集合可以任意混裝各種類型的對象,所以使用Object數組
    private Object[] list;
    //目前list數組的下標,這個下標還沒有裝東西,可以賦值。相當於集合的長度
    private int next;

    /**
     * 指定集合的初始長度的構造方法
     * @param capacity 一個數字,指定集合的初始長度
     */
    public ArrayList(int capacity){
        list=new Object[capacity];
    }

    /**
     * 默認構造方法,指定了數組初始長度爲16
     */
    public ArrayList(){
        this(16);
    }

    /**
     * 添加對象到集合裏
     * @param o 被添加到集合裏的對象,可以是任意對象,所以定義爲Object類型
     */
    public void add(Object o){
        if(next==list.length){
            //如果next剛好是集合的長度,說明集合已經滿了,自動擴容到原來的2倍
            list=Arrays.copyOf(list, next*2);
        }
        //把對象添加到數組中
        list[next]=o;
        //下標移動到下一個位置,準備接收下一個元素
        next++;
    }

    /**
     * 獲得指定位置的對象
     * @param index 整數,指定的集合下標,從0開始,不應該超過集合的最大長度。
     * @return 對象,因爲不知道集合中所裝對象的具體類型,所以也被定義爲Object
     */
    public Object get(int index){
        return list[index];
    }

    /**
     * 獲得集合的長度
     * @return 整數,集合的長度
     */
    public int size(){
        return next;
    }
}

自定義的ArrayList類,它使用了一個Object[]數組裝對象。如果在創建對象時沒有指定長度,則默認使用16。
可以通過add()方法來裝入任意對象。如果原長度不夠,則自動擴容到原來的2倍。如果要取出對象,則使用get()方法,傳入下標來獲取。如果想要知道有多少個對象裝在裏面,則可調用 size()方法。下面是一個使用的例子。

package cn.speakermore.ch06;

import java.util.Scanner;

/**
 * 測試自定義ArrayList的測試類
 * @author mouyong
 */
public class ArrayListTest {
    public static void main(String[] args){
        //實例化自定義集合對象
        ArrayList infos=new ArrayList();
        //準備鍵盤輸入
        Scanner input=new Scanner(System.in);
        //設置循環終止變量
        String isQuit="";
        do{
           System.out.println("請輸入姓名:");
           String name=input.nextLine();
           infos.add(name);//將字符串放入集合中
           System.out.println("請輸入年齡:");
           int age=input.nextInt();
           infos.add(age);//將整數放入集合中
           System.out.println("是否繼續?(y/n)");
           isQuit=input.next();
           //爲解決字符輸入的bug而多寫的接受語句(想知道bug是什麼樣,可以刪除此句)
           input.nextLine();
       }while("y".equalsIgnoreCase(isQuit));
        //循環輸出集合中所有的數據
       for(int i=0;i<infos.size();){
           System.out.println("姓名:"+infos.get(i++));
           System.out.println("年齡:"+infos.get(i++));
       }
    }
}

下面是具體執行的結果:

run:
請輸入姓名:
默然說話
請輸入年齡:
44
是否繼續?(y/n)
y
請輸入姓名:
狂獅中中
請輸入年齡:
10
是否繼續?(y/n)
n
姓名:默然說話
年齡:44
姓名:狂獅中中
年齡:10
成功構建 (總時間: 31 秒)

java.lang.Object是所有類的頂層父類,所以任意子類均可重寫其定義的非final方法,在現實中,我們也是這樣做的。有一些方法是經常會被重寫的。
1. 重寫toString()
在前面的例子中,我們已經重寫過toString()方法了,它是Object經常被重寫的一個方法,主要的作用就是用來方便我們顯示一些字符串內容(默然說話:前面的遊戲已經大量應用嘍。
在Object中toString()的方法聲明是這樣的:

public String toString(){
    return getClass().getName()+"@"+Integer.toHexString(hashCode());
}

現在還不好解釋以上代碼的具體含義,它輸出了一個類名,後跟”@”,接着是十六進制的數字(默然說話:我常告訴學生,這一串十六進制數字與內存有關,並不是內存地址,但的確是根據內存地址換算出來的。)。如果你沒有重寫過toString(),那麼用下面這句代碼,就會得到這樣的一個輸出。

ArrayList infos=new ArrayList();
System.out.println(infos);


圖6.18 System.out.println(infos)的輸出結果

注意:如果你嘗試在你的電腦上運行,那麼後面的十六進制數字會和我的不一樣。

2.重寫equals()
在第四章談過,如果想要比較兩個對象內容相等,不能使用==,而是要通過equals()方法。而equals()也是屬於Object類的一個方法,其源代碼是這樣的:

public boolean equals(Object obj){
    return this==obj;
}

如果你能看懂,其實應該看出來了,Object的equals()方法定義也是用的==,所以,如果你不重寫equals()方法,你想要的比較兩個對象內容相等的奇蹟也是不會出現的。如何定義eqauls()方法呢?這還真沒有統一的寫法,不過有一個模式可以借鑑,如下面的代碼:

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;
        }
        //排除前面兩種情況,進入自定義部分
        Student stu=(Student)obj;
       //下面這句代碼的意思,我們定義了一個規則:只要id相同,我們就認爲是同一個學生
        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;
    }
}

上面代碼的註釋就在說明這個模式,第一步首先驗證對象的內存地址相不相同,之後再驗證對象是不是同一種類型,最後是自定義規則,這部分就是要由你來決定如何寫的。也就是說,在具體的類中,你們是如何規定”對象的內容相同”。在這個例子裏,我們規定”如果對象的id是相同的,我們就認爲兩個對象是相同的”。
此外,爲了完成類型比較,我們使用了instanceof關鍵字,它是一個比較運算符,在左邊要寫一個對象變量名,在右邊要寫類的名稱,instanceof完成比較左邊的對象與右邊類是否兼容。如果不兼容,直接報語法錯。
另外要注意的是,instanceof關鍵字爲true的情況並非類名稱與對象名完全一致,類爲父類也是會返回true的。
最後,通常我們重寫了eqauls()方法之後,同時也會重寫hashCode()。等到第9章時我們再來討論。

6.2.6 關於垃圾收集

創建對象就會佔據內存,這是一個常識。如果程序執行流程中出現了無法使用的對象,這個對象就只是”佔着茅坑不拉屎”的垃圾,它佔用了內存,卻無法使用,浪費了這些內存。
放在以前,程序員是要自己來做這件很”髒”卻經常很難搞定的事情(默然說話:哦,”偷雞不成蝕把米”就是指這類”髒”活了吧。垃圾沒清乾淨,倒留下一堆bug可真是老前輩們的”家常便飯”。其實,我們經常聽老前輩們傳說C語言如何如何難學,特別是指針。其實指針一點都不難學,難的是如何確定一塊內存已經是垃圾了,何時釋放內存纔是正確的。這個過程中經常寫出bug,把程序搞崩潰。這纔是C語言真正的地獄模式。),於是Java提供了垃圾回收機制(Garbage Collection, 簡稱GC),專門用來處理這些垃圾。只要是程序裏沒有任何一個變量引用到的對象,就會被GC認定爲垃圾對象。在CPU有空的時候,或者是內存已經佔滿的時候,GC就會自動開始工作(這就是多線程運作的方式,我們在第11章說明)。
實際要說明垃圾回收的原理是很困難的,因爲它的算法就很複雜,不同的需求還會導致有不同的算法。所以作爲我們來說,只要知道“JVM會幫助我們進行內存管理,它的名稱叫垃圾回收,簡稱GC,耶,太棒了!”,就足夠了。細節讓JVM工程師幫我們搞定吧。
那到底哪些是垃圾呢?下面的例子將說明這個問題,先來看代碼:

Object o1=new Object();
Object o2=new Object();
o1=o2;

我們需要弄清楚的是,在第一行的代碼中進了三步操作,第一步聲明瞭o1變量內存,第二步創建了一塊內存放Object對象,第三步是賦值操作,把Object對象的內存地址放到了o1變量中。第二行代碼也是一樣:o2變量得到了第二次new出來的Object對象的地址。這時,兩個new出的對象都分別由o1和o2引用,所以它們目前都不是垃圾。

圖6.19 兩個對象不是垃圾
接下來是第三行代碼,把o2的值(第二個Object對象的地址)賦值給了o1。此時o1原來的值就會被覆蓋,而o1和o2兩個變量都在引用第二個對象了。第一個Object對象就沒有任何變量在引用它,它就成爲了垃圾,GC就會自動找到這樣的垃圾並予以回收。

圖6.20 第一個對象成爲垃圾

6.2.7 再看抽象類

寫程序常有些看似不合理但又非得完成的需求。舉個例子,現在老闆叫你開發一個猜數字的遊戲,隨機產生一個1000-9999的四位數,用戶輸入的數字與隨機產生的數字相比,如果相同就顯示”猜對了”,如果不同主繼續讓用戶輸入數字,一共猜12次。
這個程序有什麼難的?相信現在的你可以寫出來:

package cn.speakermore.ch06;

import java.util.Random;
import java.util.Scanner;

/**
 * 猜數遊戲:計算機產生一個四位數(1000-9999),由用戶來猜。<br />
 * <br />
 * 如果沒猜中,給出"大了"或"小了"的提示,同時還給出"猜中了x個數"的提示<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;

        //用來存放電腦想出來的四位數中的每一個位置上的數字
        int[] numberComputer=new int[4];
        int clientInput=0;


        Integer tempComputer=guess,tempClient=clientInput;
        int i=0;
        while(tempComputer!=0){
            //將電腦想出來的四位數分爲四個數字放到數組裏
            numberComputer[i]=tempComputer%10;
            tempComputer=tempComputer/10;
            i++;
        }
        System.out.println("我現在想好了一個1000-9999之間的數,你可以猜12次");

        for(i=0;i<12;i++){
            System.out.println("第"+(i+1)+"次請輸入一個數:");
            clientInput=input.nextInt();
            //如果猜對了,則結束遊戲
            if(clientInput==guess){
                System.out.println("恭喜!你猜對了!");
                System.exit(0);
            }
            //告訴用戶猜的數是大是小
            if(clientInput>guess){
                System.out.println("大了");
            }else{
                System.out.println("小了");
            }
            //告訴用戶猜中了幾個數
            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("你猜的數有"+count+"個");
            }else{
                System.out.println("你一個都沒有猜中!");
            }

        }

        System.out.println("很遺憾,沒有猜中!");


    }
}

我們可以做了一個挺複雜,富有挑戰且真的很有趣的猜數遊戲哦!(默然說話:哦,這個例子來自於一次與兒子去於密室逃脫時的一個迷題。)你興沖沖的把程序交給老闆,準備迎來一如既往的表揚時,老闆卻皺着眉頭說:”這個,我們似乎不應該在文本的狀態下執行這個遊戲呀。”,你一楞,隨即機智的問道:”那會怎麼來執行這個程序呢?”,老闆一臉看到未來的迷茫樣子:”這是個好問題,不過我們還沒有完全決定,可能用窗口程序,其實網頁或者app也不錯,難說我們需要造一臺專用的遊戲機,通過九宮按鈕直接輸入數字?下週開會討論一下吧。”,於是你舒了口氣,說:”好吧,那我下週討論完了再寫吧。”,老闆用不容置疑的口氣說:”不行!”。你只好無奈的點點頭,退出老闆的門時,你有沒有感覺到一萬隻草泥馬歡快地在你的心臟裏跳舞呢?
這可不是一個段子(默然說話:嗯,當然,我似乎把它寫成了段子)。在團隊合作、多部門開發程序時,有許多時候,有一定順序完成的工作必須要同時開工,因爲老闆是不可能閒養你3個月等上一個工序完成之後,再你完成你的工作。(默然說話:對的,如果要等3個月,那直接不請你,讓人家直接全做完就好了。)雖然需求沒有決定,但你卻要把你的程序完成的例子太多了。
有些不合理的需求,本身確實不合理,但有些看似不合理的需求,其實可以通過設計來解決。比如上面的例子,雖然用戶輸入,顯示結果的環境未定,但你負責的部分(猜數遊戲的邏輯)還是可以先操作的。我們可以這樣完成:

public abstract class GuessNumber {
    public void go(){
        Random random=new Random();
        Integer guess=random.nextInt(9000)+1000;

        //用來存放電腦想出來的四位數中的每一個位置上的數字
        int[] numberComputer=new int[4];
        int clientInput=0;


        Integer tempComputer=guess,tempClient=clientInput;
        int i=0;
        while(tempComputer!=0){
            //將電腦想出來的四位數分爲四個數字放到數組裏
            numberComputer[i]=tempComputer%10;
            tempComputer=tempComputer/10;
            i++;
        }
        //所有輸出消息均替換爲抽象方法,以便在將來不用修改這部分代碼
        print("我現在想好了一個1000-9999之間的數,你可以猜12次");

        for(i=0;i<12;i++){
            print("第"+(i+1)+"次請輸入一個數:");
            //用戶輸入替換爲抽象方法,以便在將來保證不用修改這部分代碼
            clientInput=clientInput();
            //如果猜對了,則結束遊戲
            if(clientInput==guess){
                print("恭喜!你猜對了!");
                System.exit(0);
            }
            //告訴用戶猜的數是大是小
            if(clientInput>guess){
                print("大了");
            }else{
                print("小了");
            }
            //告訴用戶猜中了幾個數
            int count=0;
            for(int j=0;j<numberComputer.length;j++){
                if(numberComputer[j]==clientInput%10){
                    count++;
                }
                clientInput=clientInput/10;
            }
            if(count!=0){
                print("你猜的數有"+count+"個");
            }else{
                print("你一個都沒有猜中!");
            }

        }

        print("很遺憾,沒有猜中!");
    }

    public abstract void print(String msg);
    public abstract Integer clientInput();
}

你可以看出,我們把不確定的部分(用戶的輸入與消息的輸出)替換爲抽象方法,這樣既解決了老闆沒決定,不知道如何輸入和輸出的問題,又解決了我們寫的代碼將來也許會面臨的大量修改的問題。
等到下週開會決定了,你只需要再寫個子類,繼承GuessNumber,重寫兩個抽象方法即可。實際上你應該已經發現了,由於這兩個抽象方法,咱們的猜數代碼可以利用繼承反覆重用了。下個月開會研究,由於猜數遊戲大受歡迎,我們需要進行”全平臺”商業化,此時你只需要再寫幾個子類,繼承GuessNumber,對兩個方法做不同的重寫就夠了,省下的時間,爲你和公司帶來了豐厚的回報。你可以買更大的房子,更漂亮的車,生更多的孩子了!這就是設計的力量!

提示:設計上的經驗,我們稱爲設計模式,上面的例子我們使用了”模板方法”模式。如果對其他設計模式感興趣,可以上網查找相關”設計模式”的資料

默然說話:在去查找之前……先擦擦你的口水,紙弄溼了不要緊,鍵盤要是溼了,說不準會電你的

6.3 重點複習

  • 面向對象中,子類繼承父類,充分進行代碼重用是對的,但是不要爲了代碼重用就濫用繼承。如何正確使用繼承,如果更好的活用多態,纔是學習繼承時的重點(默然說話:這似乎能講的東西太多了,所以這裏只說明一個你在學習過程重點需要關注的地方,後面我們還會具體的舉很多很多的例子來說明的,總之二十多年的編程經驗告訴我,優秀程序的的樣子都是長一樣的,那就大家常說的六個字:”易維護,易擴展”,如果你覺得這太高大上,不接地氣,我換六個平易近人且吸引眼球的另外六個字告訴你:”少幹活,多拿錢”!要知道,在寫這些文字之前,一般人我都不告訴他們的。
  • 如果出現代碼的反覆書寫,就應引警覺,此時可考慮的改進之一,就是把相同的程序代碼提升爲父類(默然說話:其實這是第三步,第一步應該考慮把重複代碼寫到一個獨立的方法中,第二步應該考慮使用方法重載,第三步才考慮父類。)。
  • 在Java中,繼承使用extends關鍵字,所有非私有屬性均會被繼承。但是私有屬性如果父類提供了公有方法,也可以使用。
  • Java爲了保持程序不出現”倫理道德”的爭議,只允許單繼承,即一個類有且只有一個父類(默然說話:Object例外,它沒有父類
  • 還記得父子類的類型轉換口訣麼?”子轉父,自動轉;父轉子,強制轉”。
    abstract表示抽象方法,它使用在類和方法定義的前面。抽象方法不能寫方法體(默然說話:方法體就是那對大括號,還記得麼?),子類必須重寫父類的抽象方法,否則報錯;抽象類不能實例化(默然說話:實例化就是new,new就是實例化,記得了麼?),只能由子類來實際執行它的功能代碼。另外,有抽象方法的類必須聲明爲抽象類,否則也報錯。
  • 被聲明爲portected的成員,相同包中的類可以直接存取,不同包中的類可以在繼承後的子類直接存取。
  • Java中有publicprotectedprivate三個權限關鍵字,但卻有四種權限,因爲默認權限就是不寫關鍵字的時候。
  • 如果想在子類中指定調用父類的某個方法,可以使用super關鍵字。
    重寫方法時要注意,在JDK5之後,方法重寫時可以返回被重寫方法返回類型的子類。
  • final可以用於類、方法和屬性的前面,用於類的前面表示類不能被繼承(默然說話:太監類,記得嗎?),用於方法前,表示方法不能被重寫,用於屬性前,則意味着屬性不能被第二次賦值(默然說話:就是我們常說的常量了呢)。
  • 如果定義類時沒有指定任何父類,並不意味着它沒有父類,因爲JVM會自動讓這個類繼承Object。
  • 對於在程序中沒有被變量引用的對象,JVM會進行垃圾收集(GC),這是非常重要的,因爲這能提高我們對內存的使用,不至於浪費內存。

6.4 課後練習

6.4.1 選擇題

1.如果有以下的程序片段:

class Father{
void service(){
    System.out.println("父類的服務");
}
}
class Children extends Father{
@Override
void service(){
    System.out.println("子類的服務");
}
}
public class Main{
public static void main(String[] args){
    Children child=new Children();
    child.service();
}
}

以下描述正確的是()

A. 編譯失敗
B.顯示”父類的服務”
C.顯示”子類的服務”
D.先顯示”父類的服務”,後顯示”子類的服務”

2.接上題,如果main()中改爲:

Father father=new Father();
father.service();

以下描述正確的是()
A. 編譯失敗
B.顯示”父類的服務”
C.顯示”子類的服務”
D.先顯示”父類的服務”,後顯示”子類的服務”

3.如果有以下的程序片段:

class Test{
String ToString (){
    return "某個類"
}
}
public class Main{
public static void main(String[] args){
    Test test=new Test();
System.out.println(test);
}
}

以下描述正確的是()。

A. 編譯失敗
B.顯示”某個類”
C.顯示”Test@XXXX”,XXXX爲十六進制數
D.發生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.發生ClassCastException

5.如果有以下的程序片段:

class Test{
    @Override
String ToString (){
    return "某個類"
}
}
public class Main{
public static void main(String[] args){
    Test test=new Test();
System.out.println(test);
}
}

以下描述正確的是()。

A. 編譯失敗
B.顯示”某個類”
C.顯示”Test@XXXX”,XXXX爲十六進制數
D.發生ClassCastException

6.如果有以下的程序片段:

class Father{
abstract void service();
}
class Children extends Father{
@Override
void service(){
    System.out.println("子類的服務");
}
}
public class Main{
public static void main(String[] args){
    Father father=new Children();
    child.service();
}
}

以下描述正確的是()

A. 編譯失敗
B.顯示”子類的服務”
C.執行時發生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)後,對象成員x值爲10
B.new Children(10)後,對象成員x值爲0
C.Children中無法存取x,編譯失敗
D.Children中無法調用父類構造方法,編譯失敗

8.如果有以下的程序片段:

public class StringChild extends String{
public StringChild(String str){
    super(str);
}
}

以下描述正確的是()

A. String s=new StringChild(“測試”)可通過編譯
B.StringChild s=new StringChild(“測試”)可通過編譯
C.因無法調用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設計的ArrayList類收集對象,想顯示所收集對象的字符串描述時,會顯示非常麻煩。嘗試重寫toString()方法,讓客戶端可以方便的顯示所收集對象的字符串描述。
  2. 接上題,請重寫ArrayList類的equals()方法,先比較收集的數量是否相等,然後對應位置比較各對象的內容是否相等(使用各對象的equals()),只有數量相等且對應位置的各個對象的內容相等,才判斷兩個ArrayList對象是相等的。
發佈了293 篇原創文章 · 獲贊 56 · 訪問量 72萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章