改善Java程序的151建议_改善java程序的151个建议

《编写高质量代码-改善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值和空值威胁到变长方法

54eb377e0005f343426d2941c5ee567f.png

其中client.methodA("china")和client.methodA("china",null) 是编译不通过的,因为编译器不知道选择哪个方法

6、覆写变长方法也循规蹈矩

cfef4df2f6290ab8fe2e605a06b6354b.png

ac2bbba182ed167666f172b646797f0b.png

重写是正确的,因为父类的calprice编译成字节码后的形参是一个int类型的形参加上一个int数组类型的形参,子类的参数列表也是如此。

sub.fun(100,50) 编译失败,方法参数是数组,java要求严格类型匹配

7、警惕自增的陷阱

980b093716c5b86934755db3f76ffcf5.png

输出:0

步骤1:jvm把count(此时是0)值拷贝到临时变量区

步骤2:count值加1,这时候count的值是1

步骤3:返回临时变量区的值,0

步骤4:返回值赋值给count,此时count值被重置为0

8、少用静态导入

java5开始引入 import static ,其目的是为了减少字符输入量,提高代码的可阅读性。

举个例子:

6325cd6638e416aef8816d81682ad278.png

f60b440f3c9876a9c3b6fb5abd698540.png

滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让阅读者很难弄清楚所谓何意。

举个糟糕的例子:

3fbbf5af52429daff1c38cc446e46f2c.png

对于静态导入,一定要遵循两个规则:

1)、不使用*通配符,除非是导入静态常量类(只包含常量的类或接口)

2)、方法名是具有明确、清晰表象意义的工具类

9、不要在本类中覆盖静态导入的变量和方法

本地的方法和属性会被使用。因为编译器有最短路径原则,以确保本类中的属性、方法优先

10、养成良好习惯,显示声明UID

11、避免用序列化类在构造函数中为不变量赋值

66b435507d2d776698bc4e978f6d661a.png

序列化1.0

6efbdf5102a1700179f848a2bb7ae6b8.png

序列化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,控制序列化和反序列化的过程

54e270dda67163aab958167643b1dd9d.png

序列化回调:

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、不能只替换一个类

举个例子:

4ff9d4e05992552819773774cb3bfdd8.png

如果在一个运行中项目,直接替换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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值