《Java從入門到失業》第四章:類和物件(4.3):一個完整的例子帶你深入類和物件

Java大失叔發表於2020-09-19

4.3一個完整的例子帶你深入類和物件

       到此為止,我們基本掌握了類和物件的基礎知識,並且還學會了String類的基本使用,下面我想用一個實際的小例子,逐步來討論類和物件的一些其他知識點。

4.3.1需求及分析

       大失叔比較喜歡打麻將,畢竟是國粹嘛,哈哈!因此我打算用一個“自動麻將桌”的小程式來探討(我相信你們大多數也都會打,如果實在不會,自己百度科普下吧)。需求很簡單,說明如下:

  1. 一共136張麻將牌
  2. 西施、王昭君、貂蟬、楊貴妃4個人玩
  3. 座位東固定為莊家
  4. 程式開始執行後,4個人隨機落座在東南西北座位,然後麻將桌自動洗牌,洗完後,座位東開始抓牌,按東南西北順序抓牌。
  5. 4個人都抓完牌後,在控制檯列印如下資訊:

座位東,莊家,某某某,手牌為:[1萬][2萬]………

座位南,閒家,某某某,手牌為:[1萬][2萬]………

座位西,閒家,某某某,手牌為:[1萬][2萬]………

座位北,閒家,某某某,手牌為:[1萬][2萬]………

  假如我們用程式導向的方法來做,大概思路為:

  1. 用一個陣列M來儲存136張麻將
  2. 用陣列P來儲存4個人名字,同時順序代表東南西北
  3. 用陣列A、B、C、D分別儲存座位東、南、西、北座位上的人的手牌
  4. 編寫一個落座函式,打亂P的排序
  5. 編寫一個洗牌函式,打亂M的排序
  6. 編寫一個抓牌函式,往A、B、C、D中新增麻將
  7. 編寫一個列印函式,列印結果

  用一張圖示意如下:

 

在沒有接觸物件導向程式設計之前,很容易就想到類似上面這種思路。但是如果用物件導向的思想來解決這個問題的話,一般怎麼做呢?根據我多年的經驗,總結幾個步驟如下:

  1. 分析需求中涉及到哪些事物、實體以及它們之間的關係
  2. 將事物或實體抽象成類,分析它們會有哪些屬性,應該提供哪些方法
  3. 編寫程式來實現第2步
  4. 第2、3步會相互迭代,最後解決問題

我們嘗試按照上面步驟來分析一下:

  1. 4大美人圍著一張麻將桌打麻將,涉及到的實體有:美人、麻將桌、麻將。美人手裡會抓麻將;麻將桌會洗牌(即打亂麻將順序,然後排列好)。
  2. 將實體抽象成麻將類(Mahjong)、桌子類(MahjongTable)、美人類(Player)。然後結合問題的需求和直觀感受,我們來分析下每個類具有什麼屬性和方法。
  3. 對於麻將類,每個麻將都有不同的文字,比如1萬、3筒、東風。我們把這個文字叫做文字屬性好了。至於方法暫時想不到,先空著。
  4. 對於美人,每個人都有名字屬性,其他屬性暫時也想不到。都有抓牌這個行為,那麼就有一個抓牌方法。另外真實打麻將時,一般都是由莊家來按麻將桌上的洗牌按鈕,那麼還得有一個發動洗牌的行為。
  5. 對於麻將桌,有4個座位,其實就是坐著4個人,那麼可以認為有4個屬性:東玩家、南玩家、西玩家、北玩家。其次它擁有一副麻將,可以用一個陣列來存放這副麻將,就是麻將陣列屬性。行為顯而易見,得提供一個洗牌的功能,供莊家啟動。

我們用一張圖來把上面的分析示意一下:

 

4.3.2原始檔與類

  接下來,我們開始編寫這些類。第一個知識點來了,在Java中,如何編寫多個類?之前我們只寫過一個HelloWorld的類,現在需要寫3個類,是放在一個檔案中,還是放在3個檔案中呢?事實上,在Java中,關於原始檔和類,有如下約定:

  • 一個原始檔中可以有一個或多個類
  • 一個原始檔中可以沒有公有類
  • 當一個原始檔中有多個類的時候,最多隻能有一個類被public修飾,即只能有一個公有類
  • 當原始檔中有公有類時,原始檔的命名必須和這個公有類名一致。
  • 當原始檔中沒有公有類時,原始檔的命名可以任意命名為符合命名規範的名字

是不是覺得挺繞的?事實上,我們在實際工作運用中,一般習慣一個類對應一個原始檔,只有在極少數情況下才會把多個類放在一個原始檔中。在這個例子中,我們將編寫3個原始檔來對應這3個類。

4.3.3編寫麻將類

       一般情況下,我們編寫一個類的步驟分3步:定義類名、編寫屬性、編寫方法。上面我們還提到過公有類,當一個類被public修飾符修飾的時候,這個類就是公有類,公有類可以被整個程式中任意一個其他類引用,具體關於類的修飾後面會討論。定義一個類的基本格式如下:

修飾符 class 類名{

       屬性

       構造方法

       其他方法

}

我們按照這個格式,先編寫麻將類,從示意圖上我們看到,麻將類很簡單,只有一個屬性,沒有方法:

public class Mahjong {  
    private String word;// 麻將的文字  
  
