调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。
public class variableCompare { public static int b = 0; public static void main(String[] args){ int a = 0; long starttime = System.currentTimeMillis(); for(int i=0;i<1000000;i++){ a++;//在函数体内定义局部变量 } System.out.println(System.currentTimeMillis() - starttime); starttime = System.currentTimeMillis(); for(int i=0;i<1000000;i++){ b++;//在函数体内定义局部变量 } System.out.println(System.currentTimeMillis() - starttime); } }
运行后输出如清单 13 所示。
0 15
以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。
位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。
public class yunsuan { public static void main(String args[]){ long start = System.currentTimeMillis(); long a=1000; for(int i=0;i<10000000;i++){ a*=2; a/=2; } System.out.println(a); System.out.println(System.currentTimeMillis() - start); start = System.currentTimeMillis(); for(int i=0;i<10000000;i++){ a<<=1; a>>=1; } System.out.println(a); System.out.println(System.currentTimeMillis() - start); } }
运行输出如清单 15 所示。
1000 546 1000 63
两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。
关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。
public class switchCompareIf { public static int switchTest(int value){ int i = value%10+1; switch(i){ case 1:return 10; case 2:return 11; case 3:return 12; case 4:return 13; case 5:return 14; case 6:return 15; case 7:return 16; case 8:return 17; case 9:return 18; default:return -1; } } public static int arrayTest(int[] value,int key){ int i = key%10+1; if(i>9 || i<1){ return -1; }else{ return value[i]; } } public static void main(String[] args){ int chk = 0; long start=System.currentTimeMillis(); for(int i=0;i<10000000;i++){ chk = switchTest(i); } System.out.println(System.currentTimeMillis()-start); chk = 0; start=System.currentTimeMillis(); int[] value=new int[]{0,10,11,12,13,14,15,16,17,18}; for(int i=0;i<10000000;i++){ chk = arrayTest(value,i); } System.out.println(System.currentTimeMillis()-start); } }
运行输出如清单 17 所示。
172 93
使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。
JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量 将二维数组转化为一维数组再进行处理,以提高系统的响应速度。
public class arrayTest { public static void main(String[] args){ long start = System.currentTimeMillis(); int[] arraySingle = new int[1000000]; int chk = 0; for(int i=0;i<100;i++){ for(int j=0;j<arraySingle.length;j++){ arraySingle[j] = j; } } for(int i=0;i<100;i++){ for(int j=0;j<arraySingle.length;j++){ chk = arraySingle[j]; } } System.out.println(System.currentTimeMillis() - start); start = System.currentTimeMillis(); int[][] arrayDouble = new int[1000][1000]; chk = 0; for(int i=0;i<100;i++){ for(int j=0;j<arrayDouble.length;j++){ for(int k=0;k<arrayDouble[0].length;k++){ arrayDouble[i][j]=j; } } } for(int i=0;i<100;i++){ for(int j=0;j<arrayDouble.length;j++){ for(int k=0;k<arrayDouble[0].length;k++){ chk = arrayDouble[i][j]; } } } System.out.println(System.currentTimeMillis() - start); start = System.currentTimeMillis(); arraySingle = new int[1000000]; int arraySingleSize = arraySingle.length; chk = 0; for(int i=0;i<100;i++){ for(int j=0;j<arraySingleSize;j++){ arraySingle[j] = j; } } for(int i=0;i<100;i++){ for(int j=0;j<arraySingleSize;j++){ chk = arraySingle[j]; } } System.out.println(System.currentTimeMillis() - start); start = System.currentTimeMillis(); arrayDouble = new int[1000][1000]; int arrayDoubleSize = arrayDouble.length; int firstSize = arrayDouble[0].length; chk = 0; for(int i=0;i<100;i++){ for(int j=0;j<arrayDoubleSize;j++){ for(int k=0;k<firstSize;k++){ arrayDouble[i][j]=j; } } } for(int i=0;i<100;i++){ for(int j=0;j<arrayDoubleSize;j++){ for(int k=0;k<firstSize;k++){ chk = arrayDouble[i][j]; } } } System.out.println(System.currentTimeMillis() - start); } }
运行输出如清单 19 所示。
343 624 287 390
第一段代码操作的是一维数组的赋值、取值过程,第二段代码操作的是二维数组的赋值、取值过程。可以看到一维数组方式比二维数组方式快接近一半时间。而对于数组内如果可以减少赋值运算,则可以进一步减少运算耗时,加快程序运行速度。
大部分情况下,代码的重复劳动由于计算机的高速运行,并不会对性能构成太大的威胁,但若希望将系统性能发挥到极致,还是有很多地方可以优化的。
public class duplicatedCode { public static void beforeTuning(){ long start = System.currentTimeMillis(); double a1 = Math.random(); double a2 = Math.random(); double a3 = Math.random(); double a4 = Math.random(); double b1,b2; for(int i=0;i<10000000;i++){ b1 = a1*a2*a4/3*4*a3*a4; b2 = a1*a2*a3/3*4*a3*a4; } System.out.println(System.currentTimeMillis() - start); } public static void afterTuning(){ long start = System.currentTimeMillis(); double a1 = Math.random(); double a2 = Math.random(); double a3 = Math.random(); double a4 = Math.random(); double combine,b1,b2; for(int i=0;i<10000000;i++){ combine = a1*a2/3*4*a3*a4; b1 = combine*a4; b2 = combine*a3; } System.out.println(System.currentTimeMillis() - start); } public static void main(String[] args){ duplicatedCode.beforeTuning(); duplicatedCode.afterTuning(); } }
运行输出如清单 21 所示。
202 110
两段代码的差别是提取了重复的公式,使得这个公式的每次循环计算只执行一次。分别耗时 202ms 和 110ms,可见,提取复杂的重复操作是相当具有意义的。这个例子告诉我们,在循环体内,如果能够提取到循环体外的计算公式,最好提取出来,尽可能让程序 少做重复的计算。
当性能问题成为系统的主要矛盾时,可以尝试优化循环,例如减少循环次数,这样也许可以加快程序运行速度。
public class reduceLoop { public static void beforeTuning(){ long start = System.currentTimeMillis(); int[] array = new int[9999999]; for(int i=0;i<9999999;i++){ array[i] = i; } System.out.println(System.currentTimeMillis() - start); } public static void afterTuning(){ long start = System.currentTimeMillis(); int[] array = new int[9999999]; for(int i=0;i<9999999;i+=3){ array[i] = i; array[i+1] = i+1; array[i+2] = i+2; } System.out.println(System.currentTimeMillis() - start); } public static void main(String[] args){ reduceLoop.beforeTuning(); reduceLoop.afterTuning(); } }
运行输出如清单 23 所示。
265 31
这个例子可以看出,通过减少循环次数,耗时缩短为原来的 1/8。
虽 然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算确实是非常错误的选择。在条件判断时,Java 会对布尔运算做相当充分的优化。假设有表达式 a、b、c 进行布尔运算“a&&b&&c”,根据逻辑与的特点,只要在整个布尔表达式中有一项返回 false,整个表达式就返回 false,因此,当表达式 a 为 false 时,该表达式将立即返回 false,而不会再去计算表达式 b 和 c。若此时,表达式 a、b、c 需要消耗大量的系统资源,这种处理方式可以节省这些计算资源。同理,当计算表达式“a||b||c”时,只要 a、b 或 c,3 个表达式其中任意一个计算结果为 true 时,整体表达式立即返回 true,而不去计算剩余表达式。简单地说,在布尔表达式的计算中,只要表达式的值可以确定,就会立即返回,而跳过剩余子表达式的计算。若使用位运算 (按位与、按位或) 代替逻辑与和逻辑或,虽然位运算本身没有性能问题,但是位运算总是要将所有的子表达式全部计算完成后,再给出最终结果。因此,从这个角度看,使用位运算替 代布尔运算会使系统进行很多无效计算。
public class OperationCompare { public static void booleanOperate(){ long start = System.currentTimeMillis(); boolean a = false; boolean b = true; int c = 0; //下面循环开始进行位运算,表达式里面的所有计算因子都会被用来计算 for(int i=0;i<1000000;i++){ if(a&b&"Test_123".contains("123")){ c = 1; } } System.out.println(System.currentTimeMillis() - start); } public static void bitOperate(){ long start = System.currentTimeMillis(); boolean a = false; boolean b = true; int c = 0; //下面循环开始进行布尔运算,只计算表达式 a 即可满足条件 for(int i=0;i<1000000;i++){ if(a&&b&&"Test_123".contains("123")){ c = 1; } } System.out.println(System.currentTimeMillis() - start); } public static void main(String[] args){ OperationCompare.booleanOperate(); OperationCompare.bitOperate(); } }
运行输出如清单 25 所示。
63 0
实例显示布尔计算大大优于位运算,但是,这个结果不能说明位运算比逻辑运算慢,因为在所有的逻辑与运算中,都省略了表达式“"Test_123".contains("123")”的计算,而所有的位运算都没能省略这部分系统开销。
数 据复制是一项使用频率很高的功能,JDK 中提供了一个高效的 API 来实现它。System.arraycopy() 函数是 native 函数,通常 native 函数的性能要优于普通的函数,所以,仅处于性能考虑,在软件开发中,应尽可能调用 native 函数。ArrayList 和 Vector 大量使用了 System.arraycopy 来操作数据,特别是同一数组内元素的移动及不同数组之间元素的复制。arraycopy 的本质是让处理器利用一条指令处理一个数组中的多条记录,有点像汇编语言里面的串操作指令 (LODSB、LODSW、LODSB、STOSB、STOSW、STOSB),只需指定头指针,然后开始循环即可,即执行一次指令,指针就后移一个位 置,操作多少数据就循环多少次。如果在应用程序中需要进行数组复制,应该使用这个函数,而不是自己实现。具体应用如清单 26 所示。
public class arrayCopyTest { public static void arrayCopy(){ int size = 10000000; int[] array = new int[size]; int[] arraydestination = new int[size]; for(int i=0;i<array.length;i++){ array[i] = i; } long start = System.currentTimeMillis(); for(int j=0;j>1000;j++){ System.arraycopy(array, 0, arraydestination, 0, size);//使用 System 级别的本地 arraycopy 方式 } System.out.println(System.currentTimeMillis() - start); } public static void arrayCopySelf(){ int size = 10000000; int[] array = new int[size]; int[] arraydestination = new int[size]; for(int i=0;i<array.length;i++){ array[i] = i; } long start = System.currentTimeMillis(); for(int i=0;i<1000;i++){ for(int j=0;j<size;j++){ arraydestination[j] = array[j];//自己实现的方式,采用数组的数据互换方式 } } System.out.println(System.currentTimeMillis() - start); } public static void main(String[] args){ arrayCopyTest.arrayCopy(); arrayCopyTest.arrayCopySelf(); } }
输出如清单 27 所示。
0 23166
上面的例子显示采用 arraycopy 方法执行复制会非常的快。原因就在于 arraycopy 属于本地方法,源代码如清单 28 所示。
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
src - 源数组;srcPos - 源数组中的起始位置; dest - 目标数组;destPos - 目标数据中的起始位置;length - 要复制的数组元素的数量。清单 28 所示方法使用了 native 关键字,调用的为 C++编写的底层函数,可见其为 JDK 中的底层函数。
Java 程序设计优化有很多方面可以入手,作者将以系列的方式逐步介绍覆盖所有领域。本文是该系列的第一篇文章,主要介绍了字符串对象操作相关、数据定义方面的优 化方案、运算逻辑优化及建议,从实际代码演示入手,对优化建议及方案进行了验证。作者始终坚信,没有什么优化方案是百分百有效的,需要读者根据实际情况进 行选择、实践。