编写高质量代码:改善Java程序的151个建议 --[0~25]
警惕自增的陷阱
public class Client7 {
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自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量+1,最后返回临时变量区的值。程序第一次循环处理步骤如下:
JVM把count的值(其值是0)拷贝到临时变量区;
count的值+1,这时候count的值是1;
返回临时变量区的值,注意这个值是0,没修改过;
返回值赋给count,此时count的值被重置为0.
如下理解:
public static int mockAdd(int count) {
// 先保存初始值
int temp = count;
// 做自增操作
count = count + 1;
// 返回原始值
return temp;
}
不要在本类中覆盖静态导入的变量和方法
编译器有一个"最短路径"原则:如果能够在本类中查找到相关的变量、常量、方法、就不会去其它包或父类、接口中查找,以确保本类中的属性、方法优先。
因此,如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖.
显示声明serialVersionUID可以避免对象的不一致,但尽量不要以这种方式向JVM撒谎。
避免用序列化类在构造函数中为不变量赋值
反序列化时构造函数不会执行
反序列化时final变量在以下情况下不会被重新赋值:
- 通过构造函数为final变量赋值
- 通过方法返回值为final变量赋值
- final修饰的属性不是基本类型
使用序列化类的私有方法巧妙解决部分属性持久化问题
public class Person implements Serializable {
private static final long serialVersionUID = 9146176880143026279L;
private String name;
private transient Salary salary;
public Person(String _name, Salary _salary) {
this.name = _name;
this.salary = _salary;
}
//序列化委托方法
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeInt(salary.getBasePay());
}
//反序列化委托方法
private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException {
input.defaultReadObject();
salary = new Salary(input.readInt(), 0);
}
}
switch 语句里面break不可少
慎用动态编译
- 在框架中谨慎使用:比如要在struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它注入到Spring容器中,这是需要花费老大功夫的。
- 不要在要求性能高的项目中使用:如果你在web界面上提供了一个功能,允许上传一个java文件然后运行,那就等于说:"我的机器没有密码,大家都可以看看",这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
- 记录动态编译过程:建议记录源文件,目标文件,编译过程,执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行时很不让人放心的,留下这些依据可以很好地优化程序。
断言绝对不是鸡肋
什么情况下能够使用assert呢?一句话:按照正常的执行逻辑不可能到达的代码区域可以防止assert。具体分为三种情况:
在私有方法中放置assert作为输入参数的校验:在私有方法中可以放置assert校验输入参数,因为私有方法的使用者是作者自己,私有的方法的调用者和被调用者是一种契约关系,或者说没有契约关系,期间的约束是靠作者自己控制的,因此加上assert可以更好地预防自己犯错,或者无意的程序犯错。
流程控制中不可能到达的区域:这类似于Junit的fail方法,其标志性的意义就是,程序执行到这里就是错误的
public void doSomething() {
int i = 7;
while (i > 7) {
/* 业务处理 */
}
assert false : "到达这里就表示错误";
}
不要只替换一个类
对于final修饰的基本类型和String类型,编译器会认为它是稳定态的(Immutable Status)所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-time Reference),以提高代码的执行效率。对于我们的例子来说,Client类在编译时字节码中就写上了"150",这个常量,而不是一个地址引用,因此无论你后续怎么修改常量类,只要不重新编译Client类,输出还是照旧。
对于final修饰的类(即非基本类型),编译器会认为它不是稳定态的(Mutable Status),编译时建立的则是引用关系(该类型也叫作Soft Final)。如果Client类引入的常量是一个类或实例,及时不重新编译也会输出最新值。
发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是万全之策。
用偶判断,不用奇判断
i % 2 == 0 ? "偶数" : "奇数";
用整数类型处理货币
(1)、使用BigDecimal
BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。
(2)、使用整型
把参与运算的值扩大100倍,并转为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单,准确,一般在非金融行业(如零售行业)应用较多。此方法还会用于某些零售POS机,他们输入和输出的全部是整数,那运算就更简单了.
基本类型转换时,使用主动声明方式减少不必要的Bug.
long dis2 = LIGHT_SPEED * 60L * 8;
60L是一个长整型,乘出来的结果也是一个长整型的(此乃Java的基本转换规则,向数据范围大的方向转换,也就是加宽类型),在还没有超过int类型的范围时就已经转换为long型了,彻底解决了越界问题。在实际开发中,更通用的做法是主动声明类型转化(注意,不是强制类型转换)代码如下:
long dis2 = 1L * LIGHT_SPEED * 60L * 8
既然期望的结果是long型,那就让第一个参与的参数也是Long(1L)吧,也就说明"嗨"我已经是长整型了,你们都跟着我一块转为长整型吧。
边界还是边界
int long 这些类型的最大值,最小值的边界处理
银行家舍入(Banker's Round)的近似算法,其规则如下:
舍去位的数值小于5时,直接舍去;
舍去位的数值大于等于6时,进位后舍去;
当舍去位的数值等于5时,分两种情况:5后面还有其它数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Client25 {
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);
}
}
ROUND_UP:原理零方向舍入。向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0即进位。
ROUND_DOWN:趋向0方向舍入。向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:银行家算法