持续地优化代码,提高代码的质量,是提升系统生命力的有效手段之一。软件系统思维有句话“Less coding, more thinking(少编码、多思考)”,也有这么一句俚语“Think more, code less(思考越多,编码越少)”。所以,我们在编码中多思考多总结,努力提升自己的编码水平,才能编写出更优雅、更高质、更高效的代码。
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。
一、使用通用工具函数
案例一
不完善的写法:
1 | csharp复制代码thisName != null && thisName.equals(name); |
更完善的写法:
1 | ini复制代码(thisName == name) || (thisName != null && thisName.equals(name)); |
建议方案:
1 | csharp复制代码Objects.equals(name, thisName); |
案例二
现象描述:
1 | ini复制代码!(list == null || list.isEmpty()); |
建议方案:
1 | ini复制代码CollectionUtils.isNotEmpty(list); |
主要收益
- 函数式编程,业务代码减少,逻辑一目了然;
- 通用工具函数,逻辑考虑周全,出问题概率低。
- Java常用工具类
二、减少函数代码层级
如果要使函数优美,建议函数代码层级在1-4之间,过多的缩进会让函数难以阅读。
案例一:利用return提前返回函数
现象描述:
1 | scss复制代码// 获取用户余额函数 |
建议方案:
1 | kotlin复制代码// 获取用户余额函数 |
案例二:利用continue提前结束循环
现象描述:
1 | scss复制代码// 获取合计余额函数 |
建议方案:
1 | scss复制代码// 获取合计余额函数 |
- 特殊说明
其它方式:在循环体中,先调用案例1的函数getUserBalance(获取用户余额),再进行对余额进行累加。
在循环体中,建议最多使用一次continue。如果需要有使用多次continue的需求,建议把循环体封装为一个函数。
案例三:利用条件表达式函数减少层级
主要收益
- 代码层级减少,代码缩进减少;
- 模块划分清晰,方便阅读维护。
三、封装条件表达式函数
案例一:把简单条件表达式封装为函数
现象描述:
1 | scss复制代码// 获取门票价格函数 |
建议方案:
1 | typescript复制代码// 获取门票价格函数 |
案例二:把复杂条件表达式封装为函数
现象描述:
1 | scss复制代码// 获取土豪用户列表 |
建议方案:
1 | java复制代码// 获取土豪用户列表 |
以上代码也可以用采用流式(Stream)编程的过滤来实现。
主要收益
- 把条件表达式从业务函数中独立,使业务逻辑更清晰;
- 封装的条件表达式为独立函数,可以在代码中重复使用。
四、尽量避免不必要的空指针判断
本章只适用于项目内部代码,并且是自己了解的代码,才能够尽量避免不必要的空指针判断。对于第三方中间件和系统接口,必须做好空指针判断,以保证代码的健壮性。
案例一:调用函数保证参数不为空,被调用函数尽量避免不必要的空指针判断
现象描述:
1 | scss复制代码// 创建用户信息 |
建议方案:
1 | scss复制代码// 创建用户信息 |
案例二:被调用函数保证返回不为空,调用函数尽量避免不必要的空指针判断
现象描述:
1 | scss复制代码// 保存用户函数 |
建议方案:
1 | scss复制代码// 保存用户函数 |
案例三:赋值逻辑保证列表数据项不为空,处理逻辑尽量避免不必要的空指针判断
现象描述:
1 | scss复制代码// 查询用户列表 |
建议方案:
1 | scss复制代码// 查询用户列表 |
案例四:MyBatis查询函数返回列表和数据项不为空,可以不用空指针判断
MyBatis是一款优秀的持久层框架,是在项目中使用的最广泛的数据库中间件之一。通过对MyBatis源码进行分析,查询函数返回的列表和数据项都不为空,在代码中可以不用进行空指针判断。
现象描述:
这种写法没有问题,只是过于保守了。
1 | scss复制代码// 查询用户函数 |
建议方案:
1 | scss复制代码// 查询用户函数 |
主要收益
- 避免不必要的空指针判断,精简业务代码处理逻辑,提高业务代码运行效率;
- 这些不必要的空指针判断,基本属于永远不执行的Death代码,删除有助于代码维护。
五、内部函数参数尽量使用基础类型
案例一:内部函数参数尽量使用基础类型
现象描述:
1 | ini复制代码// 调用代码 |
建议方案:
1 | arduino复制代码// 调用代码 |
案例二:内部函数返回值尽量使用基础类型
1 | scss复制代码现象描述: |
建议方案:
1 | scss复制代码// 获取订单总额函数 |
此处只是举例说明这种现象,更好的方式是采用流式(Stream)编程。
主要收益
- 内部函数尽量使用基础类型,避免了隐式封装类型的打包和拆包
- 内部函数参数使用基础类型,用语法上避免了内部函数的参数空指针判断
- 内部函数返回值使用基础类型,用语法上避免了调用函数的返回值空指针判断
六、尽量避免返回的数组和列表为null
案例一:尽量避免返回的数组为null,引起不必要的空指针判断
现象描述:
1 | scss复制代码// 调用代码 |
建议方案:
1 | ini复制代码// 调用代码 |
案例二:尽量避免返回的列表为null,引起不必要的空指针判断
现象描述:
1 | scss复制代码// 调用代码 |
建议方案:
1 | scss复制代码// 调用代码 |
主要收益
- 保证返回的数组和列表不为null, 避免调用函数的空指针判断
七、封装函数传入参数
案例一:当传入参数过多时,应封装为参数类
Java规范不允许函数参数太多,不便于维护也不便于扩展。
现象描述:
1 | typescript复制代码// 修改用户函数 |
建议方案:
1 | less复制代码// 修改用户函数 |
案例二:当传入成组参数时,应封装为参数类
既然参数成组出现,就需要封装一个类去描述这种现象。
现象描述:
1 | arduino复制代码// 获取距离函数 |
建议方案:
1 | less复制代码// 获取距离函数 |
主要收益
- 封装过多函数参数为类,使函数更便于扩展和维护;
- 封装成组函数参数为类,使业务概念更明确更清晰。
八、利用return精简不必要的代码
案例一:删除不必要的if
现象描述:
1 | kotlin复制代码// 是否通过函数 |
建议方案:
1 | typescript复制代码// 是否通过函数 |
案例二:删除不必要的else
现象描述:
1 | scss复制代码// 结算工资函数 |
建议方案:
1 | scss复制代码// 结算工资函数 |
案例三:删除不必要的变量
现象描述:
1 | ini复制代码// 查询用户函数 |
建议方案:
1 | scss复制代码// 查询用户函数 |
主要收益
- 精简不必要的代码,让代码看起来更清爽
九、利用临时变量优化代码
在一些代码中,经常会看到a.getB().getC()…getN()的写法,代码健壮性和可读性太差。建议:杜绝函数的级联调用,利用临时变量进行拆分,并做好对象空指针检查。
案例一:利用临时变量厘清逻辑
现象描述:
1 | scss复制代码// 是否土豪用户函数 |
这是精简代码控的最爱,但是可读性实在太差。
建议方案:
1 | java复制代码// 是否土豪用户函数 |
这个方案,增加了代码行数,但是逻辑更清晰。
有时候,当代码的精简性和可读性发生冲突时,更偏向于保留代码的可读性。
案例二:利用临时变量精简代码
现象描述:
1 | scss复制代码// 构建用户函数 |
建议方案:
1 | ini复制代码// 构建用户函数 |
主要收益
- 利用临时变量厘清逻辑,显得业务逻辑更清晰;
- 利用临时变量精简代码,看变量名称即知其义,减少了大量无用代码;
- 如果获取函数比较复杂耗时,利用临时变量可以提高运行效率;
- 利用临时变量避免函数的级联调用,可有效预防空指针异常。
十、其他细节
1. 尽量避免随意使用静态变量
当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如
1 | java复制代码public class A{ |
此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。
2. 尽量避免过多过常地创建Java对象
尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。
3. 尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。
4. 慎用synchronized,尽量减小synchronize的方法
都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以,synchronize的方法尽量减小,并且应尽量使用方法同步代替代码块同步。
5. 尽量使用基本数据类型代替对象
1 | ini复制代码String str = "hello"; |
上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;
1 | ini复制代码String str = new String("hello"); |
此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o
6. 多线程在未发生线程安全前提下应尽量使用HashMap、ArrayList
HashTable、Vector等使用了同步机制,降低了性能。
7. 尽量合理的创建HashMap
当你要创建一个比较大的hashMap时,充分利用这个构造函数
1 | arduino复制代码public HashMap(int initialCapacity, float loadFactor); |
避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。
8. 尽量减少对变量的重复计算
如:
1 | css复制代码for(int i=0;i<list.size();i++) |
应该改为:
1 | css复制代码for(int i=0,len=list.size();i<len;i++) |
并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。
9. 尽量避免不必要的创建
如:
1 | ini复制代码A a = new A(); |
应该改为:
1 | ini复制代码if(i==1){ |
10. 尽量在finally块中释放资源
程序中使用到的资源应当被释放,以避免资源泄漏,这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。
11. 尽量确定StringBuffer的容量
StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。
如:
1 | ini复制代码StringBuffer buffer = new StringBuffer(1000); |
12. 尽量早释放无用对象的引用
大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。
例如:
1 | typescript复制代码public void test(){ |
上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:
1 | typescript复制代码public void test(){ |
这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。
13. 尽量避免使用二维数组
二维数据占用的内存空间比一维数组多得多,大概10倍以上。
14. ArrayList & LinkedList
一个是线性表,一个是链表,一句话,随机查询尽量使用ArrayList,ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好2者得数据结构,对症下药。
15. 尽量使用System.arraycopy ()代替通过来循环复制数组
System.arraycopy() 要比通过循环来复制数组快的多
- public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度。
1 | ini复制代码public static void main(String[] args) { |
运行结果
1 | scss复制代码for循环复制数组1000000次花费时间:161 |
16. 尽量重用对象
过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。
特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。
17. 在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。
18. 在java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。
19. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层
20. 通过StringBuffer的构造函数来设定它的初始化容量,可以明显提升性能
StringBuffer的默认容量为16,当StringBuffer的容量达到最大容量时,它会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达它的最大容量,它就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的!
21. StringBuffer和StringBuilder
区别在于:java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。
####22. 考虑使用静态方法
如果没有必要去访问对象的外部,那么就使之成为静态方法。它会被更快地调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质,调用这个方法不会改变对象的状态。
23. 避免在循环条件中使用复杂表达式
在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。例子:
1 | arduino复制代码class CEL { |
更正:
1 | arduino复制代码class CEL_fixed { |
24. 为’Vectors’ 和 ‘Hashtables’定义初始大小
JVM为Vector扩充大小的时候需要重新创建一个更大的数组,将原原先数组中的内容复制过来,最后,原先的数组再被回收。可见Vector容量的扩大是一个颇费时间的事。
通常,默认的10个元素大小是不够的。你最好能准确的估计你所需要的最佳大小。
1 | ini复制代码public Vector v = new Vector(20); |
25. 对于常量字符串,用’String’ 代替 ‘StringBuffer’
常量字符串并不需要动态改变长度。
例子:
1 | ini复制代码StringBuffer s = new StringBuffer ("Hello"); |
更正:把StringBuffer换成String,如果确定这个String不会再变的话,这将会减少运行开销提高性能。
26. 在字符串相加的时候,使用 ‘ ‘ 代替 “ “,如果该字符串只有一个字符的话
例子:
1 | typescript复制代码public void method(String s) { |
更正: 将一个字符的字符串替换成’ ‘
1 | typescript复制代码public void method(String s) { |
以上仅是Java方面编程时的性能优化,性能优化大部分都是在时间、效率、代码结构层次等方面的权衡,各有利弊,不要把上面内容当成教条,或许有些对我们实际工作适用,有些不适用,还望根据实际工作场景进行取舍,活学活用,变通为宜。
本文转载自: 掘金