35 个 Java 代码性能优化总结

2020-10-16 19:17| 发布者: 温柔老虎| 查看: 89| 评论: 0

摘要: 35 个 Java 代码性能优化总结编程艺术思维2019-10-25 14:06:45代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的, ...

35 个 Java 代码性能优化总结

35 个 Java 代码性能优化总结

代码优化,一个很重要的课题。能够有些人感觉没用,一些细小的地方有什么好点窜的,改与不改对于代码的运转效力有什么影响呢?这个题目我是这么斟酌的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,可是,吃的小虾米一多以后,鲸鱼就被喂饱了。代码优化也是一样,假如项目着眼于尽快无BUG上线,那末此时可以抓大放小,代码的细节可以不精打细磨;可是倘使有充足的时候开辟、保护代码,这时辰就必须斟酌每个可以优化的细节了,一个一个细小的优化点积累起来,对于代码的运转效力绝对是有提升的。

代码优化的方针是:

1、减小代码的体积

2、进步代码运转的效力

(1)只管指定类、方式的final修饰符

带有final修饰符的类是不成派生的。在Java焦点API中,有很多利用final的例子,例如java.lang.String,全部类都是final的。为类指定final修饰符可以让类不成以被继续,为方式指定final修饰符可以让方式不成以被重写。假如指定了一个类为final,则该类一切的方式都是final的。Java编译器会寻觅机遇内联一切的final方式,内联对于提升Java运转效力感化严重,此举可以使性能均匀进步50%。

(2)只管重用工具

出格是String工具的利用,出现字符串连接时应当利用StringBuilder/StringBuffer取代。由于Java虚拟机不但要花时候天生工具,今后能够还需要花时候对这些工具停止渣滓接管和处置,是以,天生过量的工具将会给法式的性能带来很大的影响。

(3)尽能够利用部分变量

挪用方式时传递的参数以及在挪用中建立的姑且变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中建立,速度较慢。别的,栈中建立的变量,随着方式的运转竣事,这些内容就没了,不需要额外的渣滓接管。

(4)实时封闭流

Java编程进程中,停止数据库毗连、I/O流操纵时务必谨慎,在利用终了后,实时封闭以开释资本。由于对这些大工具的操纵会形成系统大的开销,稍有失慎,将会致使严重的结果。

(5)只管削减对变量的反复计较

明白一个概念,对方式的挪用,即使方式中只要一句语句,也是有消耗的,包括建立栈帧、挪用方式时庇护现场、挪用方式终了时规复现场等。所以例以下面的操纵:

for (int i = 0; i < list.size(); i++){...}

倡议替换为:

for (int i = 0, length = list.size(); i < length; i++){...}

这样,在list.size()很大的时辰,就削减了很多的消耗。

(6)只管采用懒加载的战略,即在需要的时辰才建立

例如:

String str = "aaa";if (i == 1){  list.add(str);}

倡议替换为:

if (i == 1){  String str = "aaa";  list.add(str);}

(7)慎用异常

异常对性能晦气。抛出异常首先要建立一个新的工具,Throwable接口的机关函数挪用名为fillInStackTrace()的当地同步方式,fillInStackTrace()方式检查仓库,收集挪用跟踪信息。只要有异常被抛出,Java虚拟机就必须调剂挪用仓库,由于在处置进程中建立了一个新的工具。异常只能用于毛病处置,不应当用来控制法式流程。

(8)不要在循环中利用try...catch...,应当把其放在最外层

按照网友们提出的定见,这一点我以为值得商议。

(9)假如能估量到待增加的内容长度,为底层以数组方式实现的调集、工具类指定初始长度

比如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)都可以。

(10)当复制大量数据时,利用System.arraycopy()号令

(11)乘法和除法利用移位操纵

例如:

for (val = 0; val < 100000; val += 5){  a = val * 8;  b = val / 2;}

用移位操纵可以极大地进步性能,由于在计较机底层,对位的操纵是最方便、最快的,是以倡议点窜成:

for (val = 0; val < 100000; val += 5){  a = val << 3;  b = val >> 1;}

