第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的三個訪問修飾符均登場了,它們是public
、protected
和private
。如果你一個都沒有寫,那類的成員就擁有包訪問權限,這個權限我們稱爲默認權限。同一個包內的類均可以訪問默認權限的類成員。表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中有
public
、protected
、private
三個權限關鍵字,但卻有四種權限,因爲默認權限就是不寫關鍵字的時候。 - 如果想在子類中指定調用父類的某個方法,可以使用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 操作題
- 如果使用6.2.5設計的ArrayList類收集對象,想顯示所收集對象的字符串描述時,會顯示非常麻煩。嘗試重寫toString()方法,讓客戶端可以方便的顯示所收集對象的字符串描述。
- 接上題,請重寫ArrayList類的equals()方法,先比較收集的數量是否相等,然後對應位置比較各對象的內容是否相等(使用各對象的equals()),只有數量相等且對應位置的各個對象的內容相等,才判斷兩個ArrayList對象是相等的。