    /** 
     * 構造方法 
     * @param word 該麻將的文字 
     */  
    public Mahjong(String word) {  
        this.word = word;  
    }  
}      

4.3.4構造器

       我們看到,麻將類的類名我管它叫Mahjong(這是麻將的英文翻譯),它符合識別符號的規定(還記得識別符號的規定嗎?不記得了回去翻看3.2)。然後有一個構造器方法,構造器方法和類名同名,接受一個String型別的引數。前面我們學習String類的時候,String類有15個構造器方法,同時我們也學習瞭如何構造一個新的物件,就是使用new關鍵字。我們要建立一個Mahjong物件,就可以用如下語句:

Mahjong m = new Mahjong("8萬");

現在,我們再補充一下關於構造器的一些知識點:

  • 一個類可以有一個以上的構造器
  • 構造器可以有任意個引數
  • 構造器無返回值
  • 構造器必須和類名同名

另外,我們看到,在構造器中只有一句程式碼:

this.word = word; 

目的就是將新構造出來的物件的word屬性的值設定為傳進來的值。因為方法的引數名字和屬性名字重複了,為了加以區分,用到了this關鍵字。this代表物件本身。關於this的用法以後還會講解。

4.3.5編寫麻將桌類

       有了麻將類後,我們繼續編寫麻將桌類。麻將桌類相對複雜,它具有5個屬性和1個方法,我們先編寫一個大概出來:

public class MahjongTable {  
    // 座位東上的玩家  
    private Player dong;  
    // 座位南上的玩家  
    private Player nan;  
    // 座位西上的玩家  
    private Player xi;  
    // 座位北上的玩家  
    private Player bei;  
    // 一副麻將  
    private Mahjong[] mahjongArray;  
  
    // 構造方法  
    public MahjongTable() {  
  
    }  
  
    // 洗牌方法  
    public void xipai() {  
  
    }  
}  

首先我們看到,對於座位東南西北,我們都是Player型別的。Player實際上就是美人(這裡我們叫玩家)。因為最終座位上坐著的都是人。我們提前編寫了一個空的Player類(程式碼後面展示),以便於編寫麻將桌類不會出現編譯錯誤。

接著,我們來完善一下構造方法。我們想一下,對於一張麻將桌,它其實可能存在幾種情況:

  • 一張空桌子,桌子上沒有麻將,凳子上也沒有人
  • 桌子上有麻將,凳子上沒有人
  • 桌子上有麻將,凳子上坐好了人,準備開打

因此,我們可能需要提供3個構造器,程式碼如下:

    // 構造方法  
    public MahjongTable() {  
  
    }  
  
    // 構造方法  
    public MahjongTable(Mahjong[] mahjongArray) {  
        this.mahjongArray = mahjongArray;  
    }  
  
    // 構造方法  
    public MahjongTable(Mahjong[] mahjongArray, Player dong, Player nan, Player xi, Player bei) {  
        this(mahjongArray);  
        this.dong = dong;  
        this.nan = nan;  
        this.xi = xi;  
        this.bei = bei;  
    } 

4.3.6物件的構造

       我們編寫麻將類的時候,知道如何編寫一個簡單的構造器,用來構造一個物件,同時對物件的屬性進行初始化。但是編寫麻將桌類的時候,發現有時候一個構造器不能滿足需求,因此Java提供了多種編寫構造器的方式,這裡我們將進一步討論一下。

4.3.6.1預設構造器及預設屬性

       我們注意到,麻將桌類的第一個構造器沒有任何引數,像這種構造器,我們稱之為“預設構造器”。假如我們編寫某一個類,它只需要一個預設構造器,這時候我們可以省略掉這個構造器的程式碼。這樣在編譯的時候,Java會主動給我們提供一個預設構造器。如果我們編寫了任何帶引數的構造器,Java則不會再提供預設構造器。

       一般的,我們都會在構造器中對類的屬性進行初始化,但是有時候我們可能也不會初始化。如果我們的構造器中沒有初始化某些屬性,那麼當用構造器構造物件時,那些沒有被初始化的屬性,系統會自動的給予預設值。還記得我們在學習基本資料型別時的預設值嗎?那些預設值的含義就是這時候起作用。這裡再總結一下預設值:

型別

預設值

byte

0

short

0

int

0

long

0L

float

0.0f

double

0.0d

boolean

false

char

\u0000

物件

null

       不過一般情況,不建議利用預設值的機制來給屬性賦值,良好的程式設計習慣還是建議顯性的初始化屬性。因此對於麻將桌類的預設構造器,我們應該顯性的初始化一副麻將出來,否則當利用預設構造器構造出來一個麻將桌類後,繼續呼叫洗牌方法則會報錯(因為我們洗牌必然會用到麻將陣列物件)。這裡暫時先不編寫程式碼,因為下面會討論這個地方。

