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. 编写一个打印函数,打印结果

  用一张图示意如下:

 《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象-LMLPHP

在没有接触面向对象编程之前,很容易就想到类似上面这种思路。但是如果用面向对象的思想来解决这个问题的话,一般怎么做呢?根据我多年的经验,总结几个步骤如下:

  1. 分析需求中涉及到哪些事物、实体以及它们之间的关系
  2. 将事物或实体抽象成类,分析它们会有哪些属性,应该提供哪些方法
  3. 编写程序来实现第2步
  4. 第2、3步会相互迭代,最后解决问题

我们尝试按照上面步骤来分析一下:

  1. 4大美人围着一张麻将桌打麻将,涉及到的实体有:美人、麻将桌、麻将。美人手里会抓麻将;麻将桌会洗牌(即打乱麻将顺序,然后排列好)。
  2. 将实体抽象成麻将类(Mahjong)、桌子类(MahjongTable)、美人类(Player)。然后结合问题的需求和直观感受,我们来分析下每个类具有什么属性和方法。
  3. 对于麻将类,每个麻将都有不同的文字,比如1万、3筒、东风。我们把这个文字叫做文字属性好了。至于方法暂时想不到,先空着。
  4. 对于美人,每个人都有名字属性,其他属性暂时也想不到。都有抓牌这个行为,那么就有一个抓牌方法。另外真实打麻将时,一般都是由庄家来按麻将桌上的洗牌按钮,那么还得有一个发动洗牌的行为。
  5. 对于麻将桌,有4个座位,其实就是坐着4个人,那么可以认为有4个属性:东玩家、南玩家、西玩家、北玩家。其次它拥有一副麻将,可以用一个数组来存放这副麻将,就是麻将数组属性。行为显而易见,得提供一个洗牌的功能,供庄家启动。

我们用一张图来把上面的分析示意一下:

 《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象-LMLPHP

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则不会再提供默认构造器。

       一般的,我们都会在构造器中对类的属性进行初始化,但是有时候我们可能也不会初始化。如果我们的构造器中没有初始化某些属性,那么当用构造器构造对象时,那些没有被初始化的属性,系统会自动的给予默认值。还记得我们在学习基本数据类型时的默认值吗?那些默认值的含义就是这时候起作用。这里再总结一下默认值:

       不过一般情况,不建议利用默认值的机制来给属性赋值,良好的编程习惯还是建议显性的初始化属性。因此对于麻将桌类的默认构造器,我们应该显性的初始化一副麻将出来,否则当利用默认构造器构造出来一个麻将桌类后,继续调用洗牌方法则会报错(因为我们洗牌必然会用到麻将数组对象)。这里暂时先不编写代码,因为下面会讨论这个地方。

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修饰的属性,仅仅意味着这个属性变量内存中的值不能修改,基本数据类型的变量内存中存放的就是数值本身,而类类型的变量内存中存放的实际上对象的引用(内存地址),虽然这个引用不可变,但是可以调用对象的方法改变对象的状态,因而没有达到不可变的目的。我们用一张内存示意图来表示:

《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象-LMLPHP

final还可以修饰类,用final修饰的类,表示这个类不能被继承了(关于继承后面章节会详细讨论),但是可以继承其他的类。

final也可以修饰方法,用final修饰的方法不能被重写(重写也是和继承相关的,后面章节会详细讨论)。

4.3.9static关键字

这一小节接着介绍static关键字。

4.3.9.1静态属性

       我们可以把一个类的属性定义为static,这样这个属性就变成了一个静态属性,叫做类属性(有时候也叫类变量)。相对的没有static修饰的属性叫做成员属性(有时候也叫成员变量)。

对于成员属性,我们比较熟悉了,当一个类构造了一个对象实例后,这个对象就会拥有状态,状态就是由成员属性决定的,同一个类的不同的对象实例的成员属性的取值可以是不同的,即每一个对象实例对成员属性都有一份拷贝。

类属性则不同,所有的对象实例共有这一个属性,类属性不属于任何一个对象实例,对于一个类只有一份拷贝。并且这个属性不需要实例化任何对象就存在(类加载后就存在),访问该属性的格式是:类名.类属性名,例如:

if (type == Mahjong.TYPE_WAN) 

我们用一张内存示意图来表示:

《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象-LMLPHP

一般我们用大写字母来命名静态属性。

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怎么运行程序吗?这里再教一次:

《Java从入门到失业》第四章:类和对象(4.3):一个完整的例子带你深入类和对象-LMLPHP

切换到文件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方法的相关知识

最后,留一个作业吧,把麻将改成斗地主,尝试编写一个小程序。

09-19 21:12