每天10个编码坑(《编写高质量代码 改善Java程序的151个建议》)

NO.21 用偶判断,不用奇判断

判断一个数是奇数还是偶数:能够被2整除的是偶数,不能被2整除的是奇数

public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入多个整数判断奇偶:");
        while (scanner.hasNextInt()) {
            int i = scanner.nextInt();
            String str = i + "->" + (i % 2 == 1 ? "奇数" : "偶数");
            System.out.println(str);
        }
    }

测试结果:
在这里插入图片描述
前三个结果是正确的,但是-1显然是奇数才对,但是这段代码算出来的是偶数。原因如下:
在这里插入图片描述
这是一段模拟Java中的%(取余符号)的算法的代码,当输入-1时,结果是-1,那么肯定不会等于1,所以程序的结果是没错的,错的是不可以使用奇数去做奇偶性的判断。
应该修改为:i % 2 == 0 ? “偶数” : “奇数”
在这里插入图片描述

NO.22 用整数类型处理货币

在这里插入图片描述
这段程序结果如下:
在这里插入图片描述
我们期望的结果是0.4,但是结果却是0.40000000000000036,这是因为在计算机中浮点数有可能是不准确的,他只能无限的接近准确值,而不能完全精确,这主要是和小数的存储方式有关的,在计算机中的存储都是二进制,如果想存储0.4,就要先把0.4转为二进制,那么小数的十进制转化为二进制是采用“乘2取整,顺序排列的方式”,那么会发现0.4想转化为二进制是无限循环的小数,所以计算出来的结果只能是无限接近而不能准确展示。

public static void main(String[] args) {
        System.out.println(10.00 - 9.60);
        DecimalFormat decimalFormat = new DecimalFormat("#.##");
        System.out.println(decimalFormat.format(10.00-9.60));
    }

以上代码可以通过取整对结果进行改造。但是这样的方式是不准确的,比如在金融行业的货币计算,会计系统一般会记录到小数点后四位,但是在汇总、展现、报表中,则只记录小数点后两位,如果用浮点数来计算货币,在大批量的加减乘除运算之后出来的结果差距会有多大,当然,对于货币计算也有对应的解决方案:
(1)使用BigDecimal
BigDecimal是专门为弥补浮点数计算不准确的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal字段映射的时候,BigDecimal是最优的解决方案。
(2)使用整型
把参与计算的值扩大100倍,并转为整型,然后再展现时缩小100倍。

NO.23 不要让类型默默转换

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分钟,计算太阳到地球的距离。");
        //可能超过int范围,使用long类型接收
        long dis2 = LIGHT_SPEED * 60 * 8;
        System.out.println("太阳到地球的距离:" + dis2 + "米");

    }
}

运行结果如下:
在这里插入图片描述
可以看到太阳到地球的距离成为了负数,但是我们已经使用了long类型去接收为什么还不能正确接收?
是因为Java是先运算然后再进行类型转换的,具体来讲就是dis2的三个参数都是int类型,那么计算结果也是int,但是结果超过了int的最大取值,那显示出来就是负数,再转换成long,也还是负数。
解决办法:
long dis2 = LIGHT_SPEED * 60L * 8

基本类型转化时,使用主动声明方式减少不必要的bug

NO.24 边界,边界,还是边界

public class Client {

    public static final int LIMIT = 2000;

    public static void main(String[] args) {
        int current = 1000;
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextInt()) {
            System.out.print("输入要预定的产品数量:");
            int order = scanner.nextInt();
            if (order > 0 && order + current <= LIMIT) {
                System.out.println("已成功预订" + order + "个产品");
            } else {
                System.out.println("超过限额,预定失败");
            }
        }
    }
}

结果如下:
在这里插入图片描述
在这里插入图片描述
2147483647是int的最大值,已经远远超过限定额2000,但是依然预定成功,原因是2147483647加上1000已经超过了int的最大值,那么他的结果就是负数,负数肯定是小于2000的,数字越界使校验条件失效

在单元测试中有一项测试叫做边界测试,如果一个方法接收的是int类型的参数,那么以下三个值是必测的:0、正最大、负最小,只有这三个值的结果都没有问题,方法才是安全可靠的。其中正最大、负最小就是边界值。