4.3.6.2方法過載

       我們看到,麻將桌類除了提供一個預設構造器外,另外還提供了2個構造器用於滿足不同情況的需求。這種多個同名的方法現象稱之為“過載”(overloading)。過載可以是構造方法過載,也可以是其他方法,事實上,Java允許過載任何方法。那麼當外界呼叫具有多個同名方法中的一個時,編譯器如何區分呼叫的是哪一個呢?這就要求過載需要滿足一定的規定。

       我們先看一下方法的構成:修飾符、返回值、方法名、引數列表。理論上只要這4項不完全一樣,就可以區分一個方法,但是實際上在Java中,只用後2項來完整的描述一個方法,稱之為方法簽名。過載的規定就是要求方法簽名不一樣即可,既然過載的方法方法名是一樣的,那麼實質上也就是要求引數列表不能一樣。引數列表有2個要素:引數個數和引數型別。因此只需要滿足下列要求即可:

  • 引數數量不同
  • 引數數量相同時,對應位置上的引數型別不完全相同

前面我們學習過String類,String類中就15個構造方法,同時它還有很多其他的過載方法,例如:

indexOf(int ch)
indexOf(String str)
indexOf(int ch, int fromIndex)
indexOf(String str, int fromIndex)

這裡特別需要注意的是,返回值不屬於方法簽名的一部分,因此不能存在2個方法名相同、引數列表完全一致、返回值不同的方法。

4.3.6.3構造器中呼叫另一個構造器

       我們觀察一下麻將桌類的第3個構造器的第一句程式碼:

this(mahjongArray); 

這裡又一次用到了this關鍵字。在這裡,表示呼叫另外一個構造器,實際上就是第2個構造器。用這種方式有一個很大的好處,就是對於構造物件的公共程式碼可以只需要編寫一次。這種方式在實際工作運用中會經常用到。這裡需要注意的是,呼叫另一個構造器的程式碼必須放在第一句。

4.3.7重新設計麻將類

       還記得上面討論預設構造器的時候,說過需要顯式的初始化一副麻將嗎? 一副麻將一共有136張,我們要初始化一副麻將,如果按照我們上面麻將類的定義,需要呼叫136次麻將類的構造方法才能完成,這顯然不是一個很好的設計,因此我們有理由懷疑我們一開始的設計存在缺陷,因此我們需要重新思考一下麻將類的設計。這也是為什麼我在討論用物件導向的思想解決問題步驟中說到“抽象類”與“編寫程式碼”這2個過程需要相互迭代的原因,因為在實際工作運用中,需求比這個問題複雜的多,沒有人一開始就能設計的非常完美,經常在編碼階段需要回過頭去重新設計。當然隨著經驗的增長,會讓這種迭代工作越來越少。此為後話,我們先討論如何重新設計麻將類。

       我們的目標是不想重複呼叫多次麻將的構造方法,前面我們學習流程控制的時候,學過迴圈語句,迴圈就可以用來解決這種重複勞動。要使用迴圈,就得找到規律,麻將類的屬性是文字,就是需要找到麻將的文字屬性的規律。

       我們發現麻將的文字可以分成4大類:萬、條、筒、風。前3者的數字部分都是1-9。風牌有7張,我們也可以人為規定用1-7分別代表東南西北中發白。這樣文字屬性實際上可以拆成2部分的組合:數字+類別。對於類別我們也可以用數字來表示:1-4分別代表萬條筒風。這樣我們就可以把麻將類重新編碼如下:

 1 public class Mahjong {  
 2     public static final int TYPE_WAN = 1;  
 3     public static final int TYPE_TIAO = 2;  
 4     public static final int TYPE_TONG = 3;  
 5     public static final int TYPE_FENG = 4;  
 6   
 7     // 麻將的型別部分,取值範圍1-4,1代表萬,2代表條,3代表筒,4代表風  
 8     private int type;  
 9     // 麻將的數字部分,取值範圍1-9,如果是型別是風牌,則為1-7  
10     private int number;  
11   
12     // 構造方法  
13     public Mahjong(int type, int number) {  
14         this.type = type;  
15         this.number = number;  
16     }  
17   
18     // 返回麻將的文字屬性  
19     public String getWord() {  
20         StringBuilder sb = new StringBuilder();  
21         if (type == Mahjong.TYPE_WAN) {  
22             sb.append(this.number).append("萬");  
23         } else if (type == Mahjong.TYPE_TIAO) {  
24             sb.append(this.number).append("條");  
25         } else if (type == Mahjong.TYPE_TONG) {  
26             sb.append(this.number).append("筒");  
27         } else {  
28             if (this.number == 1) {  
29                 sb.append("東風");  
30             } else if (this.number == 2) {  
31                 sb.append("南風");  
32             } else if (this.number == 3) {  
33                 sb.append("西風");  
34             } else if (this.number == 4) {  
35                 sb.append("北風");  
36             } else if (this.number == 5) {  
37                 sb.append("紅中");  
38             } else if (this.number == 6) {  
39                 sb.append("發財");  
40             } else if (this.number == 7) {  
41                 sb.append("白板");  
42             }  
43         }  
44         return sb.toString();  
45     }  
46 }  

我們發現,第2345行多了幾行奇怪的程式碼,第19行多了一個getWord()方法。下面我們針對這些程式碼分別引入相關知識點。

4.3.8final關鍵字

我們看第2345行程式碼:

public static final int TYPE_WAN = 1;  
public static final int TYPE_TIAO = 2;  
public static final int TYPE_TONG = 3;  
public static final int TYPE_FENG = 4;  

