一、分析
Java引入包装类型(Wrapper Types)是为了解决基本类型的实例化问题,以便一个基本类型也能参与到面向对象的编程世界中。
而在Java5中泛型更是对基本类型说了“不”,如果想把一个整形放到List中,就必须使用Integer包装类型。
基本类型和包装类型都是可以通过自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)自由转换的。如,整型的拆箱过程是通过调用包装类型的inValue方法来实现,如果包装类型是null,访问其intValue方法报空指针异常也就在所难免了。
二、场景
代码如下:
- public static int f(List<Integer> list){
- int count = 0;
- for(int i : list){
- //隐含了自动拆箱操作
- count += i;
- }
- }
- public static void main(String[] args){
- List<Integer> list = new ArrayList<Integer>();
- list.add(1);
- list.add(2);
- list.add(null);
- System.out.println(f(list));
- }
运行失败,结果:
Exception in thread "main" java.lang.NullPointerException
我们稍微思考一下,在for循环过程中,隐含了一个拆箱过程,将包装类型转换成基本类型,调用intValue方法。由于存在null值,故调其intValue方法出现空指针异常。
解决办法:在自动拆箱过程中,剔除null值的情况,该情况不转换成基本类型。
- public static int f(List<Integer> list){
- int count = 0;
- for(Integer i : list){
- count += (i != null) ? i : 0;
- }
- return null;
- }
三、建议
我们禁忌一点:包装类型参与运算时,要做null值校验
一、分析
在许多数学计算的场景中,会用到近似取值的计算方法。常用的近似取值有四舍五入。
但是在某些金融行业或特殊场景中,四舍五入的方式就不太适宜。目前Java支持一下其中舍入方式:
-
ROUND_UP:远离零方向舍入,向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0即进位。
-
ROUND_DOWN:趋向零方向的舍入,向0方向靠拢,也就是说,向绝对值最小的方向输入。注意,所有的位都舍弃不存在进位的情况。
-
ROUND_CEILING:向正无穷方向舍入,向正最大方向靠拢,如果是正数,舍入行为类似于ROUND_UP;如果是负数,则舍入行为类似于ROUND_DOWN。注意,Math.round方法使用的即此模式。
-
ROUND_FLOOR:向负无穷方向舍入,向负无穷方向靠拢,如果是正数,则舍入行为类似于ROUND_DOWN;如果是负数,则舍入行为类似于ROUND_UP。
-
HALF_UP:最近数字舍入(5进),这就是我们最经典的四舍五入模式。
-
HALF_DOWN:最近数字舍入(5舍),在四舍五入中,5是进位的,而在HALF_DOWN中却是舍弃不进位。
-
HALF_EVEN:银行家算法,四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
二、场景
使用Math.round来指定精度的整数或小数:
- public class Client{
- public static void main(String[] args){
- System.out.println("10.5的近似值:" + Math.round(10.5));
- System.out.println("-10.5的近似值:" + Math.round(-10.5));
- }
- }
由于Math.round采用的舍入规则是正无穷方向舍入。所以输出结果为:
10.5近似值:11
-10.5近似值:-10
但是在银行计算利息的场景下,适应四舍五入计算利息就会出现如下情况:
按照概率计算克制,被舍入的数字均匀的分布在0到9之间,下面以10笔存款利息计算作为模型,以银行家的身份来思考这个算法:
-
四舍:舍弃的数值:0.000、0.001、0.002、0.003、0.004,因为是舍弃的,对于银行家来说,就不用付给储户利息了,那每舍弃一个数字就会赚取相应的金额:0.000、0.001、0.002、0.003、0.004。
-
五入:进位的数值:0.005、0.006、0.007、0.008、0.009,因为是进位,对于银行家来说,每进一位就会多付款给储户,也就是亏损了,那亏损的部分就是对应的10进制补数:0.005、0.004、0.003、0.002、0.001。
因为舍弃和进位的数字在0到9之间是均匀分布的,所以对于银行家来说,每10笔存款利息因采用四舍五入而获得的盈利是:
0.000+0.001+0.002+0.003+0.004-0.005-0.004-0.003-0.002-0.001=-0.005。
也就是说没10笔利息的计算中,就损失了0.005元,每笔利息计算损失0.0005元。对于一家银行(上亿储户)来说,这个误差造成的损失也不可小视觉。
这个误差是由美国的银行家发现的,所以提供出了一个修正算法,叫做银行家舍入的近似算法,其规则如下:
-
舍去的位数小于5时,直接舍去;
-
舍去的位数大于等于6时,进位后舍去;
-
当舍去的数值等于5时,分两种情况:
-
5后面还有其它数字(非0),则进位后舍去;
-
若5后面是0(即5是最后一个数字),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。
-
以上规则汇总成一句话:四舍六入考虑五,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
在Java5以上的版本中,使用银行家的舍入算法非常简单,直接使用RoundingMode类,提供Round模式即可,示例代码如下:
- public class Client{
- public static void main(String[] args){
- //存款
- BigDecimal d = new BigDecimal(88888);
- //月利率,乘3计算季利率
- BigDecimal r = new BigDecimal(0.001875*3);
- //计算利息
- BigDecimal i = d.mutiply(r)setScale(2,RoundingMode.HALF_EVEN);
- System.out.println("季利息是:" + i);
- }
- } <span style="background-color: transparent; color: windowtext; font-size: 11pt; line-height: 17px; font-family: Calibri, sans-serif;"> </span>
三、建议
根据不同的场景,慎重选择不同的舍入模式,以提高项目的精准度,减少算法损失。在大量与货币数字交互的项目中,一定要选择好近似的计算模式,尽量减少因算法不同而造成的损失。
一、分析
在单元测试中,有一项测试叫做边界测试(也有叫做临界测试),它能避免出现:数字越界使检验条件失效。
如果一个方法接受的是Int类型的参数,那一些三个值是必须的:0、正最大、负最大、其中正最大和负最大是边界值,如果这三个值都没有问题,方法才是比较安全可靠的。
二、场景
某生产的电子产品非常畅销,需要提前30天预订才能抢到手,同时它还规定了一个会员可拥有的最多产品数量,方式囤积压货肆意加价。
代码如果如下:
- public class Clinet{
- //一个会员拥有的最多数量
- pubilc final static int LIMIT = 2000;
- public static void main(String[] args){
- //会员当前拥有的产品数量
- int cur = 1000;
- Scanner input = new Scanner(System.in);
- System.out.println("请输入需要预订的数量:");
- while(input.hasNextInt()){
- int order = input.nextInt();
- //当前拥有的与准备订购的产品的数量之和
- if(order > 0 && order + cur <= LIMIT){
- System.out.println("你已经成功预订的" + order + "个产品");
- }else{
- System.out.println("超限额,预订失败!");
- }
- }
- }
- }
业务逻辑非常简单,但是经过自动化测试两小时后,出现了异常的结果:
请输入要需要预订的数量:2147483647
你已经成功预订的2147483647个产品!
看着2147483647这个数字很眼熟,它是int类型的最大值。当如数order是2147483647时,order+cur就超越了int的范围,结果是-2147482649,orfer+cur<2000与order>0的条件就成立了。故由于:数字越界使检验条件失效。
三、建议
在测试过程中,必须要注意临界测试的重要性。避免越界导致的问题,保证程序的的安全可靠。
一、分析
在Java运算中的类型转换,是先运算在进行类型转换的。具体场景如下。
二、场景
在如下程序中:
- public class Client{
- public static final int LIGHT_SPEED = 30 * 10000 * 1000;
- public static void main(String[] args){
- System.out.println("月亮照射到地球需要1秒,计算月亮到地球的距离。");
- long dis1 = LIGHT_SPEED * 1;
- System.out.println("月亮与地球的距离是:" + dis1 + "米");
- System.out.println("----------------------------------------------------");
- System.out.println("太阳照射到地球上需要8分钟,计算太阳到地球的距离。");
- //可能超出整数范围,使用long型
- long dis2 = LIGHT_SPEED * 60 * 8;
- System.out.println("太阳与地球的距离是:" + dis2 + "米");
- }
- }
运行结果是:
月亮照射到地球需要1秒,计算月亮到地球的距离。
月亮与地球的距离是:300000000米
----------------------------------------------------
太阳照射到地球上需要8分钟,计算太阳到地球的距离
太阳与地球的距离是:-2028888064米
太阳和地球的距离竟然是负的!那是因为Java是运算然后再进行类型的转换,具体地说是因为dis2的三个运算参数都是int类型,三者相乘的结果虽然也是int类型,但是已经超过了int的最大值,所以其值就是负值了,在转换成long型,结果是负值。
解决办法,只要加个L即可,如下:
long dis2 = LIGHT_SPEED * 60 L * 8;
60L是long型,乘出来的结果也是long型,在还没有超过int类型的范围时就已经转换成long型了。
三、建议
在实际开发中,基本类型的转换时,通用的做法是主动声明式类型转换(注意不是强制类型转换)。代码如下:
long dis2 = 1L * LIGHT_SPEED * 60 * 8;
一、分析
当你使用三元运算符,两边的操作数的类型不一致的时候,这就涉及到三元操作符的转换规则:
1.若果两个操作数不可转换,则不做转换,返回值为Object类型。
2.若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换。int类型转换为long类型,long类型转换成float类型。
3.若两个操作数中有一个是数字S,另外一个是表达式,且其类型为T,那么,若数字S在T的范围内,则转换为T类型;若S超过了T的范围,则T转换为S类型。
4.若两个操作数字都是直接数字。则返回值类型为范围较大者。
二、场景
- public class Client{
- public static void main(String[] args){
- int i = 80;
- String s = String.valueOf(i < 100? 90 : 100);
- String s1 = String.valueOf(i < 100? 90 : 100.0);
- System.out.println("两者是否相等:" + s.equals(s1)):
- }
- }
分析,两个三元操运算,条件都为真,返回第一个值,结果“两者相等:true”。结果果真如此吗?结果“两者相等:false”!
问题出在于100和100.0这两个数字上:
在变量s中,第一个操作数(90)和第二个操作数(100)都是int类型,类型相同,返回的是int型的90;
在变量s1中,第一个操作数类型为(90)int类型,第二个操作数是(100.0)浮点型。
可是三元操作符必须返回同一个数据,而且类型要确定,不可能条件为真返回int类型,条件为假返回float类型,编译器是不会允许的,所以进行类型转换了。int转换成90.0,也就是所返回值是90.0当然和90不相等了。
三、建议
保证三元操作符中的两个操作类型一致,即可减少错误的发生。