NO.25 不要让四舍五入亏了一方

	public static void main(String[] args) {
        System.out.println("10.5近似值:"+Math.round(10.5));
        System.out.println("-10.5近似值:"+Math.round(-10.5));
    }

在Java5之前使用Math.round来获得指定精度的整数或者小数。但是这种方式是不适用于银行的计算的,根据计算所得:使用这种方式每10笔银行利息的计算中就损失0.005元,对于一家有5千万客户的银行来说,每年仅仅因为四舍五入的误差而损失的金额就有100000元,这个算法误差是由美国银行家发现的,并且对此提出了一个修正算法,叫做银行家舍入的近似算法,规则如下:
(1)舍去位的数值小于5时,直接舍去
(2)舍去位的数值大于等于6时,进位后舍去
(3)当舍去位的值为5时,分两种情况:一种是5后面还有其他的数值(非0),则进位后舍去;另一种是5后面是0(5是最后一个数值),则根据5前一位的奇偶性来判断是否需要进位,奇数进位后舍去,偶数不进直接舍去

在Java5以上的版本使用这个舍入法则就更加简单,直接使用RoundingMode类提供的Round模式就可以:

	public static void main(String[] args) {
        //存款
        BigDecimal d = new BigDecimal(888888);
        //月利率,乘3是季利率
        BigDecimal r = new BigDecimal(0.001875 * 3);
        //计算利息
        BigDecimal i = d.multiply(r).setScale(2, RoundingMode.HALF_EVEN);
        System.out.println("季利息:" + i);
    }

使用了BigDecimal类,并且使用setScale()设置了精度,同时传递了
RoundingMode.HALF_EVEN表示使用银行家舍入法则进行近似计算,BigDecimal和RoundingMode是绝配,想使用什么舍入模式就可以使用RoundingMode设置。目前Java支持的7种舍入方式:

  • UP:向远离零的方向舍入。

     若舍入位为非零,则对舍入部分的前一位数字加1;若舍入位为零,则直接舍弃。即为向外取整模式。
    
  • DOWN:向接近零的方向舍入。

      不论舍入位是否为零,都直接舍弃。即为向内取整模式。
    
  • CEILING:向正无穷大的方向舍入。

      若 BigDecimal 为正,则舍入行为与 ROUND_UP 相同;若为负,则舍入行为与 ROUND_DOWN 相同。即为向上取整模式。
    
  • FLOOR:向负无穷大的方向舍入。

      若 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同;若为负,则舍入行为与 ROUND_UP 相同。即为向下取整模式。
    
  • HALF_UP:向“最接近的”整数舍入。

      若舍入位大于等于5,则对舍入部分的前一位数字加1;若舍入位小于5,则直接舍弃。即为四舍五入模式。
    
  • HALF_DOWN:向“最接近的”整数舍入。

      若舍入位大于5,则对舍入部分的前一位数字加1;若舍入位小于等于5,则直接舍弃。即为五舍六入模式。
    
  • HALF_EVEN:向“最接近的”整数舍入。

      若(舍入位大于5)或者(舍入位等于5且前一位为奇数),则对舍入部分的前一位数字加1;
    
      若(舍入位小于5)或者(舍入位等于5且前一位为偶数),则直接舍弃。即为银行家舍入模式。
    
  • UNNECESSARY

      断言请求的操作具有精确的结果,因此不需要舍入。
    
      如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。
    

总结:根据不同的场景,慎重选择不同的舍入模式,以提高项目的精确度,减少算法损失

NO.26 提防包装类型的null值

	public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(null);
        System.out.println(f(list));
    }

    public static int f(List<Integer> list) {
        int count = 0;
        for (int i : list) {
            count += i;
        }
        return count;
    }

结果如下:
在这里插入图片描述
基本类型和包装类型都是可以通过自动装箱和自动拆箱自动转换的,但是这里的null并未自动转成Integer的默认值0,报了空指针异常。
在for循环中隐藏了一个拆箱过程,包装类型转化为了基本类型,拆箱过程是需要调用包装对象的intValue()方法实现的,但是包装对象是null,那自然就报了空指针。