這裡針對一個變數用到了3個修飾符:publicstaticfinalpublic就不用解釋了,表示它是一個公開的屬性,那麼任何類的任何方法都可以訪問。static關鍵字放在下一小節來介紹,這裡主要介紹final關鍵字。

       我們可以把屬性定義為final,當把一個類的屬性定義為final,那麼表示這個屬性在物件構建之後將不能再被修改。並且,這個屬性必須在構建的時候初始化。

       一般我們會用final修飾符來修飾基本資料型別的屬性。如果用來修飾類型別的屬性,要保證這個類是不可變類,例如前面我們介紹過的String類(String類就是用final修飾的類,一旦例項化後,就不能修改)。如果我們用來修飾一個可變類,將會引起不可預測的問題。因為final修飾的屬性,僅僅意味著這個屬性變數記憶體中的值不能修改,基本資料型別的變數記憶體中存放的就是數值本身,而類型別的變數記憶體中存放的實際上物件的引用(記憶體地址),雖然這個引用不可變,但是可以呼叫物件的方法改變物件的狀態,因而沒有達到不可變的目的。我們用一張記憶體示意圖來表示:

final還可以修飾類,用final修飾的類,表示這個類不能被繼承了(關於繼承後面章節會詳細討論),但是可以繼承其他的類。

final也可以修飾方法,用final修飾的方法不能被重寫(重寫也是和繼承相關的,後面章節會詳細討論)。

4.3.9static關鍵字

這一小節接著介紹static關鍵字。

4.3.9.1靜態屬性

       我們可以把一個類的屬性定義為static,這樣這個屬性就變成了一個靜態屬性,叫做類屬性(有時候也叫類變數)。相對的沒有static修飾的屬性叫做成員屬性(有時候也叫成員變數)。

對於成員屬性,我們比較熟悉了,當一個類構造了一個物件例項後,這個物件就會擁有狀態,狀態就是由成員屬性決定的,同一個類的不同的物件例項的成員屬性的取值可以是不同的,即每一個物件例項對成員屬性都有一份拷貝。

類屬性則不同,所有的物件例項共有這一個屬性,類屬性不屬於任何一個物件例項,對於一個類只有一份拷貝。並且這個屬性不需要例項化任何物件就存在(類載入後就存在),訪問該屬性的格式是:類名.類屬性名,例如:

if (type == Mahjong.TYPE_WAN) 

我們用一張記憶體示意圖來表示:

一般我們用大寫字母來命名靜態屬性。

4.3.9.2靜態方法

       我們可以用static修飾一個類的方法,這樣的方法叫做靜態方法,也可以叫做類方法。相對的,不用static修飾的類方法叫做成員方法。

       靜態方法不屬於任何一個物件,它不能操作任何物件例項,因此不能訪問成員屬性,但是可以訪問自身類的類屬性。呼叫靜態方法也不需要例項化物件。呼叫靜態方法的格式為:類名.靜態方法,其實我們已經接觸過許多靜態方法了,例如學習陣列拷貝的時候用到了System.arraycopy()方法,Arrays.copyOf()方法,麻將桌類中打亂一副麻將的Collections.shuffle()。還有Java程式的入口main方法也是靜態方法。

       其實我們也可以用物件.靜態方法的格式呼叫靜態方法,但是不建議這樣做,因為靜態方法的呼叫不需要例項化物件,這樣做容易引起誤解。

4.3.9.3靜態常量

       當我們用staticfinal同時修飾一個屬性的時候,這個屬性就變成了靜態常量。靜態常量在實際運用中會經常用到。一般我們希望一個屬性不屬於任何一個物件例項,而且不希望被修改的時候,就會定義為靜態常量。比如前面提到的麻將類的4個奇怪的屬性:

public static final int TYPE_WAN = 1;  
public static final int TYPE_TIAO = 2;  
public static final int TYPE_TONG = 3;  
public static final int TYPE_FENG = 4;  

因為我們規定用1234分別代表萬、條、筒、風。因此我們不希望被修改,同時這個規定不需要物件例項化就存在,因此我們定義為靜態常量。一般我們用大寫字母來命名靜態常量。

定義為靜態常量還有一個好處,就是我們編碼的時候,可以用類名.類屬性名的方式訪問。當我們因為設計的問題,導致需要修改常量值的時候,編寫的訪問程式碼可以不用修改,而只需要修改常量的定義即可。例如我們改為規定用5678代表萬、條、筒、風,在getWord()方法中,不需要做任何修改。

一般我們希望把屬性都定義為private,因為我們不希望外部可以訪問它。但是對於靜態常量,我們往往會定義為public,因為它是final的,因此不能被修改,只能讀取。

4.3.10修改器與訪問器

       介紹完了finalstatic關鍵字後,我們繼續討論getWord()方法。我們看到上面的麻將類、麻將桌類的所有屬性都是用private修飾符來修飾。private的意思是私有的,因此這種屬性只能由物件本身才能訪問和修改。因為我們希望把屬性封裝起來,不想讓其他類能隨便訪問到屬性。這就是體現了類的封裝性。

       但是我們在後面列印手牌的時候,需要獲得一個麻將的文字,將它顯示出來,這就必須要要訪問,因此我們提供了一個getWord()方法來獲取麻將顯示的文字。這種獲取物件的屬性值的方法,我們把它稱為屬性訪問器或屬性訪問方法。

有的時候,我們可能還會希望能夠修改某個屬性,例如對於麻將桌類,如果我們採用預設構造方法構造了一個麻將桌,那麼這個桌子上的座位暫時是沒有人的。我們接下來肯定要安排人坐到某個座位上,這就需要提供修改屬性的額方法。因此我們還需要提供4個修改座位屬性的方法:

