编写高质量代码:改善Java程序的151个建议(7)

  第2章 基本类型

  不积跬步,无以至千里;

  不积小流,无以成江海。

  —荀子《劝学篇》

  Java中的基本数据类型(Primitive Data Types)有8个:byte、char、short、int、long、float、double、boolean,它们是Java最基本的单元,我们的每一段程序中都有它们的身影,但我们对如此熟悉的“伙伴”又了解多少呢?

  积少成多,积土成山,本章我们就来一探这最基本的8个数据类型。

  建议21:用偶判断,不用奇判断

  判断一个数是奇数还是偶数是小学里学的基本知识,能够被2整除的整数是偶数,不能被2整除的是奇数,这规则简单又明了,还有什么好考虑的?好,我们来看一个例子,代码如下:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ //接收键盘输入参数 Scannerinput=newScanner(System.in); System.out.print(“请输入多个数字判断奇偶:”); while(input.hasNextInt()){ inti=input.nextInt(); Stringstr=i+”->”+(i%2==1?”奇数”:”偶数”); System.out.println(str); } } }

  输入多个数字,然后判断每个数字的奇偶性,不能被2整除就是奇数,其他的都是偶数,完全是根据奇偶数的定义编写的程序,我们来看看打印的结果:

  

    请输入多个数字判断奇偶:120-1-2 1->奇数 2->偶数 0->偶数 -1->偶数 -2->偶数

  前三个还很靠谱,第四个参数﹣1怎么可能会是偶数呢,这Java也太差劲了,如此简单的计算也会错!别忙着下结论,我们先来了解一下Java中的取余(%标示符)算法,模拟代码如下:

  

    //模拟取余计算,dividend被除数,divisor除数 publicstaticintremainder(intdividend,intdivisor){ returndividend-dividend/divisor*divisor; }

  看到这段程序,相信大家都会心地笑了,原来Java是这么处理取余计算的呀。根据上面的模拟取余可知,当输入-1的时候,运算结果是-1,当然不等于1了,所以它就被判定为偶数了,也就是说是我们的判断失误了。问题明白了,修正也很简单,改为判断是否是偶数即可,代码如下:

  i%2==0?”偶数”:”奇数”

  注意:对于基础知识,我们应该“知其然,并知其所以然”。

  建议22:用整数类型处理货币

  在日常生活中,最容易接触到的小数就是货币,比如你付给售货员10元钱购买一个9.60元的零食,售货员应该找你0.4元也就是4毛钱才对,我们来看下面的程序:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ System.out.println(10.00-9.60); } }

  我们期望的结果是0.4,也应该是这个数字,但是打印出来的却是0.40000000000000036,这是为什么呢?

  这是因为在计算机中浮点数有可能(注意是可能)是不准确的,它只能无限接近准确值,而不能完全精确。为什么会如此呢?这是由浮点数的存储规则所决定的,我们先来看0.4这个十进制小数如何转换成二进制小数,使用“乘2取整,顺序排列”法(不懂?这就没招了,太基础了),我们发现0.4不能使用二进制准确的表示,在二进制数世界里它是一个无限循环的小数,也就是说,“展示”都不能“展示”,更别说是在内存中存储了(浮点数的存储包括三部分:符号位、指数位、尾数,具体不再介绍),可以这样理解,在十进制的世界里没有办法准确表示1/3,那在二进制世界里当然也无法准确表示1/5(如果二进制也有分数的话倒是可以表示),在二进制的世界里1/5是一个无限循环小数。

  各位要说了,那我对结果取整不就对了吗?代码如下:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ NumberFormatf=newDecimalFormat(“#.##”); System.out.println(f.format(10.00-9.60)); } }

  打印出结果是0.4,看似解决了,但是隐藏了一个很深的问题。我们来思考一下金融行业的计算方法,会计系统一般记录小数点后的4位小数,但是在汇总、展现、报表中,则只记录小数点后的2位小数,如果使用浮点数来计算货币,想想看,在大批量的加减乘除后结果会有多大的差距(其中还涉及后面会讲到的四舍五入问题)!会计系统要的就是准确,但是却因为计算机的缘故不准确了,那真是罪过。要解决此问题有两种方法:

  (1)使用BigDecimal

  BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。

  (2)使用整型

  把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单、准确,一般在非金融行业(如零售行业)应用较多。此方法还会用于某些零售POS机,它们的输入和输出全部是整数,那运算就更简单。

  建议23:不要让类型默默转换

  我们出一个小学生的题目给大家做做看,光速是每秒30万公里,根据光线旅行的时间,计算月亮与地球、太阳与地球之间的距离。代码如下:

  

    publicclassClient{ //光速是30万公里/秒,常量 publicstaticfinalintLIGHT_SPEED=30*10000*1000; publicstaticvoidmain(String[]args){ System.out.println(“题目1:月亮光照射到地球需要1秒,计算月亮和地球的距离。”); longdis1=LIGHT_SPEED*1; System.out.println(“月亮与地球的距离是:”+dis1+”米”); System.out.println(“——————————————–“); System.out.println(“题目2:太阳光照射到地球上需要8分钟,计算太阳到地球的距离。”); //可能要超出整数范围,使用long型 longdis2=LIGHT_SPEED*60*8; System.out.println(“太阳与地球的距离是:”+dis2+”米”); } }

  估计你要鄙视了,这种小学生乘法计算有什么可做的。不错,确实就是一个乘法运算,我们运行一下看看结果:

  

    题目1:月亮光照射到地球需要1秒,计算月亮和地球的距离。 月亮与地球的距离是:300000000米 ——————————————– 题目2:太阳光照射到地球上需要8分钟,计算太阳到地球的距离。 太阳与地球的距离是:-2028888064米

  太阳和地球的距离竟然是负的,诡异。dis2不是已经考虑到int类型可能越界的问题,并使用了long型吗,为什么还会出现负值呢?

  那是因为Java是先运算然后再进行类型转换的,具体地说就是因为disc2的三个运算参数都是int类型,三者相乘的结果虽然也是int类型,但是已经超过了int的最大值,所以其值就是负值了(为什么是负值?因为过界了就会从头开始),再转换成long型,结果还是负值。

  问题知道了,解决起来也很简单,只要加个小小的“L”即可,代码如下:

  longdis2=LIGHT_SPEED*60L*8;

  60L是一个长整型,乘出来的结果也是一个长整型(此乃Java的基本转换规则,向数据范围大的方向转换,也就是加宽类型),在还没有超过int类型的范围时就已经转换为long型了,彻底解决了越界问题。在实际开发中,更通用的做法是主动声明式类型转化(注意不是强制类型转换),代码如下:

  longdis2=1L*LIGHT_SPEED*60*8;

  既然期望的结果是long型,那就让第一个参与运算的参数也是long型(1L)吧,也就是明说“嗨,我已经是长整型了,你们都跟着我一起转为长整型吧”。

  注意:基本类型转换时,使用主动声明方式减少不必要的Bug。

  建议24:边界,边界,还是边界

  某商家生产的电子产品非常畅销,需要提前30天预订才能抢到手,同时它还规定了一个会员可拥有的最多产品数量,目的是防止囤积压货肆意加价。会员的预定过程是这样的:先登录官方网站,选择产品型号,然后设置需要预订的数量,提交,符合规则即提示下单成功,不符合规则提示下单失败。后台的处理逻辑模拟如下:

  

    publicclassClient{ //一个会员拥有产品的最多数量 publicfinalstaticintLIMIT=2000; publicstaticvoidmain(String[]args){ //会员当前拥有的产品数量 intcur=1000; Scannerinput=newScanner(System.in); System.out.print(“请输入需要预定的数量:”); while(input.hasNextInt()){ intorder=input.nextInt(); //当前拥有的与准备订购的产品数量之和 if(order>0&&order+cur<=LIMIT){ System.out.println(“你已经成功预定的”+order+”个产品!”); }else{ System.out.println(“超过限额,预订失败!”); } } } }

  这是一个简易的订单处理程序,其中cur代表的是会员已经拥有的产品数量,LIMIT是一个会员最多拥有的产品数量(现实中这两个参数当然是从数据库中获得的,不过这里是一个模拟程序),如果当前预订数量与拥有数量之和超过了最大数量,则预订失败,否则下单成功。业务逻辑很简单,同时在Web界面上对订单数量做了严格的校验,比如不能是负值、不能超过最大数量等,但是人算不如天算,运行不到两小时数据库中就出现了异常数据:某会员拥有产品的数量与预订数量之和远远大于限额。怎么会这样?程序逻辑上不可能有问题呀,这是如何产生的呢?我们来模拟一下,第一次输入:

  

    请输入需要预定的数量:800 你已经成功预定的800个产品!

  这完全满足条件,没有任何问题,继续输入:

  

    请输入需要预定的数量:2147483647 你已经成功预定的2147483647个产品!

  看到没,这个数字远远超过了2000的限额,但是竟然预订成功了,真是神奇!

  看着2147483647这个数字很眼熟?那就对了,它是int类型的最大值,没错,有人输入了一个最大值,使校验条件失效了,Why?我们来看程序,order的值是2147483647,那再加上1000就超出int的范围了,其结果是-2147482649,那当然是小于正数2000了!一句话可归结其原因:数字越界使检验条件失效。

  在单元测试中,有一项测试叫做边界测试(也有叫做临界测试),如果一个方法接收的是int类型的参数,那以下三个值是必测的:0、正最大、负最小,其中正最大和负最小是边界值,如果这三个值都没有问题,方法才是比较安全可靠的。我们的例子就是因为缺少边界测试,致使生产系统产生了严重的偏差。

  也许你要疑惑了,Web界面既然已经做了严格的校验,为什么还能输入2147483647这么大的数字呢?是否说明Web校验不严格?错了,不是这样的,Web校验都是在页面上通过JavaScript实现的,只能限制普通用户(这里的普通用户是指不懂HTML、不懂HTTP、不懂Java的简单使用者),而对于高手,这些校验基本上就是摆设,HTTP是明文传输的,将其拦截几次,分析一下数据结构,然后再写一个模拟器,一切前端校验就都成了浮云!想往后台提交个什么数据那还不是信手拈来?!

  相关链接:

  编写高质量代码:改善Java程序的151个建议(1)

  编写高质量代码:改善Java程序的151个建议(2)

  编写高质量代码:改善Java程序的151个建议(3)

  编写高质量代码:改善Java程序的151个建议(4)

  编写高质量代码:改善Java程序的151个建议(5)

  编写高质量代码:改善Java程序的151个建议(6)

关于爱情的句子:情不知所起,一往而情深。

编写高质量代码:改善Java程序的151个建议(7)

相关文章:

你感兴趣的文章:

标签云: