视频:《零基础学习Android开发》第四课 Java语言基础3-2
参数传递方式、重构
三、继续代码整理
有了方法的基本知识打底后,我们继续进行代码的整理,当我们想用已经做好的getSum方法来代替后面的2处点数代码时,就发现坏了!后面的代码不仅同时进行了求和,还有字符串的处理,以字符串的形式记录了牌局结算信息。我们也同样用图来表示:
那我们已经做好的getSum方法显然是不满足要求的,需要增加对字符串text的输出。但是我们看一下方法定义的语法会发现问题来了,
解决办法有两个,一个是把这两个变量合成一个,也就是使用复合类型的数据结构。复合类型我们到目前为止只学了数组,数组显然是不合适的,因为数组要求放进去的元素类型是一样的。那有没有把不同类型放在一起的数据结构呢?也是有的,在有的语言里有“结构体”,英文是struct,这种复合类型。但是!有一个坏消息一个好消息,先说坏消息,Java语言里没有结构体这个类型!好消息呢?牛粪有的是!不是,Java里有结构体的升级版本“类”。类放在后面再说,而且这里用起来也太麻烦,我们就不用这种方法。第二个则是从输入参数上想办法,我们先试试把getSum方法改写成如下所示:
private int getSum(int[] pokersA, int i, String text){ int sum = 0; for(int j = 0; j <= i; j++){ if( pokersA[j] == 0 ){ // 手里的牌是0,说明没有拿牌了 break; } sum += pokersA[j]; text += pokersA[j] + ","; } return sum;}
将第一次调用它的代码改写成如下:
int sum = getSum(pokersA, i, "");
因为第一次调用不需要接收字符串的输出,因此我们传入一个空字符串。第二次调用与第三次调用则改写如下:
// 显示玩家手里的牌及总数 String text = "你手中的牌分别是:"; int sumA = getSum(pokersA, 3, text); text += "总数是:" + sumA + "。对家手里的牌是:"; // 显示电脑手里的牌及总数 int sumB = getSum(pokersB, 3, text);
我们看一下运行结果,如图:
可以发现没有得到我们想要的结果,获得点数之和的功能依然是正确的,但是并没有让字符串变量发生变化。这是什么原因呢?
四、Java基础(三)
4. 方法参数的传递方式
出现上面问题的原因是方法参数的传递方式。调用方法时传入的参数是“按值传递”的方式传入的。方法头的参数列表中声明的变量实际是在方法内部声明的局部变量。调用参数时会将对应位置参数的值复制给内部变量,也就是将值进行传递。将上面方法被调用后值传递的过程用代码展开表示出来,就比较直观了。
int sumA = getSum(pokersA, 3, text);// 调用语句 // 方法内部实际展开代码 { int[] pokersA = pokersA_address; int i = 3; String text = text_value; ... }
在第三课的学习中我们知道基本数据类型是进行值的复制,引用类型复制的是地址(即引用),而字符串虽然是引用类型但是模拟了基本类型的方式。因此在方法getSum内部对变量i与text进行赋值,都不能改变传递值给它的原变量的值。但是因为pokersA是引用类型,如果在方法内部访问pokersA中元素,是可以改变元素的值的。这就给了我们一个思路。
五、改变输入参数值的正确方式
我们的思路就是将String字符串包装一下,让它变为数组。具体代码如下:
private int getSum(int[] pokersA, int i, String[] texts){ int sum = 0; for(int j = 0; j <= i; j++){ if( pokersA[j] == 0 ){ break; } sum += pokersA[j]; texts[0] += pokersA[j] + ","; } return sum;}
调用代码对应修改。第一调用修改如下:
// 获得当前玩家手中牌的总点数 String[] texts = {""}; int sum = getSum(pokersA, i, texts);
我们建立一个数组,里面只有一个元素,该元素被初始化为空字符串。第二次调用与第三次调用则改写如下:
// 显示玩家手里的牌及总数 String[] texts = {"你手中的牌分别是:"} ; int sumA = getSum(pokersA, 3, texts); texts[0] += "总数是:" + sumA + "。对家手里的牌是:"; // 显示电脑手里的牌及总数 int sumB = getSum(pokersB, 3, texts);
运行之后显示就正常了。
1. 复用性、可扩展性与运行效率
这样通过将重复的代码提取出来做成一个方法,我们大大减少了代码的长度,同时也使得算法逻辑更清晰。对getSum方法我们进行了3次调用,使得同样的代码获得了重复使用,提高了代码的“重用性”。重用性高是优质代码很重要的特征。有的同学可能提出别的思路来简化原来的代码。比如,其实我们可以把后两次循环进行合并,合并以后,只需增加2个变量,一个整数变量表示另一个数组的点数之和,一个字符串记录另一个数组牌局结算信息。这样原来的代码有3次循环,用方法改造的代码也是3次循环,而这种思路只需要2次循环。是不是还要更好?
答案是否定的。减少一次循环看起来提高了代码的运行效率,但是这种效率提高是微乎其微的,不是我们改进代码的努力方向。要知道代码写完以后不可能不改,需求的改变是随时可能发生的。“变是正常的,不变是不可能的”这是软件开发的常态。因此我们写代码的时候一定要为这个做好准备。如果用后一种思路改造的话,考虑这样的需求变化:如果增加几个玩家呢?大家就很容易看出提取出方法的代码适应性,只要再添几行对方法的调用就可以了。而增加变量的思路就会使得代码开始杂乱了。那么多的变量,很容易搞乱。因此,提取出方法的改造思路是扩展性比较好的。可扩展性强也是优质代码的特征。在实际开发中,复用性与可扩展性是相辅相承的,复用性高的代码其可扩展性往往比较强,要让代码可扩展性强常常也是要往提高复用性的思路上走。
那有的同学会问那运行效率是不是就不重要呢?不能说不重要,但是在实际编程中这不是考虑的重点。原因一来大部分程序员是在应用层面编程,需要考虑底层运行效率的机会本来就不多。二来软件开发的经验是,就算真是出现了效率问题,到时直接定位出问题的代码也比较容易,那时再进行优化就行。
2. 重构
我们在本次课程中提取一段代码做成方法的代码改进办法,用术语来说叫做“重构”,英文是Refactor。重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是提升代码复用性与可扩展性的重要办法,有许多种进行重构的操作,将代码提取出来成为方法,就是一种重构操作。有专业的书籍给大家介绍如何进行重构。我们在这里就不展开讲,只给大家介绍一下,IDE对重构的支持。主流的IDE现在都提供重构的功能,我们现在所用的Android Studio也不例外。前面我给大家演示过如何用重构来给变量重命名,接下来我再给同学们演示如何用IDE的功能来实现提取方法。具体步骤为:
在编辑器中选中想要提取方法的代码。然后在菜单上依次选择Refactor>Exatract>Method...。会弹出如下对话框:
我们可以看到对话框里,已经把方法名称都起好了,参数列表与返回值也选好了。这对话框里有一个概念要解释一下,所谓Signature,“签名”是指方法头中的方法名与参数列表的组合,签名中的各部分只要有一项不同,就可以认为是不同方法。在对话框中点击“Refactor”后,IDE就会自动生成方法的代码,同时也会在原代码位置改写成调用方法的代码。【是不是很爽?有好工具我们就得会用,而且只有多用,我们才能更深地理解重构的概念,把代码的质量不断提高。】
那后面两次循环的代码是否可以自动重构呢?我们可以试试。同样选择代码后点击同样的菜单项,这时弹出了一个提示框。
原来IDE检查到了这段代码有两个输出值,这就不能单纯用返回值来返回了。点击“OK”以后,会弹出一个新的窗口,引导我们创建一个内部类,也就是我们前面提到的第一种解决思路。这里就不继续了,感兴趣的同学可以自己试试。
我不推荐同学们写长篇代码,应该多用重构来优化代码结构。给大家提供一个经验:如果写的代码超过了两屏,你就该考虑用方法把其中的一部分包装一下移出去了。
六、让我们来洗牌吧
本次课我们学习了Java中方法的概念,掌握了一种很方便的代码组织方式。既然我们已经会用方法了,那我们就用方法来给我们的游戏增加一个新的能力,那就是“洗牌”。前面我们用随机数简单模拟了洗牌过程,但这个随机存在一个问题:它虽然给出了随机的数字,但并不能避免前后的数字相同,而在现实中在一副牌的情况下是不会出现重复的牌的。因此需要换一种新的算法。我们先对洗牌问题进行一个简单的分析。首先就是要确定数据类型,因为21点游戏不用大小猫,所以总共是52张,4种花色,每种花色13张牌。要表示这52张牌,很明显用整型数组是合适的,每个元素里放一张牌。但是应该用一维数组还二维数组呢?表面上看用二维数组好,因为每张牌有2个维度,花色与点数,那就是一个4×13的二维数组。但涉及到到实际使用的时候就会发现有很大问题,二维数组是该存什么数呢?那就只能存1~13的数字了,4组1~13的数字又如何彼此区分呢?这种造成了困难。因此不能使用二维数组,还是应该用一维数组,依次存入1~52的数,这样打乱之后就好区分了。那又如何获得花色与实际的点数呢?其实很简单,把拿到的牌的数字减去1后除13就是花色,减去1后模13再加1就是点数。再说如何打乱数字的算法。因为我们的课主要是讲如何使用语言与工具来实际编程,关于算法我们不做深入讨论。同学们可能听说过关于算法设计的一些术语,像冒泡算法、贪心算法等等。算法设计是计算机编程的重要内容,主要侧重于逻辑思维能力。而我们的课程主要侧重于实用编程,在于语言的表达和对计算机资源的使用,对算法这块我的态度是拿来主义:如果有需要的时候直接拿来用就行。感兴趣的同学可以自己找找资料,网上这样的资料是很多的。我这里直接采用一种高效的算法。这个算法的思路是:做一个从序号从51到0的降序for循环,每次循环都让随机数从0到比当前序号小1的数字中产生一个数,把当前序号位置的数组元素的值与随机产生的数字位置的元素值互换,这样循环完成就实现了数组元素值的打乱。这个算法的具体分析同学们可以看这篇文章:https://mp.weixin.qq.com/s/uYPnZ0MsQIT2_t3lk8ju1g。按这个算法实现的洗牌方法如下所示:
/** * 洗牌算法 * @param nums 需要洗牌的数组 * @return 洗过牌的数组 */public static int[] shuffle(int[] nums) { java.util.Random rnd = new java.util.Random(); for(int i = nums.length-1; i > 0; i-- ) { int j = rnd.nextInt(i+1); int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } return nums;}
这里有几点需要说明一下。
- 前面给大家介绍过注释的两种形式单行注释与多行注释,这里在方法声明之前的注释与多行注释类似,但是开头多了一个*号,这种注释称为Java文档注释。大家可以试着IDE中在方法前面敲一下/**再回车,就可以发现它自动生成了好几行,它把参数与返回值都@标明了,这样就不仅可以对方法的作用进行注释,还可以对其参数与返回值的作用进行注释。
- 这里用java.util.Random类的方式产生随机数,作用与前面用到的java.lang.Math.random方法的作用是一样的,但是方便一些。
- nums.length是调用的数组对象的一个属性,它的值是数组的长度,这个属于“类”的范畴,会在下一课讲到。
因为牌数值的范围从1~13变成了1~52,其包含的信息发生了变化,所以求和算法也要相应变化。修改后如下:
private int getSum(int[] pokers, String[] texts){ int sum = 0; for (int i = 0; i < pokers.length; i++){ if( pokers[i] == 0 ){ // 如果当前牌为0,说明已经没叫牌了 break; } int count = (pokers[i] - 1) % 13 + 1; // 获得真正点数 int color = (pokers[i] - 1) / 13; // 花色 sum += count; switch (color){ case 0:{ texts[0] += "黑桃" + count + ","; break; } case 1:{ texts[0] += "红桃" + count + ","; break; } case 2:{ texts[0] += "梅花" + count + ","; break; } case 3:{ texts[0] += "方块" + count + ","; break; } } } return sum;}
在上面的代码的方法声明里我们就没有再传入代表数组长度的整型变量i,因为长度可以通过数组的length属性来获得。内部通过对数组的除与模的操作获得牌的花色与实际点数信息,并通过swithc语句将花色信息翻译成文字。
两个方法准备好之后,我把调用代码也调整一下,修改后如下:
int[] pokers = new int[52]; // 一副牌组成的数组 for(int i = 0; i < 52; i++){ pokers[i] = i + 1; // 初始化牌数组,序号从0~51,点数从1到52 } pokers = shuffle(pokers); // 调用洗牌方法 int[] pokersA = new int[4]; // 玩家手里的牌 int[] pokersB = {7, 8, 0, 0}; // 电脑手里的牌 for(int i = 0; i < 4; i++){ pokersA[i] = pokers[i]; // 依次从洗好的牌中取牌 // 获得当前玩家手中牌的总点数 String[] texts = {""}; int sum = getSum(pokersA, texts); // 在玩家手中牌点数大于电脑时停止叫牌 if(sum > 15){ break; } } // 显示玩家手里的牌及总数 String[] texts = {"你手中的牌分别是:"} ; int sumA = getSum(pokersA, texts); texts[0] += "总数是:" + sumA + "。对家手里的牌是:"; // 显示电脑手里的牌及总数 int sumB = getSum(pokersB, texts); texts[0] += "总数是:" + sumB; if(sumA > sumB && sumA <= 21){ // 赢的要求是玩家牌比电脑大并且不大于21点 texts[0] += "。你赢了!"; }else{ texts[0] += "。你输了!"; } TextView txtResult = (TextView)findViewById(R.id.txtResult); txtResult.setText(texts[0]);
点击运行以后显示结果如下:
从上面可以看到我们程序显示的牌的信息丰富了,出现了花色信息。多运行几次也会发现洗牌算法也工作正常,每次拿到的牌都会不一样。那是不是要把12这样的数字变成Q?因为这样才是实际牌面的信息。这个不着急,我们后面会直接用图片来显示。这样我们离一个完整的Android游戏越来越近了。
同学们,这次的课程我们从整理代码,避免重复的目的出发,讲解了方法的相关知识,这里进行一下总结:
- 方法的声明,参数列表、返回值,以及方法签名的概念。
- 局部变量、作用域、代码块等概念。
- 简单地介绍了递归概念。
- 方法中输出多个变量的需求引出通过引用改变调用者变量的解决办法。
- 介绍了重构的概念,以及编程中对复用性、可扩展性的追求。
- 用方法实现洗牌算法并调整代码。
我们再把已经接触到的Java语言的内容进行一个回顾。我们已经学过了数据的声明与类型(基本类型、引用类型和null)、运算符与关键词、3大语句(赋值、选择、循环)、数据结构(数组)和方法等。其中数据的声明与类型、数据结构是用于表达数和数的组织方式,其它的则是表达运算的逻辑。关于语言的内容我们只剩下类还没有讲到,下次课我们将讲类,同学们自己可以先找找这方面的资料自学一下。