public void setDong(Player dong) {  
    this.dong = dong;  
}  
  
public void setNan(Player nan) {  
    this.nan = nan;  
}  
  
public void setXi(Player xi) {  
    this.xi = xi;  
}  
  
public void setBei(Player bei) {  
    this.bei = bei;  
}

這種簡單的修改屬性的方法,我們把它稱為屬性修改器或屬性修改方法。

可能有的人會問了,既然又想修改又想訪問,為什麼不直接把屬性定義為public的呢?這樣就可以隨便訪問和修改了。這其實就是封裝性的一個好處,如果我們用public開放,那麼將在專案的任何地方都有可能修改這個屬性,如果我們確定某個bug是由於這個屬性導致的,那麼除錯起來將痛苦至極。而用修改器來實現,則除錯相當簡單,我們只需要除錯修改器方法即可。

另外,對於像麻將類的文字屬性來說,我們實際儲存並不是一個文字,而是由2部分int組成的屬性,但是對於外部來說,並不需要關心內部的文字是如何組合的,我們隨時可以改變內部的實現,外部呼叫getWord方法的結果不會受到影響。

事實上,以後在實際工作運用中,訪問器和修改器是一個經常會使用的方法,Eclipse甚至提供了快捷的方式直接生成訪問器和修改器,具體這裡暫時不表,以後找機會介紹。

4.3.11完善麻將桌類

重新設計完麻將類後,我們再看一下麻將桌類的預設構造方法,就可以用迴圈來實現了,程式碼如下:

public MahjongTable() {  
    this.mahjongArray = new Mahjong[136];  
    int index = 0;  
    // 用一個雙迴圈實現  
    for (int type = 1; type <= 4; type++) {  
        for (int number = 1; number <= 9; number++) {  
            // 當構造風牌的時候,數字部分不能超過7  
            if (type == 4 && number > 7) {  
                break;  
            }  
            // 每一張牌有4張  
            for (int c = 1; c <= 4; c++) {  
                this.mahjongArray[index] = new Mahjong(type, number);  
                index++;  
            }  
        }  
    }  
} 

麻將類完美了,麻將桌的預設構造方法也完成了,接下來我們繼續完成麻將類的洗牌邏輯。洗牌邏輯比較簡單,就是打亂麻將陣列的順序。

因為教程到此為止,我們還沒有學習過陣列之外的其他的資料結構,因此便於理解,一開始我故意先用陣列來存放一副麻將。事實上,陣列這種資料結構對於打亂順序這種操作的實現是比較複雜的,其實在Java中專門提供了一大塊類庫來支援資料結構,這個到後面我們會花較大的篇幅來討論,這裡為了程式能夠順利往下進行編寫,暫時先用其中的一個陣列列表類:ArrayList來實現,這裡先可以把ArrayList暫時理解為陣列。ArrayList實現打亂順序就超級簡單了,一會大家就會看到。因此我們需要重新編寫麻將桌類如下:

public class MahjongTable {  
    // 座位東上的玩家  
    private Player dong;  
    // 座位東上的玩家  
    private Player nan;  
    // 座位東上的玩家  
    private Player xi;  
    // 座位東上的玩家  
    private Player bei;  
    // 一副麻將,這裡改用ArrayList來存放  
    private ArrayList<Mahjong> mahjongList;  
    // 一副麻將  
    // private Mahjong[] mahjongArray;  
  
    // 構造方法  
    public MahjongTable() {  
        this.initMahjongList();  
    }  
  
    // 構造方法  
    public MahjongTable(ArrayList<Mahjong> mahjongList) {  
        this.mahjongList = mahjongList;  
    }  
  
    // 構造方法  
    public MahjongTable(ArrayList<Mahjong> mahjongList, Player dong, Player nan, Player xi, Player bei) {  
        this(mahjongList);  
        this.dong = dong;  
        this.nan = nan;  
        this.xi = xi;  
        this.bei = bei;  
    }  
  
    private void initMahjongList() {  
        this.mahjongList = new ArrayList<Mahjong>();// 建立一個麻將陣列列表  
        // 用一個雙迴圈實現  
        for (int type = 1; type <= 4; type++) {  
            for (int number = 1; number <= 9; number++) {  
                // 當構造風牌的時候,數字部分不能超過7  
                if (type == 4 && number > 7) {  
                    break;  
                }  
                // 每一張牌有4張  
                for (int c = 1; c <= 4; c++) {  
                    this.mahjongList.add(new Mahjong(type, number));// 往麻將陣列列表裡新增麻將  
                }  
            }  
        }  
    }  
  
    // 洗牌方法  
    public void xipai() {  
        Collections.shuffle(this.mahjongList);  
    }  
}

這裡省略了上面提到的修改器方法。針對其他部分稍做說明如下:

  • 一副麻將改用ArrayList來存放
  • 帶引數的2個構造方法的第1個引數都變成了ArrayList
  • 注意預設構造方法,內部呼叫了另一個方法,這個內容將在下一小結闡述。
  • 洗牌方法非常簡單,只有一句程式碼,這就是Java類庫提供的便利。具體會在以後討論集合類的時候詳細討論。