包装类型参与运算时,要做null值的校验

NO.27 谨慎包装类型的大小比较

	public static void main(String[] args) {
        Integer i = new Integer(100);
        Integer j = new Integer(100);
        compare(i, j);
    }

    public static void compare(Integer i, Integer j) {
        System.out.println(i == j);
        System.out.println(i < j);
        System.out.println(i > j);
    }

结果如下:
在这里插入图片描述
在Java中“==”是判断两个操作数是否有相等关系的,如果是基本类型就是判断值是否相等,如果是对象类型就是判断引用是否相等,这里明显是两个对象,所以不可能相等。
“<”和“>”是用来判断两个数字的大小关系,对于Integer包装类型,是用的intValue()的返回值进行比较的。
可以使用包装类型的compareTo()方法。

NO.28 优先使用整型池

	public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextInt()) {
            int ii = scanner.nextInt();
            System.out.println("======" + ii + "的相等判断");
            //比较两个通过new产生的对象
            Integer i = new Integer(ii);
            Integer j = new Integer(ii);
            System.out.println(i == j);

            //比较两个基本类型转化的包装类型
            i = ii;
            j = ii;
            System.out.println(i == j);

            //比较通过静态方法产生的实例
            i = Integer.valueOf(ii);
            j = Integer.valueOf(ii);
            System.out.println(i == j);
        }
    }

结果如下:
在这里插入图片描述
大于127的数字结果都不是一个对象。
(1)new产生的Integer对象
new声明的都是一个新对象,地址肯定不会相等
(2)装箱生成的对象
装箱是通过valueOf()实现的,所以后面两个是一样的,
可以看valueOf的源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

这里有一个判断,在low(-128)和high(127)之间的可以直接返回,这是因为这个区间的可以直接从整型池取数据,所以不管输入多少次127,获得的对象都是相同的,那么地址也都是相等的。但是如果是在这个区间之外的,都是通过new产生的新对象。

在判断对象是否相等时,最好使用equals方法,不要使用" == "
通过包装类的valueOf生成包装实例可以显著提高空间和时间性能

NO.29 优先选择基本类型

	public static void main(String[] args) {
        Client client = new Client();
        int i = 140;
        client.f(i);
        client.f(Integer.valueOf(i));
    }

    private void f(long i) {
        System.out.println("基本类型参数的方法被调用");
    }
    private void f(Long i) {
        System.out.println("基本类型参数的方法被调用");
    }

这段代码编译通过且结果如下:
在这里插入图片描述
对于client.f(i)来讲,编译器会把i的类型自动加宽,并将其转换为long类型,这是基本类型的转换规则。
那么client.f(Integer.valueOf(i))执行的依然也是基本类型的方法,是因为自动装箱有一个重要的原则:基本类型可以先加宽,再转变成宽类型的包装类型,但是不能直接转换成款类型的包装类型,具体来讲就是int要先加宽为long,然后才能转变成Long。

整个f(Integer.valueOf(i))的执行过程如下:

  • i通过valueOf()包装成Integer对象
  • 由于没有f(Integer i)方法,编译器聪明的将Integer转成int
  • int自动加宽为long,编译结束

重申:基本类型优先考虑

NO.30 不要随便设置随机种子

在Java中通常是使用Math.random和Random类来获取随机数的。

	public static void main(String[] args) {
        Random random = new Random(1000);
        for (int i = 0; i < 4; i++) {
            System.out.println("第" + i + "次:" + random.nextInt());
        }
    }

当直接使用new Random()时,每次打印获取的随机数都是不同的,但是如果使用了Random的有参构造,同一台机器不管执行多少次都是同一批随机数,以下是我打印三次的随机数(三次打印都是这四个随机数):
在这里插入图片描述
这是因为产生随机数的种子被固定了,在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从一下两个规则:

  • 种子不同产生不同的随机数
  • 种子相同,即使实例不同也会产生相同的随机数
    Random类的无参构造使用的种子是System.nanoTime()的返回值,这就证明每次调用种子都不同,所以无参构造时产生的随机数都不同。

若非必要,不要设置随机数种子

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值