编写java程序151条建议读书笔记(3)

来源:互联网 发布:淘宝蚂蚁花呗还款 编辑:程序博客网 时间:2024/06/10 04:34

建议18:避免instanceof非预期结果

instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类的实现,其操作类似于>=、==,

import java.util.Date;public class test {public static void main(String[] args) {// String对象是否是Object的实例 trueboolean b1 = "String" instanceof Object;// String对象是否是String的实例 trueboolean b2 = new String() instanceof String;// Object对象是否是String的实例 falseboolean b3 = new Object() instanceof String;// 拆箱类型是否是装箱类型的实例 编译不通过boolean b4 = 'A' instanceof Character;// 空对象是否是String的实例 falseboolean b5 = null instanceof String;// 转换后的空对象是否是String的实例 falseboolean b6 = (String) null instanceof String;// Date是否是String的实例 编译不通过boolean b7 = new Date() instanceof String;// 在泛型类型中判断String对象是否是Date的实例 falseboolean b8 = new GenericClass<String>().isDateInstance("");}}class GenericClass<T> {// 判断是否是Date类型public boolean isDateInstance(T t) {return t instanceof Date;}}
1."String" instanceof Object:返回值是true,"String"是一个字符串,字符串又继承了Object,那当然返回true了。 
2.new String() instanceof String:返回值是true,没有任何问题,一个类的对象当然是它的实例了。 
3.new Object() instanceof String:返回值为false,Object是父类,其对象当然不是String类的实例了。要注意的是,这句话其实完全可以编译通过,只要instanceof关键字的左右两个操作数有继承或实现关系,就可以编译通过。 
4.'A' instanceof Character:这句话编译不通过,为什么呢?因为'A'是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断,不能用于基本类型的判断。 
5.null instanceof String:返回值为false,这是instanceof特有的规则,若做操作数为null,结果就直接返回false,不再运算右操作数是什么类。这对我们的程序非常有利,在使用instanceof操作符时,不用关心被判断的类(也就是左操作数)是否为null,这与我们经常用到的equals、toString方法不同。
 6.(String) null instanceof String:返回值为false,不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,也就是说它可以没类型,即使做类型转换还是个null。 
7.new Date() instanceof String:编译不通过,因为Date类和String没有继承或实现关系,所以在编译时就直接报错了,instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。 

8.new GenericClass<String>().isDateInstance(""):编译不通过,非也,编译通过了,返回值为false,T是个String类型,于Date之间没有继承或实现关系,为什么"t instanceof Date"会编译通过呢?那是因为Java的泛型是为编码服务的,在编译成字节码时,T已经是Object类型了传递的实参是String类型,也就是说T的表面类型是Object,实际类型是String,那么"t instanceof Date"等价于"Object instanceof Date"了,所以返回false就很正常了。

建议19:断言绝对不是鸡肋

在防御式编程中经常会用断言(Assertion)对参数和环境做出判断,避免程序因不当的判断或输入错误而产生逻辑异常

在Java中断言使用的是assert关键字,其基本用法如下:assert<布尔表达式>
assert<布尔表达式> : <错误信息>,在布尔表达式为假时,跑出AssertionError错误,并附带了错误信息。assert的语法比较简单,有以下两个特性:(1)assert默认是不启用的,断言是为调试程序服务的,目的是为了能够迅速、方便地检查到程序异常,但Java在默认条件下是不启用的,要启用就要在编译、运行时加上相关的关键字,这就不多说,有需要的话可以参考一下Java规范。(2)assert跑出的异常AssertionError是继承自Error的,断言失败后,JVM会抛出一个AssertionError的错误,它继承自Error,这是一个错误,不可恢复,也就是表明这是一个严重问题,开发者必须予以关注并解决之。assert虽然是做断言的,但不能将其等价于if...else...这样的条件判断,它在以下两种情况下不可使用:(1)在对外的公开方法中
防御式编程最核心的一点就是:所有的外部因素(输入参数、环境变量、上下文)都是"邪恶"的,都存在着企图摧毁程序的罪恶本源,为了抵制它,我们要在程序处处检验。满地设卡,不满足条件,就不执行后续程序,以保护后续程序的正确性,处处设卡没问题,但就是不能用断言做输入校验,特别是公开方法。看一个例子:

public class test {public static void main(String[] args) {System.out.println(StringUtils.encode(null));;}}class StringUtils{public static String encode(String str){assert    str != null : "加密的字符串为null";/*加密处理*/return str;}}
encode方法对输入参数做了不为空的假设,如果为空则抛出AssertionError错误,但这段程序存在一个严重的问题,encode是一个public方法,这标志着它是对外公开的任何一个类只要能传递一个String类型的参数就可以调用,但是test类按照规定调用encode方法,却获得了一个AssertionError错误信息,是谁破坏了契约协议?是encode方法自己。(2)在执行逻辑代码的情况下,assert的支持是可选的,在开发时可以让他运行,但在生产环境中系统则不需要其运行了(以便提高性能),因此在assert的布尔表达式中不能执行逻辑代码,否则会因为环境的不同而产生不同的逻辑。

按照正常的执行逻辑不可能到达的代码区域可以放置assert。具体分为三种情况:
1)在私有方法中放置assert作为输入参数的校验:在私有方法中可以放置assert校验输入参数,因为私有方法的使用者是作者自己,私有的方法的调用者和被调用者是一种契约关系,或者说没有契约关系,期间的约束是靠作者自己控制的,因此加上assert可以更好地预防自己犯错,或者无意的程序犯错。
2)流程控制中不可能到达的区域:这类似于Junit的fail方法,其标志性的意义就是,程序执行到这里就是错误的。
3)建立程序探针:我们可能会在一段程序中定义两个变量,分别代两个不同的业务含义但是两者有固定的关系,例如:var1=var2 * 2,那我们就可以在程序中到处设"桩"了,断言这两者的关系,如果不满足即表明程序已经出现了异常,业务也就没有必要运行下去了。

