改善Java程序的151个建议--记录(持续更新)

原书下载地址:http://download.csdn.net/detail/hua245942641/9272199

建议7: 警惕自增的陷阱

自增有两种形式,分别是i++和++i,i++ 表示的是先赋值后加1,++i 是先加1后赋值。而对于下面的代码:

public class Client {
    public static void main(String[] args) {
        int count =0;
        for(int i=0;i<10;i++){
            count=count++;
        }
        System.out.println("count="+count);
    }
}

这个程序输出的count 等于几?是count 自加10 次吗?答案等于10 ?可以非常肯定地告诉你,答案错误!运行结果是count 等于0。为什么呢?
count++ 是一个表达式,是有返回值的,它的返回值就是count 自加前的值,Java 对自加是这样处理的:首先把count 的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count 变量加1,最后返回临时变量区的值。程序第一次循环时的详细处理步骤如下:
步骤1 JVM 把count 值(其值是0)拷贝到临时变量区。
步骤2 count 值加1,这时候count 的值是1。
步骤3 返回临时变量区的值,注意这个值是0,没修改过。
步骤4 返回值赋值给count,此时count 值被重置成0。
“count=count++”这条语句可以按照如下代码来理解:

public static int mockAdd(int count){
    // 先保存初始值
    int temp =count;
    // 做自增操作
    count = count+1;
    // 返回原始值
    return temp;
}

于是第一次循环后count 的值还是0,其他9 次的循环也是一样的,最终你会发现count的值始终没有改变,仍然保持着最初的状态。

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

instanceof 是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,其操作类似于>=、==,非常简单,我们来看段程序,代码如下:

public class Client {
    public static void main(String[] args) {
        //String 对象是否是Object 的实例
        boolean b1 = "Sting" instanceof Object;
        //String 对象是否是String 的实例
        boolean b2 = new String() instanceof String;
        //Object 对象是否是String 的实例
        boolean b3 = new Object() instanceof String;
        // 拆箱类型是否是装箱类型的实例
        boolean b4 = 'A' instanceof Character;
        // 空对象是否是String 的实例
        boolean b5 = null instanceof String;
        // 类型转换后的空对象是否是String 的实例
        boolean b6 = (String)null instanceof String;
        //Date 对象是否是String 的实例
        boolean b7 = new Date() instanceof String;
        // 在泛型类中判断String 对象是否是Date 的实例
        boolean b8 = new GenericClass<String>().isDateInstance("");
    }
}
class GenericClass<T>{
        // 判断是否是Date 类型
    public boolean isDateInstance(T t){
        return t instanceof Date;
    }
}

就这么一段程序,instanceof 的所有应用场景都出现了,同时问题也产生了:这段程序中哪些语句会编译通不过?我们一个一个地来解说。

1、”Sting” 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().isDateInstance(“”)
编译通不过?非也,编译通过了,返回值是false,T 是个String 类型,与Date之间没有继承或实现关系,为什么”t instanceof Date” 会编译通过呢?那是因为Java 的泛型是为编码服务的,在编译成字节码时,T 已经是Object 类型了,传递的实参是String 类型,也就是说T 的表面类型是Object,实际类型是String,那”t instanceof Date” 这句话就等价于Object instance of Date” 了,所以返回false 就很正常了。

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

这个建议主要是针对被final修饰的constant常量。
对于final 修饰的基本类型和String 类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-timeReference),以提高代码的执行效率。类在编译时,字节码中就写上了这个常量,而不是一个地址引用,因此无论你后续怎么修改常量类,只要不重新编译Client 类,输出还是照旧。
而对于final 修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫做Soft Final),如果Client 类引入的常量是一个类或实例,即使不重新编译也会输出最新值。

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

这个建议主要是因为在做奇偶性判断时,一般会用取余的方式来进行。
如果要判断n的奇偶性,最好使用n%2==0 来做判断依据,而不要使用n%2 == 1来进行。因为在java中的取余操作的模拟算法如下:

// 模拟取余计算,dividend 被除数,divisor 除数
public static int remainder(int dividend,int divisor){
    return dividend - dividend / divisor * divisor;
}

当输入-1 的时候,运算结果是-1,当然不等于1 了,所以它就被判定为偶数了。因此最好使用n%2==0 来做判断依据

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

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

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

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

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

int LIGHT_SPEED = 30 * 10000 * 1000;
long dis2 = LIGHT_SPEED * 60 * 8;

