《编写高质量代码-改善java程序的151个建议》
--秦小波
一、开发中通用的方法和准则
1、不要在常量和变量中出现易混淆的字母
long a=0l; --> long a=0L;
2、莫让常量蜕变成变量
static final int t=new Random().nextInt();
3、三元操作符的类型无比一致
int i=80;
String s=String.valueOf(i<100?90:100);
String s1=String.valueOf(i<100?90:100.0);
System.out.print(s.equals(s1)); //false
编译器会进行类型转换,将90转为90.0。有一定的原则,细节不表
4、避免带有变长参数的方法重载
public class MainTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(PriceTool.calPrice(12, 1)); // 1
}
}
class PriceTool {
public static int calPrice(int price, int discount) {
return 1;
}
public static int calPrice(int price, int... discount) {
return 2;
}
}
编译器会从最简单的开始猜想,只要符合编译条件的即采用
5、别让null值和空值威胁到变长方法
其中client.methodA("china")和client.methodA("china",null) 是编译不通过的,因为编译器不知道选择哪个方法
6、覆写变长方法也循规蹈矩
重写是正确的,因为父类的calprice编译成字节码后的形参是一个int类型的形参加上一个int数组类型的形参,子类的参数列表也是如此。
sub.fun(100,50) 编译失败,方法参数是数组,java要求严格类型匹配
7、警惕自增的陷阱
输出:0
步骤1:jvm把count(此时是0)值拷贝到临时变量区
步骤2:count值加1,这时候count的值是1
步骤3:返回临时变量区的值,0
步骤4:返回值赋值给count,此时count值被重置为0
8、少用静态导入
java5开始引入 import static ,其目的是为了减少字符输入量,提高代码的可阅读性。
举个例子:
滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让阅读者很难弄清楚所谓何意。
举个糟糕的例子:
对于静态导入,一定要遵循两个规则:
1)、不使用*通配符,除非是导入静态常量类(只包含常量的类或接口)
2)、方法名是具有明确、清晰表象意义的工具类
9、不要在本类中覆盖静态导入的变量和方法
本地的方法和属性会被使用。因为编译器有最短路径原则,以确保本类中的属性、方法优先
10、养成良好习惯,显示声明UID
11、避免用序列化类在构造函数中为不变量赋值
序列化1.0
序列化2.0
此时反序列化,name:混世魔王
因为饭序列化时构造函数不会执行。jvm从数据流中获取一个object对象,然后根据数据流中的类文件描述信息查看,发现时final变量,需要重新计算,于是引用person类中的name值,而辞职jvm又发现name竟然没有赋值,不能引用,于是不再初始化,保持原值状态。
12、避免为final变量复杂赋值
反序列化时final变量在以下情况不会被重新赋值
1)通过构造函数为final变量赋值
2)通过方法返回值为final变量赋值
3)final修饰的属性不是基本类型
原理:
保存在磁盘(网络传输)的对象文件包括两部分
1)类描述信息
包括路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。与class文件不同的是,它不记录方法、构造函数、statis变量等的具体实现。
2)非瞬态(transient关键字)和非静态实例变量值
这里的值如果是一个基本类型,就保存下来;如果是复杂对象,就连该对象和关联类信息一起保存,并且持续递归下去,其实还是基本数据类型的保存。
也正是因为这两点,一个持久化后的对象文件会比一个class类文件大很多
13、使用序列化类的私有方法巧妙解决部分属性持久化问题
举个例子
一个服务像另一个服务屏蔽类A的一个属性x
classA{inta;intb;intx;
}
可能有几种解决方案
1)在属性x前加上transient关键字(失去了分布式部署的能力?todo)
2)新增业务对象类A1,去掉x属性(符合开闭原则,而且对原系统没有侵入型,但是增加代码冗余,且增加了工作量)
classA{inta;intb;intx;
}
3)请求端过滤。获得A对象以后,过滤掉x属性。(方案可行但不合规矩,自己服务的安全性需要外部服务承担,不符合设计规范)
理想的解决方案:
用Serializable接口的两个私有方法 writeObject和readObject,控制序列化和反序列化的过程
序列化回调:
java调用objectOutputStream类把一个对象转换成流数据时,会通过反射检查被序列化的类是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法。
14、switch-case-break 不要忽略break
15、易变业务使用脚本语言编写
java世界一直在遭受异种语言的入侵,比如php,ruby,groovy,js等。这种入侵者都有一个共同特征:脚本语言,他们都在运行期解释执行。为什么java这种强编译型语言会需要这些脚本语言呢?那是因为脚本语言的三大特性:
1)灵活。脚本语言一般都是动态类型,可以不用声明变量类型而直接使用,也在可以在运行期改变类型
2)便捷。脚本语言是一种解释性语言,不需要编译成二进制代码,也不需要像java一样生成字节码。它的执行是依靠解释器解释的,因此在运行期变更带啊吗非常容易,而且不用停止应用
3)简单。
脚本语言的这些特性是java所缺少的,引入脚本语言可以使java更强大,于是java6开始正式支持脚本语言。但是因为脚本语言比较多,java的开发者也很难确定该支持哪种语言,于是jcp提出了jsr223规范,只要符合该规范的语言都可以在java平台上运行(默认支持js)
16、慎用动态编译(热部署)
动态编译一直是java的梦想,从java6版本开始支持动态编译,可以在运行期直接编译.java文件,执行.class等,只要符合java规范都可以在运行期动态家在。
在使用动态编译时,需要注意以下几点:
1)在框架中谨慎使用
比如在Spring中,写一个动态类,要让它动态注入到spring容器中,这是需要花费老大功夫的
2)不要在要求高性能的项目使用
动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果在工具类项目中它则可以很好的发挥其优越性,比如在idea中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试,非常方便。
3)考虑安全问题
如果你在web界面上提供了一个功能,允许上传一个java文件然后运行,那就等于说“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意java程序就可以让你所有的安全工作毁于一旦。
4)记录动态编译过程
建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序
17、避免instanceof非预期结果
instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,两侧操作符需要有继承或实现关系。
1)‘A’ instanceof Character :编译不通过 ‘A’ 是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断。
2)null instanceof String:编译通过,返回false。这是instanceof特有的规则:若左操作符是null,结果直接返回false
3)(String)null instanceof String :编译通过,返回false。null是一个万用类型,也可以说是没类型,即使做类型转换还是个null
4)new Date()instanceof String:编译不通过,date类和string没有继承或实现关系
5)new GenericClass().isDateInstance("") :编译通过,返回false。T是string,与date之间没有继承或实现关系,是因为java的泛型是为编码服务的,在编译成字节码时,T已经是object类型了。传递的实参是string类型,也就是说T的表面类型是object,实际类型是string,这句话等价于object instance of date ,所以返回false。
18、断言绝对不是鸡肋
在防御式编程中经常会用断言对参数和环境做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常,断言在很多语言中都存在,c、c++、python都有不同的断言表达形式。在java中断言的使用是assert关键字,如下
assert :
在布尔表达式为假时,抛出AssertionError错误,并附带错误信息
两个特性
1)assert默认是不开启的
2)AssertionError是继承自Error的。这是错误,不可恢复
不可使用断言的情况:
1)在对外公开的方法中
2)在执行逻辑代码的情况下。因为生产环境是不开启断言的。避免因为环境的不同产生不同的业务逻辑
建议使用断言的情况:
1)在私有方法中,私有方法的使用者是自己,可以更好的预防自己犯错
2)流程控制中不可能到达的区域。如果到达则抛异常
3)建立程序探针。我们可能会在一段程序中定义两个变量,分别代码两个不同的业务含义,但是两者有固定的关系。例如 var1=var2*2,那我们就可以在程序中到处设‘桩’,断言这两者的关系,如果不满足即表明程序已经出现了异常,业务也就没有必要运行下去了
19、不能只替换一个类
举个例子:
如果在一个运行中项目,直接替换constans.class ,其中 maxage改为180。client中的输入依然是150
原因是
对于final修饰的基本类型和string类型,编译器会认为它是稳定态,所以在编译时就直接把值编译到字节码中了,避免了在运行期引用,以提高代码的执行效率。
对于final修饰的类,编译器认为它是不稳定态,在编译时建立的则是引用关系(soft final),如果client类引入的常量是一个类或实例,即使不重新编译也会输出最新值
基本数据类型相关
21、用偶判断,不用奇判断
i%2==1?奇数:偶数
这个逻辑是不对的,当i为负数时计算错误。因为取余的计算逻辑为
int remainder(int a,intb){return a-a/b*b;
}
22、用整数类型处理货币
在计算机中浮点数有可能是不准确的,它只能无限接近准确值,而不能完全精确。这是由于浮点数的存储规则决定的(略过)。
举个例子:system.out.print(10.00-9.06) :0.4000000000000036
有两种解决方案:
1)BigDecimal
BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDeciaml是最优的解决方案。
2)使用整型
把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍。
23、不要让类型默默转换
举个例子:
太阳逛照射到地球上需要8分钟,计算太阳到地球的距离。
long result=light_speed * 60 * 8;
输出的结果是 -202888064
原因:java是先运算然后再进行类型转换的,三者相乘,超过了int的最大值,所以其值是负值(溢出是负值的原因看一下)
正确的处理是 long result=light_speed * 60L * 8;
24、数字边界问题
举个例子:
if(order+base
当order+base足够大时,超过了int的最大值,其值是负值,所以业务逻辑会有问题
25、四舍五入问题
math.round(-10.5) 输出 -10 这是math。round采用的舍入规则所决定的(采用的是正无穷方向舍入规则)
以上算法对于一个5000w存款的银行来说,一年将损失10w。一个美国银行家发现了此问题并提出了一个修正算法,叫做银行家舍入的近似算法(规则不记录了)。java5可以直接用RoundingMode类提供的Round模式。与BigDecimal绝配。RoundingMode支持7种舍入模式:
远离零方向舍入、趋向零方向舍入、向正无穷方向舍入、向负无穷方向舍入、最近数字舍入、银行家算法
原文:https://www.cnblogs.com/amei0/p/10140120.html