4.3.12公有方法和私有方法

       上面麻將桌類的預設構造方法呼叫了另外一個方法,這個方法是用private修飾的。為什麼這麼設計呢?public和和private有什麼區別呢?

       前面我們說過,對於一個類,一般來說,我們習慣把屬性都設定為private的,因為設計為public的比較危險,也破壞了類的封裝性。那麼對於方法來說,一般我們會把方法設計為public的,因為我們大多數方法都相當於類的行為,這些行為類似於功能,都需要提供給外部使用的。但是有的方法是我們內部輔助用的,並不希望暴露給外部使用,這時候我們就可以用private關鍵字來修飾。像上面麻將桌類的initMahjongList() 這個方法主要是用來初始化一副麻將的,並不希望暴露給外部使用。用private修改後,我們可以隨意修改實現,只要不影響暴露給外部的哪些方法的結果即可,這也同樣體現了類的封裝性的優越性。這就好比iphone11,不同批次的iphone11可能內部某些零件廠商不一樣,但是對使用者來說是透明的。

       到此為止,我們瞭解了用publicprivate來修飾類的屬性、類的方法,也知道了修飾後帶來的結果以及基本原理,這樣我們自己在設計類的時候,可以靈活運用。其實還可以用publicprivate來修飾類,像我們的麻將類、麻將桌類都是用public來修飾的。publicprivate主要用來控制訪問級別的,其實在Java中,一共有4中訪問級別,關於這部分內容我們以後還會闡述。

4.3.13美人類

       前面我們編寫麻將桌類的時候,實際上已經引用了美人類Player。按照我們最初的設計,美人類有2個屬性:名字和手牌;2個方法:抓牌方法和啟動洗牌。我們先把程式碼結構編寫出來:

public class Player {  
    // 名字  
    private String name;  
    // 手牌  
    private ArrayList<Mahjong> handList;  
  
    // 構造方法  
    public Player(String name) {  
        this.name = name;  
        this.handList = new ArrayList<Mahjong>();  
    }  
  
    // 抓牌方法  
    public void zhuapai(Mahjong mahjong) {  
        this.handList.add(mahjong);  
    }  
  
    // 啟動洗牌  
    public void xipai() {  
  
    }  
  
    // 獲取手牌列表,以便列印手牌  
    public ArrayList<Mahjong> getHandList() {  
        return this.handList;  
    }  
}

接下來,我們肯定是要完善啟動洗牌方法,但是我們發現,如果需要啟動洗牌,必須要呼叫麻將桌的洗牌方法,那麼就得在美人類中持有一個麻將桌,感覺這樣挺彆扭的。其實我們還可以換一種思路,就是把麻將桌看成一個主導類,美人落座後,由它來洗牌,洗完牌後由它來給每個美人發牌,這樣設計以後,美人類就可以沒有啟動洗牌方法了。這樣設計以後,麻將桌類需要補一個發牌方法:

public void fapai() {  
    // 抓3輪,每一輪每個人抓4張  
    for (int i = 0; i < 3; i++) {  
        for (int j = 0; j < 4; j++) {  
            this.dong.zhuapai(this.mahjongList.remove(0));  
        }  
        for (int j = 0; j < 4; j++) {  
            this.nan.zhuapai(this.mahjongList.remove(0));  
        }  
        for (int j = 0; j < 4; j++) {  
            this.xi.zhuapai(this.mahjongList.remove(0));  
        }  
        for (int j = 0; j < 4; j++) {  
            this.bei.zhuapai(this.mahjongList.remove(0));  
        }  
    }  
    // 最後一輪,莊家抓2張,其餘抓1張  
    this.dong.zhuapai(this.mahjongList.remove(0));  
    this.nan.zhuapai(this.mahjongList.remove(0));  
    this.xi.zhuapai(this.mahjongList.remove(0));  
    this.bei.zhuapai(this.mahjongList.remove(0));  
    this.dong.zhuapai(this.mahjongList.remove(0));  
}  

4.3.14main方法

       到此為止,我們已經編寫完所有的類了,但是如何讓程式執行呢?還記得我們在第三章的HelloWorld的例子中介紹過嗎?一個程式執行必須需要有一個入口,Java的入口就是main方法,他的標準格式為:public static void main(String args[])

Java的規範要求必須這麼寫,為什麼要這麼定義呢?這和JVM的執行有關係。還記得我們用命令列執行Java程式嗎?當我們執行命令“java 類名時,虛擬機器會執行該類中的main方法。因為不需要例項化這個類的物件,因此需要是限制為public staticJava還規定main方法不能由返回值,因此返回值型別為void

main方法中還有一個輸入引數,型別為String[],這個也是java的規範,main()方法中必須有一個入參,型別必須String[],至於字串陣列的名字,可以自己命名,但是根據習慣一般都叫args

事實上,我們可以在每個類中都寫一個main方法,這樣有一個好處,就是可以非常方便的做單元測試。這個好處等以後大家實際工作中就會體會到了。

4.3.15執行程式

       介紹完main方法,我們就需要著手編寫一個main方法。為了不影響任何一個類,我們可以再編寫一個原始檔,專門用來存放main方法,我們叫做Main好了。Main方法的步驟如下:

  1. 構造一個麻將桌
  2. 構造4個美人
  3. ArrayList存放4個美人,然後打亂順序
  4. 4個美人落座到麻將桌中
  5. 洗牌、發牌
  6. 列印

