某一个Java前辈曾这样理解过Java代码优化:
就像鲸鱼吃虾米一样,也许吃一个两个虾米对于鲸鱼来说作用不大,但是吃的虾米多了,鲸鱼自然饱了。 代码优化一样,也许一个两个的优化,对于提升代码的运行效率意义不大,但是只要处处都能注意代码优化,总体来说对于提升代码的运行效率就很有用了。
这个说法,虽然很有道理,但也不全对。因为在这个硬件发达的今天,服务器内核日渐增大到了16核,CPU也增大至64位,关于代码执行效率已经是非常高了,即使我们不注重日常撸代码的优化小常识,对代码执行效率影响也是微乎其微。
本文的内容有些来自于网络前辈的领悟,有些来源于平时的工作学习,当然这些都不重要,重要的是这些代码优化细节是否真正的对我们有用。
代码优化部分细节:
(1).尽量使用final修饰类,方法名。
带有final的修饰符是不可派生的,也就是被final修饰的类不可被继承,被final修饰的方法不可被重写。这些基础知识大家都早已烂熟于心了,但为什么用到final修饰会提高程序性能呢?
标准答案:(1).把方法锁定,防止任何继承类修改它的意义和实现 (2).编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
非正式解析:使用了final修饰目的是为了阻止改变和提高效率,阻止改变大家都看出来了,提高效率却还是一头雾水。简单说高效的原因:Final方法会在编译过程中,被java内嵌机制进行inline优化。
PS: inline优化就是指:在编译时,直接调用被final修饰的方法代码替换,也就是内嵌,而不是在代码运行时期再去调用。inline需要在编译时期就知道需要调用那个方法,所以就要求方法是final修饰的。因为非final方法会存在被子类重写现象即多态,则编译器在编译阶段就无法确定将来调用的方法真正类型,也就无法确定调用哪一个方法。
(2).及时关闭I/O流,数据连接
在日常撸代码时候,若遇到各种流操作,或者数据库连接操作时,应该时刻注意关闭连接。因为这些大对象的操作未及时关闭,会对系统造成大笔开销。日常中常见灾难就是,各种上线的系统,莫名服务器宕机,其中部分原因,就是我们未及时关闭连接,从而造成的内存泄漏导致。
(3).尽量使用懒加载模式
在程序真正需要调用时候才去new对象。
(4).尽可能的使用局部变量
首先我们该知道,日常撸代码会经常用到java内存的那些区域:1.静态存储区:主要存放静态数据、常量,程序结束后由系统释放 。2.栈区:当方法执行时,方法体内的局部变量,和参数在此区创建。编译器自动分配释放。 3.堆区:动态内存的分配,通常指对象的实例,一般由程序员分配释放,若程序员不释放,程序结束时可能由GC回收。
调用方法时的参数以及方法里需要用到的局部变量会临时存放在栈区,运行速度相比较快。而且该变量会随着方法的结束而释放,不用额外GC回收
(5).尽量重用对象
特别是对于String对象使用,出现字符串链接时尽量使用StringBuffer/StringBuilder代替,因为java虚拟机不仅需要花时间生成对象,也需要花时间进行GC。所以生成过多的对象会给程序性能带来挺大的影响
(6).尽量减少对变量的重复计算
明确一个概念,只要是对方法的调用,里面即使只有一句语句都是有消耗的,所以如下列:
(7).慎用异常
异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
(8).如果能估计到待添加字节的使用长度,为底层是以数组方式实现的集合、工具类初始化长度
比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:
StringBuilder() // 默认分配16个字符的空间
StringBuilder(int size) // 默认分配size个字符的空间
StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间
可以通过类(这里指的不仅仅是上面的StringBuilder)的构造函数来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中----这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:
在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间
把原来的4096个字符拷贝到新的的字符数组中去
这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。
(8).当复制大量数组数据时,使用System.arraycop()命令替换使用For each()
System中提供了一个native静态方法arraycopy(),可以使用这个方法来实现数组之间的复制。对于一维数组来说,这种复制属性值传递,修改副本不会影响原来的值。对于二维或者一维数组中存放的是对象时,复制结果是一维的引用变量传递给副本的一维数组,修改副本时,会影响原来的数组。
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
(9).使用乘法和除法时用移位运算
(10).避免在循环体内new对象
(11).尽量避免随意使用静态变量
要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的。
此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止
(12).实现RandomAccess接口的集合比如ArrayList,应当使用for循环,而不是foreach循环
在对List特别的遍历算法中,要尽量来判断是属于RandomAccess(如ArrayList)还是SequenceAccess(如LinkedList),因为适合RandomAccess List的遍历算法,用在SequenceAccess List上就差别很大,即对于实现了RandomAccess接口的类实例而言,此循环
for (int i=0, i<list.size(); i++) list.get(i);的运行速度要快于以下循环:
for (Iterator i=list.iterator(); i.hasNext(); ) i.next();实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高
遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式Iterator
(13).将常量声明为 static fianl ,并以大写命名
这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量
(14).程序运行过程中,避免使用反射
反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存----用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。
(15).程序中使用连接池和线程池
这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程
(16).使用带缓冲的输入输出流进行IO操作
带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率
(17).将ArrayList、linkLis运用于合适的场景
顺序插入、随机访问运用多的是ArrayList,元素删除,中间插入较多的是LinkList
(18).不要让public方法中有太多形参
public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:
违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合 参数太多势必导致方法调用的出错概率增加 多个形参建议用DTO封装成对象
(19).字符串常量和字符串变量使用equals时,常量写在前面
(20).把一个基本数据类型转换为字符串
把一个基本数据类型转为一般有三种方式,我有一个Integer型数据i,可以使用i.toString()、String.valueOf(i)、i+""三种方式,效率依次降低
(21).对于ThreadLocal使用前或使用后一定要remove
当前基本所有的项目都使用了线程池技术,这非常好,可以动态配置线程数、可以重用线程。
然而,如果你在项目中使用到了ThreadLocal,一定要记得使用前或者使用后remove一下。这是因为上面提到了线程池技术做的是一个线程重用,这意味着代码运行过程中,一条线程使用完毕,并不会被销毁而是等待下一次的使用
线程不销毁意味着上条线程set的ThreadLocal.ThreadLocalMap中的数据依然存在,那么在下一条线程重用这个Thread的时候,很可能get到的是上条线程set的数据而不是自己想要的内容。
这个问题非常隐晦,一旦出现这个原因导致的错误,没有相关经验或者没有扎实的基础非常难发现这个问题,因此在写代码的时候就要注意这一点,这将给你后续减少很多的工作量。
(22).循环体内不要使用+进行字符串拼接,而直接用StirngBuilder的append
意思就是每次虚拟机碰到"+"这个操作符对字符串进行拼接的时候,会new出一个StringBuilder,然后调用append方法,最后调用toString()方法转换字符串赋值给对象,即循环多少次,就会new出多少个StringBuilder()来,这对于内存是一种浪费
(23).不捕获java类库中定义的继承RuntimeException的运行异常
异常处理效率低,RuntimeException的运行时异常类,其中绝大多数完全可以由程序员来规避,比如:
ArithmeticException可以通过判断除数是否为空来规避 NullPointerException可以通过判断对象是否为空来规避 IndexOutOfBoundsException可以通过判断数组/字符串长度来规避 ClassCastException可以通过instanceof关键字来规避 ConcurrentModificationException可以使用迭代器来规避
(24).把静态类、单列类、工厂类的构造函数用private修饰
这是因为静态类、单例类、工厂类这种类本来我们就不需要外部将它们new出来,将构造函数置为private之后,保证了这些类不会产生实例对象。
做为一个非正式程序员,还是得有一种专业的精神。十分倡导大家像我一样还是该多学习java代码的优化常识,日常使用时多思考多注意。为什么要这样去做?因为我们代码优化最重要的目的,不是为了使得自己撸的某一模块代码效率最高,而是为了防患未知的错误。在写代码的源头开始,我们就应该注意各种细节,权衡使用最优的选择,这样才能最大程度的避免未知的错误,从长远来看,也是提高了效率,真正降低了工作量。