Java 是先运算然后再进行类型转换的,具体地说就是因为disc2 的三个运算参数都是int 类型,三者相乘的结果虽然也是int 类型,但是已经超过了int 的最大值,所以其值就是负值了(为什么是负值?因为过界了就会从头开始),再转换成long 型,结果还是负值。
解决起来也很简单,只要加个小小的“L” 即可,代码如下:

long dis2 = LIGHT_SPEED * 60L * 8;

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

四舍五入,小于5 的数字被舍去,大于等于5 的数字进位后舍去,由于所有位上的数字都是自然计算出来的,按照概率计算可知,被舍入的数字均匀分布在0 到9 之间,下面以10 笔存款利息计算作为模型,以银行家的身份来思考这个算法。
1、四舍:舍弃的数值:0.000、0.001、0.002、0.003、0.004,因为是舍弃的,对银行家来说,就不用付款给储户了,那每舍弃一个数字就会赚取相应的金额:0.000、0.001、0.002、0.003、0.004。
2、五入:进位的数值: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 元。

这个算法误差是由美国银行家发现的,并且对此提出了一个修正算法,叫做银行家舍入(Banker’s 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

目前Java 支持以下七种舍入方式:
1、ROUND_UP: 远离零方向舍入。
向远离0 的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0 即进位。
2、ROUND_DOWN:趋向零方向舍入。
向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 :银行家算法。

建议26: 提防包装类型的null 值

Java 引入包装类型(Wrapper Types)是为了解决基本类型的实例化问题,以便让一个基本类型也能参与到面向对象的编程世界中。
基本类型和包装类型都是可以通过自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)自由转换的。而需要注意的是在自动拆箱的过程中,如果是对象为null,那么就会抛出NullPointException
我们谨记一点:包装类型参与运算时,要做null值校验。

建议27: 谨慎包装类型的大小比较

基本类型是可以比较大小的,其所对应的包装类型都实现了Comparable 接口也说明了此问题,那我们来比较一下两个包装类型的大小,代码如下:

public class Client {
    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);
    }
}

代码很简单,产生了两个Integer 对象,然后比较两者的大小关系,既然基本类型和包装类型是可以自由转换的,那上面的代码是不是就可打印出两个相等的值呢?让事实说话,运行结果如下:

false
false
false

竟然是3 个false,也就是说两个值之间不等,也没大小关系,这也太奇怪了吧。不奇怪,我们来一一解释。
1、i == j
在Java 中“==”是用来判断两个操作数是否有相等关系的,如果是基本类型则判断值是否相等,如果是对象则判断是否是一个对象的两个引用,也就是地址是否相等,这里很明显是两个对象,两个地址,不可能相等。
2、i > j 和 i < j
在Java 中,“>”和“<”用来判断两个数字类型的大小关系,注意只能是数字型的判断,对于Integer 包装类型,是根据其intValue() 方法的返回值(也就是其相应的基本类型)进行比较的(其他包装类型是根据相应的value 值来比较的,如doubleValue、floatValue 等),那很显然,两者不可能有大小关系的。
总的来说,在这个问题中,i==j比较的是地址,肯定不相等,而> 或者 < 比较的是两个对象的xxxValue的返回值,是相等的,所以结果反倒是false了。

建议28: 优先使用整型池

java在Integer这个对象中存在一组从-128到127的整数池。
要实例出一个Integer对象,可以采用new和valueOf方法这两种方式:
1、new Integer(i),这样生成的Integer对象,每次地址都是不一样的。
2、使用Integer.valueOf(i)生成的对象,如果i的范围在-128到127之间,那么不管调用多少次这个方法得到的都是同一个对象,也就是说用==来比较是相等的。这是因为Integer.valueOf方法导致的。方法如下:

static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
    for(int i = 0; i < cache.length; i++)
    cache[i] = new Integer(i - 128);
}

public static Integer valueOf(int i) {
    final int offset = 128;
    if (i >= -128 && i <= 127) { // must cache
        return IntegerCache.cache[i + offset];
    }
    return new Integer(i);
}

cache 是IntegerCache 内部类的一个静态数组,容纳的是- 128 到127 之间的Integer 对象。通过valueOf 产生包装对象时,如果int 参数在- 128 和127 之间,则直接从整型池中获得对象,不在该范围的int 类型则通过new 生成包装对象。
整型池的存在不仅仅提高了系统性能,同时也节约了内存空间,这也是我们使用整型池的原因,也就是在声明包装对象的时候使用valueOf 生成,而不是通过构造函数来生成的原因。顺便提醒大家,在判断对象是否相等的时候,最好是用equals 方法,避免用“==”产生非预期结果。

发布了24 篇原创文章 · 获赞 5 · 访问量 5万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章