建议20:不要只替换一个类

我们经常在系统中定义一个常量接口(或常量类),以囊括系统中所涉及的常量,从而简化代码,方便开发,在很多的开源项目中已经采用了类似的方法,比如在struts2中,org.apache.struts2.StrutsConstants就是一个常量类,它定义Struts框架中与配置有关的常量,而org.apache.struts2.StrutsConstants则是一个常量接口,其中定义了OGNL访问的关键字。代码是在"智能型"IDE工具中完成的是这些输出结果都不会也逻辑上错误,但是用记事本编写代码,修改常量结果中并没有改变,因为对于final修饰的基本类型和String类型,编译器会认为它是稳定态的(Immutable Status)所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-time Reference),以提高代码的执行效率。对于final修饰的类(即非基本类型),编译器会认为它不是稳定态的(Mutable Status),编译时建立的则是引用关系(该类型也叫作Soft Final)。

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

判断一个数是奇数还是偶数是小学里的基本知识,能够被2整除的整数是偶数,不能被2整除的数是奇数

public class test {    public static void main(String[] args) {        // 接收键盘输入参数        Scanner input = new Scanner(System.in);        System.out.println("输入多个数字判断奇偶:");        while (input.hasNextInt()) {            int i = input.nextInt();            String str = i + "-->" + (i % 2 == 1 ? "奇数" : "偶数");            System.out.println(str);        }    }    //取余计算模拟    public static int remainder(int dividend, int divisor) {        return dividend - dividend / divisor * divisor;    }}
输入多个数字判断奇偶:1  2 -2     1-->奇数    2-->偶数     -2-->偶数,由取余计算当输入-1的时候,运算结果为-1,修正也很简单,改为判断是否是偶数即可。i % 2 == 0 ? "偶数" : "奇数"。

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

System.out.println(10.00-9.60);简单的输出语句,你期望是0.4,但是结果为0.40000000000000036。因为在计算机中浮点数有可能是不准确的它只能无限接近准确值,而不能完全精确。这是由浮点数的存储规则所决定的,0.4这个十进制小数如何转换成二进制小数,使用"乘2取整顺序排列"法,我们发现0.4不能使用二进制准确的表示,在二进制数世界里它是一个无限循环的小数,也就是说,展示都不能展示,更别说在内存中存储了(浮点数的存储包括三部分:符号位、指数位、尾数),可以这样理解,在十进制的世界里没有办法唯一准确表示1/3,那么在二进制的世界里当然也无法准确表示1/5(如果二进制也有分数的话倒是可以表示),在二进制的世界里1/5是一个无限循环的小数。解决办法有{NumberFormat f = new DecimalFormat("#.##");  System.out.println(f.format(10.00-9.60));}强制转换,但是存在精度隐患。要解决此问题有两种方法:
(1)使用BigDecimal专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。
(2)使用整型把参与运算的值扩大100倍,并转为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单,准确,一般在非金融行业(如零售行业)应用较多。此方法还会用于某些零售POS机,他们输入和输出的全部是整数,那运算就更简单了。

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

public class test {  public static final int LIGHT_SPEED = 30 * 10000 * 1000;public static void main(String[] args) {long dis1 = LIGHT_SPEED * 1;System.out.println( dis1);long dis2 = LIGHT_SPEED * 60 * 8;System.out.println( dis2);}}
输出结果为300000000和-2028888064。因为Java是先运算然后进行类型转换的,具体的说就是因为dis2的三个运算参数都是int型,三者相乘的结果虽然也是int型,但是已经超过了int的最大值,所以其值就是负值了,再转换为long型,结果还是负值。解决也很简单只要加个小小的L即可:long dis2 = LIGHT_SPEED * 60L * 8;
60L是一个长整型,乘出来的结果也是一个长整型的,在还没有超过int类型的范围时就已经转换为long型了,彻底解决了越界问题。在实际开发中,更通用的做法是主动声明类型转化(注意,不是强制类型转换)代码如下:
long dis2 = 1L * LIGHT_SPEED * 60L * 8。

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

建议24:边界还是边界

数字越界使校验条件失效。

public class test {  public final static int LIMIT = 2000;public static void main(String[] args) {int cur = 1000;Scanner input = new Scanner(System.in);while (input.hasNextInt()) {int order = input.nextInt();if (order > 0 && order + cur <= LIMIT) {System.out.println("你已经成功预定:" + order + " 个产品");} else {System.out.println("超过限额,预定失败!");}}}}
order的值是2147483647那再加上1000就超出int的范围了,其结果是-2147482649,那当然是小于正数2000了!一句归其原因:数字越界使校验条件失效。在单元测试中,有一项测试叫做边界测试(也叫临界测试),如果一个方法接收的是int类型的参数,那么以下三个值是必须测试的:0、正最大、负最小,其中正最大、负最小是边界值,如果这三个值都没有问题,方法才是比较安全可靠的。我们的例子就是因为缺少边界测试,致使生产系统产生了严重的偏差。

建议25:不要让四舍五入亏了一方

四舍五入。四舍五入是一种近似精确的计算方法,在Java5之前,我们一般是通过Math.round来获得指定精度的整数或小数的10.5近似值: 11       -10.5近似值: -10绝对值相同的两个数字,近似值为什么就不同了呢?这是由Math.round采用的舍入规则决定的(采用的是正无穷方向舍入规则),我们知道四舍五入是有误差的:其误差值是舍入的一半。这种四舍五入对于银行利息的计算会对银行大为不利,有人提出银行家算法:1)舍去位的数值小于5时,直接舍去;2)舍去位的数值大于等于6时,进位后舍去;3)当舍去位的数值等于5时,分两种情况:5后面还有其它数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。以上规则汇总成一句话:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。我们举例说明,取2位精度;round(10.5551)  =  10.56   round(10.555)  =  10.56   round(10.545)  =  10.54 