1. 但是列印的時候,我們發現需要呼叫美人類的getHandList方法,但是麻將桌並沒有開放美人類屬性,因此無法訪問。因此決定在麻將桌類開放一個列印方法。

       最終,將編寫好的4個類程式碼摘抄如下:

麻將類:

public class Mahjong {  
    public static final int TYPE_WAN = 1;  
    public static final int TYPE_TIAO = 2;  
    public static final int TYPE_TONG = 3;  
    public static final int TYPE_FENG = 4;  
  
    // 麻將的型別部分,取值範圍1-4,1代表萬,2代表條,3代表筒,4代表風  
    private int type;  
    // 麻將的數字部分,取值範圍1-9,如果是型別是風牌,則為1-7  
    private int number;  
  
    // 構造方法  
    public Mahjong(int type, int number) {  
        this.type = type;  
        this.number = number;  
    }  
  
    // 返回麻將的文字屬性  
    public String getWord() {  
        StringBuilder sb = new StringBuilder();  
        if (type == Mahjong.TYPE_WAN) {  
            sb.append(this.number).append("萬");  
        } else if (type == Mahjong.TYPE_TIAO) {  
            sb.append(this.number).append("條");  
        } else if (type == Mahjong.TYPE_TONG) {  
            sb.append(this.number).append("筒");  
        } else {  
            if (this.number == 1) {  
                sb.append("東風");  
            } else if (this.number == 2) {  
                sb.append("南風");  
            } else if (this.number == 3) {  
                sb.append("西風");  
            } else if (this.number == 4) {  
                sb.append("北風");  
            } else if (this.number == 5) {  
                sb.append("紅中");  
            } else if (this.number == 6) {  
                sb.append("發財");  
            } else if (this.number == 7) {  
                sb.append("白板");  
            }  
        }  
        return sb.toString();  
    }  
}  

美人類:

public class Player {  
    // 名字  
    private String name;  
    // 手牌  
    private ArrayList<Mahjong> handList;  
  
    // 構造方法  
    public Player(String name) {  
        this.name = name;  
        this.handList = new ArrayList<Mahjong>();  
    }  
  
    // 抓牌方法  
    public void zhuapai(Mahjong mahjong) {  
        this.handList.add(mahjong);  
    }  
  
    public String getName() {  
        return this.name;  
    }  
  
    // 獲取手牌列表,以便列印手牌  
    public ArrayList<Mahjong> getHandList() {  
        return this.handList;  
    }  
}  

麻將桌類:

public class MahjongTable {  
    // 座位東上的玩家  
    private Player dong;  
    // 座位東上的玩家  
    private Player nan;  
    // 座位東上的玩家  
    private Player xi;  
    // 座位東上的玩家  
    private Player bei;  
    // 一副麻將,這裡改用ArrayList來存放  
    private ArrayList<Mahjong> mahjongList;  
    // 一副麻將  
    // private Mahjong[] mahjongArray;  
  
    // 構造方法  
    public MahjongTable() {  
        this.initMahjongList();  
    }  
  
    // 構造方法  
    public MahjongTable(ArrayList<Mahjong> mahjongList) {  
        this.mahjongList = mahjongList;  
    }  
  
    // 構造方法  
    public MahjongTable(ArrayList<Mahjong> mahjongList, Player dong, Player nan, Player xi, Player bei) {  
        this(mahjongList);  
        this.dong = dong;  
        this.nan = nan;  
        this.xi = xi;  
        this.bei = bei;  
    }  
  
    private void initMahjongList() {  
        this.mahjongList = new ArrayList<Mahjong>();// 建立一個麻將陣列列表  
        // 用一個雙迴圈實現  
        for (int type = 1; type <= 4; type++) {  
            for (int number = 1; number <= 9; number++) {  
                // 當構造風牌的時候,數字部分不能超過7  
                if (type == 4 && number > 7) {  
                    break;  
                }  
                // 每一張牌有4張  
                for (int c = 1; c <= 4; c++) {  
                    this.mahjongList.add(new Mahjong(type, number));// 往麻將陣列列表裡新增麻將  
                }  
            }  
        }  
    }  
  
    // 洗牌方法  
    public void xipai() {  
        Collections.shuffle(this.mahjongList);  
    }  
  
    // 發牌方法  
    public void fapai() {  
        // 抓3輪,每一輪每個人抓4張  
        for (int i = 0; i < 3; i++) {  
            for (int j = 0; j < 4; j++) {  
                this.dong.zhuapai(this.mahjongList.remove(0));  
            }  
            for (int j = 0; j < 4; j++) {  
                this.nan.zhuapai(this.mahjongList.remove(0));  
            }  
            for (int j = 0; j < 4; j++) {  
                this.xi.zhuapai(this.mahjongList.remove(0));  
            }  
            for (int j = 0; j < 4; j++) {  
                this.bei.zhuapai(this.mahjongList.remove(0));  
            }  
        }  
        // 最後一輪,莊家抓2張,其餘抓1張  
        this.dong.zhuapai(this.mahjongList.remove(0));  
        this.nan.zhuapai(this.mahjongList.remove(0));  
        this.xi.zhuapai(this.mahjongList.remove(0));  
        this.bei.zhuapai(this.mahjongList.remove(0));  
        this.dong.zhuapai(this.mahjongList.remove(0));  
    }  
  