移位操纵虽然快,可是能够会使代码不太好了解,是以最好加上响应的正文。

(12)循环内不要不竭建立工具援用

例如:

for (int i = 1; i <= count; i++){ Object obj = new Object(); }

这类做法会致使内存中有count份Object工具援用存在,count很大的话,就花费内存了,倡议为改成:

Object obj = null;for (int i = 0; i <= count; i++){ obj = new Object();}

这样的话,内存中只要一份Object工具援用,每次new Object()的时辰,Object工具援用指向分歧的Object而已,可是内存中只要一份,这样就大大节省了内存空间了。

(13)基于效力和范例检查的斟酌,应当尽能够利用array,没法肯定数组巨细时才利用ArrayList

(14)只管利用HashMap、ArrayList、StringBuilder,除非线程平安需要,否则不保举利用Hashtable、Vector、StringBuffer,后三者由于利用同步机制而致使了性能开销

(15)不要将数组声明为public static final

由于这毫无意义,这样只是界说了援用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个平安缝隙,这意味着这个数组可以被内部类所改变。

(16)只管在合适的场所利用单例

利用单例可以减轻加载的负担、收缩加载的时候、进步加载的效力,但并不是一切地方都适用于单例,简单来说,单例首要适用于以下三个方面:

  • 控制资本的利用,经过线程同步来控制资本的并发拜候
  • 控制实例的发生,以到达节俭资本的目标
  • 控制数据的同享,在不建立间接关联的条件下,让多个不相关的进程或线程之间实现通讯

(17)只管避免随意利用静态变量

要晓得,当某个工具被界说为static的变量所援用,那末gc凡是是不会接管这个工具所占有的堆内存的,如:

/** * Java进修交换QQ群:589809992 我们一路学Java! */public class A{ private static B b = new B(); }

此时静态变量b的生命周期与A类不异,假如A类不被卸载,那末援用B指向的B工具会常驻内存,直到法式停止。

(18)实时断根不再需要的会话

为了断根不再活动的会话,很多利用办事器都有默许的会话超不时候,通常是30分钟。当利用办事器需要保存更多的会话时,假如内存不敷,那末操纵系统会把部分数据转移到磁盘,利用办事器也能够按照MRU(比来最频仍利用)算法把部分不活跃的会话转储到磁盘,甚至能够抛出内存不敷的异常。假如会话要被转储到磁盘,那末必必要先被序列化,在大范围集群中,对工具停止序列化的价格是很高贵的。是以,当会话不再需要时,该当实时挪用HttpSession的invalidate()方式断根会话。

(19)实现RandomAccess接口的调集比如ArrayList,该当利用最普通的for循环而不是foreach循环来遍历

这是JDK保举给用户的。JDK API对于RandomAccess接口的诠释是:实现RandomAccess接口用来表白其支持快速随机拜候,此接口的首要目标是答应一般的算法变动其行为,从而将其利用到随机或持续拜候列表时能供给杰出的性能。现实经历表白,实现RandomAccess接口的类实例,假如是随机拜候的,利用普通for循环效力将高于利用foreach循环;反过来,假如是顺序拜候的,则利用Iterator会效力更高。可以利用类似以下的代码作判定:

if (list instanceof RandomAccess){ for (int i = 0; i < list.size(); i++){}}else{ Iterator iterator = list.iterable(); while (iterator.hasNext()){iterator.next()}}

foreach循环的底层实现道理就是迭代器Iterator,所今后半句"反过来,假如是顺序拜候的,则利用Iterator会效力更高"的意义就是顺序拜候的那些类实例,利用foreach循环去遍历。

(20)利用同步代码块替换同步方式

除非能肯定一全部方式都是需要停止同步的,否则只管利用同步代码块,避免对那些不需要停止同步的代码也停止了同步,影响了代码履行效力。

(21)将常量声明为static final,并以大写命名

这样在编译时代便可以把这些内容放入常量池中,避免运转时代计较天生常量的值。别的,将常量的名字以大写命名也可以方便区分出常量与变量。