public class test { 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支持以下七种舍入方式:
1:ROUND_UP:原理零方向舍入。向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0即进位。
2:ROUND_DOWN:趋向0方向舍入。向0方向靠拢,也就是说,向绝对值最小的方向输入,注意:所有的位都舍弃,不存在进位情况。
3:ROUND_CEILING:向正无穷方向舍入。向正最大方向靠拢,如果是正数,舍入行为类似于ROUND_UP;如果为负数,则舍入行为类似于ROUND_DOWN.注意:Math.round方法使用的即为此模式。
4:ROUND_FLOOR:向负无穷方向舍入。向负无穷方向靠拢,如果是正数,则舍入行为类似ROUND_DOWN,如果是负数,舍入行为类似以ROUND_UP。
5:HALF_UP:最近数字舍入(5舍),这就是我们经典的四舍五入。
6:HALF_DOWN:最近数字舍入(5舍)。在四舍五入中,5是进位的,在HALF_DOWN中却是舍弃不进位。
7:HALF_EVEN:银行家算法
在普通的项目中舍入模式不会有太多影响,可以直接使用Math.round方法,但在大量与货币数字交互的项目中,一定要选择好近似的计算模式,尽量减少因算法不同而造成的损失。
注:根据不同的场景,慎重选择不同的舍入模式,以提高项目的精准度,减少算法损失。



0 0