    // 列印手牌方法  
    public void dayin() {  
        StringBuilder sb = new StringBuilder();  
        // 列印座位東  
        ArrayList<Mahjong> hands = this.dong.getHandList();  
        sb.append("座位東,莊家,").append(this.dong.getName()).append(",手牌為:");  
        for (Mahjong m : hands) {  
            sb.append("[").append(m.getWord()).append("]");  
        }  
        System.out.println(sb.toString());  
        // 列印座位南  
        sb = new StringBuilder();  
        hands = this.nan.getHandList();  
        sb.append("座位南,閒家,").append(this.nan.getName()).append(",手牌為:");  
        for (Mahjong m : hands) {  
            sb.append("[").append(m.getWord()).append("]");  
        }  
        System.out.println(sb.toString());  
        // 列印座位西  
        sb = new StringBuilder();  
        hands = this.xi.getHandList();  
        sb.append("座位西,閒家,").append(this.xi.getName()).append(",手牌為:");  
        for (Mahjong m : hands) {  
            sb.append("[").append(m.getWord()).append("]");  
        }  
        System.out.println(sb.toString());  
        // 列印座位北  
        sb = new StringBuilder();  
        hands = this.bei.getHandList();  
        sb.append("座位北,閒家,").append(this.bei.getName()).append(",手牌為:");  
        for (Mahjong m : hands) {  
            sb.append("[").append(m.getWord()).append("]");  
        }  
        System.out.println(sb.toString());  
    }  
  
    public void setDong(Player dong) {  
        this.dong = dong;  
    }  
  
    public void setNan(Player nan) {  
        this.nan = nan;  
    }  
  
    public void setXi(Player xi) {  
        this.xi = xi;  
    }  
  
    public void setBei(Player bei) {  
        this.bei = bei;  
    }  
}  

入口類:

public class Main {  
    public static void main(String[] args) {  
        // 第一步,構造一個麻將桌  
        MahjongTable table = new MahjongTable();  
  
        // 第二步,構造4個美人  
        Player xishi = new Player("西施");  
        Player wangzhaojun = new Player("王昭君");  
        Player diaochan = new Player("貂蟬");  
        Player yangguifei = new Player("楊貴妃");  
  
        // 第三步,用ArrayList存放4個美人,然後隨機打亂順序  
        ArrayList<Player> playerList = new ArrayList<Player>();  
        playerList.add(xishi);  
        playerList.add(wangzhaojun);  
        playerList.add(diaochan);  
        playerList.add(yangguifei);  
        Collections.shuffle(playerList);  
  
        // 第4步,美人落座  
        table.setDong(playerList.get(0));  
        table.setNan(playerList.get(1));  
        table.setXi(playerList.get(2));  
        table.setBei(playerList.get(3));  
  
        // 第5步,洗牌,發牌  
        table.xipai();  
        table.fapai();  
  
        // 第6步,列印  
        table.dayin();  
    }  
}

最後,我們執行一下,還記得Eclipse怎麼執行程式嗎?這裡再教一次:

切換到檔案Main,然後點選工具欄上的紅框圖示,按照圖示即可。當然,還有其他方式,這個等以後有經驗了,熟練了自然都會學會。我們看一下執行結果:

座位東,莊家,王昭君,手牌為:[8筒][西風][9條][6萬][2萬][3萬][6筒][2筒][4筒][紅中][3筒][3萬][8條][5條]  
座位南,閒家,楊貴妃,手牌為:[9筒][8萬][發財][4萬][南風][3筒][紅中][7萬][6條][南風][1筒][5條][4萬]  
座位西,閒家,貂蟬,手牌為:[南風][5條][北風][9筒][8萬][6條][7條][紅中][4筒][8筒][9萬][西風][紅中]  
座位北,閒家,西施,手牌為:[2條][8條][東風][南風][白板][5萬][白板][東風][2筒][2條][1條][7條][7筒]  

執行多次,可以發現每次執行的結果都不一樣,表示無論座次還是手牌,都是隨機的,完全滿足需求。當然,這些程式碼有些地方是為了引入知識點而故意設計的,不是最好的解決方案。

4.3.16總結

本小結用一個有一點小小複雜的例子,引入了相當多的知識點,旨在幫助我們學習和理解類和物件,掌握一些基礎的知識。現在簡單的總結一下:                                                                                                                        

  • 物件導向思路的基本步驟

通過4個步驟,學會分析問題需求,如何抽象出類,然後設計和編碼相互迭代的過程

  • 原始檔與類的關係

一般情況下,建議一個類一個原始檔

  • 物件的構造

掌握如何編寫構造方法、預設構造方法、構造物件時屬性的預設值規定、方法過載、this關鍵字等

  • final關鍵字

特別注意不要用final修飾可變類

  • static關鍵字

瞭解類變數和成員變數區別、類方法和成員方法的區別、靜態常量的使用等

  • 公有方法和私有方法

掌握怎麼設計類的方法,瞭解類封裝性的作用和好處

  • 修改器與訪問器

掌握怎麼設計類的屬性,瞭解類封裝性的作用和好處

  • 入口main方法

進一步闡述main方法的相關知識

最後,留一個作業吧,把麻將改成鬥地主,嘗試編寫一個小程式。

相關文章