(22)不要建立一些不利用的工具,不要导入一些不利用的类

这毫无意义,假如代码中出现"The value of the local variable i is not used"、"The import java.util is never used",那末请删除这些无用的内容。

(23)法式运转进程中避免利用反射

反射是Java供给给用户一个很强大的功用,功用强大常常意味着效力不高。不倡议在法式运转进程中利用特别是频仍利用反射机制,出格是Method的invoke方式,假如确切有需要,一种倡议性的做法是将那些需要经过反射加载的类在项目启动的时辰经过反射实例化出一个工具并放入内存----用户只关心和对端交互的时辰获得最快的响应速度,并不关心对真个项目启动花多久时候。

(24)利用数据库毗连池和线程池

这两个池都是用于重用工具的,前者可以避免频仍地翻开和封闭毗连,后者可以避免频仍地建立和烧毁线程。

(25)利用带缓冲的输入输出流停止IO操纵

带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效力。

(26)顺序插入和随机拜候比力多的场景利用ArrayList,元素删除和中心插入比力多的场景利用LinkedList

这个,了解ArrayList和LinkedList的道理就晓得了。

(27)不要让public方式中有太多的形参

public方式即对外供给的方式,假如给这些方式太多形参的话首要有两点害处:

  • 违反了面向工具的编程思惟,Java讲求一切都是工具,太多的形参,和面向工具的编程思惟并不符合
  • 参数太多势必致使方式挪用的出错几率增加

至于这个"太多"指的是几多个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方式,有10个门生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方式的形参。

(28)字符串变量和字符串常量equals的时辰将字符串常量写在前面

这是一个比力常见的小技能了,倘使有以下代码:

String str = "123";if (str.equals("123")){ ...}

倡议点窜成:

String str = "123";if ("123".equals(str)){ ...}

这么做主如果可以避免空指针异常。

(29)请晓得,在java中if (i == 1)和if (1 == i)是没有区此外,但从阅读习惯上讲,倡议利用前者

平常有人问,"if (i == 1)"和"if (1== i)"有没有区分,这就要从C/C++讲起。

在C/C++中,"if (i == 1)"判定条件建立,是以0与非0为基准的,0暗示false,非0暗示true,倘使有这么一段代码:

int i = 2;if (i == 1){ ...}else{ ...}

C/C++判定"i==1"不建立,所以以0暗示,即false。可是假如:

int i = 2;if (i = 1){ ...}else{ ...}

万一法式员一个不谨慎,把"if (i == 1)"写成"if (i = 1)",这样就有题目了。在if之内将i赋值为1,if判定里面的内容非0,返回的就是true了,可是明显i为2,比力的值是1,应当返回的false。这类情况在C/C++的开辟中是极能够发生的而且会致使一些难以了解的毛病发生,所以,为了避免开辟者在if语句中不正确的赋值操纵,倡议将if语句写为:

int i = 2;if (1 == i){ ...}else{ ...}

这样,即使开辟者不谨慎写成了"1 = i",C/C++编译器也可以第一时候检查出来,由于我们可以对一个变量赋值i为1,可是不能对一个常量赋值1为i。

可是,在Java中,C/C++这类"if (i = 1)"的语法是不成能出现的,由于一旦写了这类语法,Java就会编译报错"Type mismatch: cannot convert from int to boolean"。可是,虽然Java的"if (i == 1)"和"if (1 == i)"在语义上没有任何区分,从阅读习惯上讲,倡议利用前者会更好些。

(30)不要对数组利用toString()方式

看一下对数组利用toString()打印出来的是什么:

public static void main(String[] args){ int[] is = new int[]{1, 2, 3}; System.out.println(is.toString());}

成果是:

[I@18a992f

本意是想打印出数组内容,却有能够由于数组援用is为空而致使空指针异常。不外虽然对数组toString()没成心义,可是对调集toString()是可以打印出调集里面的内容的,由于调集的父类AbstractCollections重写了Object的toString()方式。

(31)不要对超越范围的根基数据范例做向下强迫转型

这绝不会获得想要的成果:

public static void main(String[] args){ long l = 12345678901234L; int i = (int)l; System.out.println(i);}

我们能够期望获得其中的某几位,可是成果却是:

1942892530

诠释一下。Java中long是8个字节64位的,所以12345678901234在计较机中的暗示应当是:

0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010

一个int型数据是4个字节32位的,从低位取出上面这串二进制数据的前32位是:

0111 0011 1100 1110 0010 1111 1111 0010

这串二进制暗示为十进制1942892530,所以就是我们上面的控制台上输出的内容。从这个例子上还能顺便获得两个结论:

1、整型默许的数据范例是int,long l = 12345678901234L,这个数字已经超越了int的范围了,所以最初有一个L,暗示这是一个long型数。顺便,浮点型的默许范例是double,所以界说float的时辰要写成""float f = 3.5f"

2、接下来再写一句"int ii = l + i;"会报错,由于long + int是一个long,不能赋值给int

(32)公用的调集类中不利用的数据一定要实时remove掉

假如一个调集类是公用的(也就是说不是方式里面的属性),那末这个调集里面的元素是不会自动开释的,由于始终有援用指向它们。所以,假如公用调集里面的某些数据不利用而不去remove掉它们,那末将会形成这个公用调集不竭增大,使得系统有内存泄露的隐患。

(33)把一个根基数据范例转为字符串,根基数据范例.toString()是最快的方式、String.valueOf(数据)次之、数据+""最慢

把一个根基数据范例转为一般有三种方式,我有一个Integer型数据i,可以利用i.toString()、String.valueOf(i)、i+""三种方式,三种方式的效力若何,看一个测试:

public static void main(String[] args){ int loopTime = 50000; Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = String.valueOf(i); }  System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = i.toString(); }  System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = i + ""; }  System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms");}

运转成果为:

String.valueOf():11msInteger.toString():5msi + "":25ms

所以今后碰到把一个根基数据范例转为String的时辰,优先斟酌利用toString()方式。至于为什么,很简单:

  • String.valueOf()方式底层挪用了Integer.toString()方式,可是会在挪用前做空判定
  • Integer.toString()方式就不说了,间接挪用了
  • i + ""底层利用了StringBuilder实现,先用append方式拼接,再用toString()方式获得字符串

三者对照下来,明显是2最快、1次之、3最慢

(34)利用最有用力的方式去遍历Map

遍历Map的方式有很多,凡是场景下我们需要的是遍历Map中的Key和Value,那末保举利用的、效力最高的方式是:

public static void main(String[] args){ HashMap hm = new HashMap(); hm.put("111", "222"); Set> entrySet = hm.entrySet(); Iterator> iter = entrySet.iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); System.out.println(entry.getKey() + "\t" + entry.getValue()); }}

假如你只是想遍历一下这个Map的key值,那用"SetkeySet = hm.keySet();"会比力合适一些。

(35)对资本的close()倡议分隔操纵

意义是,比如我有这么一段代码:

try{ XXX.close(); YYY.close();}catch (Exception e){ ...}

倡议点窜成:

try{ XXX.close();}catch (Exception e){ ...}try{ YYY.close();}catch (Exception e){ ...}

虽然有些麻烦,却能避免资本泄露。我们想,假如没有修悔改的代码,万一XXX.close()抛异常了,那末就进入了catch块中了,YYY.close()不会履行,YYY这块资本就不会接管了,一向占用着,这样的代码一多,是能够引发资本句柄泄露的。而改成下面的写法以后,就保证了不管若何XXX和YYY城市被close掉。


最初,我自己是一位处置了多年开辟的Java老法式员,告退今朝在做自己的Java私人定制课程,今年年头我花了一个月整理了一份最合适2019年进修的Java进修干货,可以送给每一位喜好Java的小伙伴,想要获得的可以关注我并在背景私信我:01,即可免费获得。

收藏
告发

路过

雷人

握手

鲜花

鸡蛋

相关分类

返回顶部