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()的返回值,这就证明每次调用种子都不同,所以无参构造时产生的随机数都不同。
若非必要,不要设置随机数种子