华为鸿蒙 ASP.NET Core browser pyspark bluetooth jboss alertifyjs vue响应式布局 npm安装vue jq获取元素 java两个数组合并 java三维数组 java多行注释 完全去vm去虚拟化工具 idea批量替换快捷键 java9 java中string java中的string java写文件 java的运行环境 java获取当前ip java连接mysql的jar包 java注释规范 groupby 高等数学同济第七版 咪咕客户端下载 php取整函数 脚本错误怎么解决 朋友圈访客记录教程 c语言编程实例 tomcat修改端口 kafka权威指南 深入解析windows操作系统 web聊天室 黑客攻防技术宝典 例外被抛出且未被接住 ps给图片加边框 steam错误代码118怎么解决 拼多多推广软件 二代妖精下载
当前位置: 首页 > 学习教程  > 编程语言

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

2020/9/19 14:46:46 文章标签:

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 }  
我们发现,第2、3、4、5行多了几行奇怪的代码,第19行多了一个getWord()方法。下面我们针对这些代码分别引入相关知识点。

4.3.8final关键字

我们看第2、3、4、5行代码:

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个修饰符:public、static、final。public就不用解释了,表示它是一个公开的属性,那么任何类的任何方法都可以访问。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静态常量

       当我们用static和final同时修饰一个属性的时候,这个属性就变成了静态常量。静态常量在实际运用中会经常用到。一般我们希望一个属性不属于任何一个对象实例,而且不希望被修改的时候,就会定义为静态常量。比如前面提到的麻将类的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; 

因为我们规定用1、2、3、4分别代表万、条、筒、风。因此我们不希望被修改,同时这个规定不需要对象实例化就存在,因此我们定义为静态常量。一般我们用大写字母来命名静态常量。

定义为静态常量还有一个好处,就是我们编码的时候,可以用类名.类属性名的方式访问。当我们因为设计的问题,导致需要修改常量值的时候,编写的访问代码可以不用修改,而只需要修改常量的定义即可。例如我们改为规定用5、6、7、8代表万、条、筒、风,在getWord()方法中,不需要做任何修改。

一般我们希望把属性都定义为private,因为我们不希望外部可以访问它。但是对于静态常量,我们往往会定义为public,因为它是final的,因此不能被修改,只能读取。

4.3.10修改器与访问器

       介绍完了final、static关键字后,我们继续讨论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可能内部某些零件厂商不一样,但是对用户来说是透明的。

       到此为止,我们了解了用public和private来修饰类的属性、类的方法,也知道了修饰后带来的结果以及基本原理,这样我们自己在设计类的时候,可以灵活运用。其实还可以用public和private来修饰类,像我们的麻将类、麻将桌类都是用public来修饰的。public和private主要用来控制访问级别的,其实在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 static。Java还规定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方法的相关知识

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


本文链接: http://www.dtmao.cc/news_show_200171.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?