Java 字串操作、基本運算方法等優化策略

developerworks發表於2015-08-13

字串操作優化

字串物件

字串物件或者其等價物件 (如 char 陣列),在記憶體中總是佔據最大的空間塊,因此如何高效地處理字串,是提高系統整體效能的關鍵。

String 物件可以認為是 char 陣列的延伸和進一步封裝,它主要由 3 部分組成:char 陣列、偏移量和 String 的長度。char 陣列表示 String 的內容,它是 String 物件所表示字串的超集。String 的真實內容還需要由偏移量和長度在這個 char 陣列中進行定位和擷取。

String 有 3 個基本特點:

1. 不變性;

2. 針對常量池的優化;

3. 類的 final 定義。

不變性指的是 String 物件一旦生成,則不能再對它進行改變。String 的這個特性可以泛化成不變 (immutable) 模式,即一個物件的狀態在物件被建立之後就不再發生變化。不變模式的主要作用在於當一個物件需要被多執行緒共享,並且訪問頻繁時,可以省略同步和鎖等待的時間,從而大幅提高系統效能。

針對常量池的優化指的是當兩個 String 物件擁有相同的值時,它們只引用常量池中的同一個拷貝,當同一個字串反覆出現時,這個技術可以大幅度節省記憶體空間。

下面程式碼 str1、str2、str4 引用了相同的地址,但是 str3 卻重新開闢了一塊記憶體空間,雖然 str3 單獨佔用了堆空間,但是它所指向的實體和 str1 完全一樣。程式碼如下清單 1 所示。

清單 1. 示例程式碼
public class StringDemo {
 public static void main(String[] args){
 String str1 = "abc";
 String str2 = "abc";
 String str3 = new String("abc");
 String str4 = str1;
 System.out.println("is str1 = str2?"+(str1==str2));
 System.out.println("is str1 = str3?"+(str1==str3));
 System.out.println("is str1 refer to str3?"+(str1.intern()==str3.intern()));
 System.out.println("is str1 = str4"+(str1==str4));
 System.out.println("is str2 = str4"+(str2==str4));
 System.out.println("is str4 refer to str3?"+(str4.intern()==str3.intern()));
 }
}

輸出如清單 2 所示。

清單 2. 輸出結果
is str1 = str2?true
is str1 = str3?false
is str1 refer to str3?true
is str1 = str4true
is str2 = str4true
is str4 refer to str3?true

SubString 使用技巧

String 的 substring 方法原始碼在最後一行新建了一個 String 物件,new String(offset+beginIndex,endIndex-beginIndex,value);該行程式碼的目的是為了能高效且快速地共享 String 內的 char 陣列物件。但在這種通過偏移量來擷取字串的方法中,String 的原生內容 value 陣列被複制到新的子字串中。設想,如果原始字串很大,擷取的字元長度卻很短,那麼擷取的子字串中包含了原生字串的所有內容,並佔據了相應的記憶體空間,而僅僅通過偏移量和長度來決定自己的實際取值。這種演算法提高了速度卻浪費了空間。

下面程式碼演示了使用 substring 方法在一個很大的 string 獨享裡面擷取一段很小的字串,如果採用 string 的 substring 方法會造成記憶體溢位,如果採用反覆建立新的 string 方法可以確保正常執行。

清單 3.substring 方法演示
import java.util.ArrayList;
import java.util.List;

public class StringDemo {
 public static void main(String[] args){
 List<String> handler = new ArrayList<String>();
 for(int i=0;i<1000;i++){
 HugeStr h = new HugeStr();
 ImprovedHugeStr h1 = new ImprovedHugeStr();
 handler.add(h.getSubString(1, 5));
 handler.add(h1.getSubString(1, 5));
 }
 }

 static class HugeStr{
 private String str = new String(new char[800000]);
 public String getSubString(int begin,int end){
 return str.substring(begin, end);
 }
 }

 static class ImprovedHugeStr{
 private String str = new String(new char[10000000]);
 public String getSubString(int begin,int end){
 return new String(str.substring(begin, end));
 }
 }
}

輸出結果如清單 4 所示。

清單 4. 輸出結果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.StringValue.from(Unknown Source)
at java.lang.String.<init>(Unknown Source)
at StringDemo$ImprovedHugeStr.<init>(StringDemo.java:23)
at StringDemo.main(StringDemo.java:9)

ImprovedHugeStr 可以工作是因為它使用沒有記憶體洩漏的 String 建構函式重新生成了 String 物件,使得由 substring() 方法返回的、存在記憶體洩漏問題的 String 物件失去所有的強引用,從而被垃圾回收器識別為垃圾物件進行回收,保證了系統記憶體的穩定。

String 的 split 方法支援傳入正規表示式幫助處理字串,但是簡單的字串分割時效能較差。

對比 split 方法和 StringTokenizer 類的處理字串效能,程式碼如清單 5 所示。

切分字串方式討論

String 的 split 方法支援傳入正規表示式幫助處理字串,操作較為簡單,但是缺點是它所依賴的演算法在對簡單的字串分割時效能較差。清單 5 所示程式碼對比了 String 的 split 方法和呼叫 StringTokenizer 類來處理字串時效能的差距。

清單 5.String 的 split 方法演示
import java.util.StringTokenizer;

public class splitandstringtokenizer {
 public static void main(String[] args){
 String orgStr = null;
 StringBuffer sb = new StringBuffer();
 for(int i=0;i<100000;i++){
 sb.append(i);
 sb.append(",");
 }
 orgStr = sb.toString();
 long start = System.currentTimeMillis();
 for(int i=0;i<100000;i++){
 orgStr.split(",");
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 String orgStr1 = sb.toString();
 StringTokenizer st = new StringTokenizer(orgStr1,",");
 for(int i=0;i<100000;i++){
 st.nextToken();
 }
 st = new StringTokenizer(orgStr1,",");
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 String orgStr2 = sb.toString();
 String temp = orgStr2;
 while(true){
 String splitStr = null;
 int j=temp.indexOf(",");
 if(j<0)break;
 splitStr=temp.substring(0, j);
 temp = temp.substring(j+1);
 }
 temp=orgStr2;
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

輸出如清單 6 所示:

清單 6. 執行輸出結果
39015
16
15

當一個 StringTokenizer 物件生成後,通過它的 nextToken() 方法便可以得到下一個分割的字串,通過 hasMoreToken 方法可以知道是否有更多的字串需要處理。對比發現 split 的耗時非常的長,採用 StringTokenizer 物件處理速度很快。我們嘗試自己實現字串分割演算法,使用 substring 方法和 indexOf 方法組合而成的字串分割演算法可以幫助很快切分字串並替換內容。

由於 String 是不可變物件,因此,在需要對字串進行修改操作時 (如字串連線、替換),String 物件會生成新的物件,所以其效能相對較差。但是 JVM 會對程式碼進行徹底的優化,將多個連線操作的字串在編譯時合成一個單獨的長字串。

以上例項執行結果差異較大的原因是 split 演算法對每一個字元進行了對比,這樣當字串較大時,需要把整個字串讀入記憶體,逐一查詢,找到符合條件的字元,這樣做較為耗時。而 StringTokenizer 類允許一個應用程式進入一個令牌(tokens),StringTokenizer 類的物件在內部已經標識化的字串中維持了當前位置。一些操作使得在現有位置上的字串提前得到處理。 一個令牌的值是由獲得其曾經建立 StringTokenizer 類物件的字串所返回的。

清單 7.split 類原始碼
import java.util.ArrayList;

public class Split {
public String[] split(CharSequence input, int limit) { 
int index = 0; 
boolean matchLimited = limit > 0; 
ArrayList<String> matchList = new ArrayList<String>(); 
Matcher m = matcher(input); 
// Add segments before each match found 
while(m.find()) { 
if (!matchLimited || matchList.size() < limit - 1) { 
String match = input.subSequence(index, m.start()).toString(); 
matchList.add(match); 
index = m.end(); 
} else if (matchList.size() == limit - 1) { 
// last one 
String match = input.subSequence(index,input.length()).toString(); 
matchList.add(match); 
index = m.end(); 
} 
} 
// If no match was found, return this 
if (index == 0){ 
return new String[] {input.toString()}; 
}
// Add remaining segment 
if (!matchLimited || matchList.size() < limit){ 
matchList.add(input.subSequence(index, input.length()).toString()); 
}
// Construct result 
int resultSize = matchList.size(); 
if (limit == 0){ 
while (resultSize > 0 && matchList.get(resultSize-1).equals("")) 
resultSize--; 
 String[] result = new String[resultSize]; 
 return matchList.subList(0, resultSize).toArray(result); 
}
}

}

split 藉助於資料物件及字元查詢演算法完成了資料分割,適用於資料量較少場景。

合併字串

由於 String 是不可變物件,因此,在需要對字串進行修改操作時 (如字串連線、替換),String 物件會生成新的物件,所以其效能相對較差。但是 JVM 會對程式碼進行徹底的優化,將多個連線操作的字串在編譯時合成一個單獨的長字串。針對超大的 String 物件,我們採用 String 物件連線、使用 concat 方法連線、使用 StringBuilder 類等多種方式,程式碼如清單 8 所示。

清單 8. 處理超大 String 物件的示例程式碼
public class StringConcat {
 public static void main(String[] args){
 String str = null;
 String result = "";

 long start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 str = str + i;
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 result = result.concat(String.valueOf(i));
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 StringBuilder sb = new StringBuilder();
 for(int i=0;i<10000;i++){
 sb.append(i);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

輸出如清單 9 所示。

清單 9. 執行輸出結果
375
187
0

雖然第一種方法編譯器判斷 String 的加法執行成 StringBuilder 實現,但是編譯器沒有做出足夠聰明的判斷,每次迴圈都生成了新的 StringBuilder 例項從而大大降低了系統效能。

StringBuffer 和 StringBuilder 都實現了 AbstractStringBuilder 抽象類,擁有幾乎相同的對外借口,兩者的最大不同在於 StringBuffer 對幾乎所有的方法都做了同步,而 StringBuilder 並沒有任何同步。由於方法同步需要消耗一定的系統資源,因此,StringBuilder 的效率也好於 StringBuffer。 但是,在多執行緒系統中,StringBuilder 無法保證執行緒安全,不能使用。程式碼如清單 10 所示。

清單 10.StringBuilderVSStringBuffer
public class StringBufferandBuilder {
public StringBuffer contents = new StringBuffer(); 
public StringBuilder sbu = new StringBuilder();

public void log(String message){ 
for(int i=0;i<10;i++){ 
/*
contents.append(i); 
contents.append(message); 
contents.append("/n"); 
*/
contents.append(i);
contents.append("/n");
sbu.append(i);
sbu.append("/n");
} 
} 
public void getcontents(){ 
//System.out.println(contents); 
System.out.println("start print StringBuffer");
System.out.println(contents); 
System.out.println("end print StringBuffer");
}
public void getcontents1(){ 
//System.out.println(contents); 
System.out.println("start print StringBuilder");
System.out.println(sbu); 
System.out.println("end print StringBuilder");
}

 public static void main(String[] args) throws InterruptedException { 
StringBufferandBuilder ss = new StringBufferandBuilder(); 
runthread t1 = new runthread(ss,"love");
runthread t2 = new runthread(ss,"apple");
runthread t3 = new runthread(ss,"egg");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}

}

class runthread extends Thread{ 
String message; 
StringBufferandBuilder buffer; 
public runthread(StringBufferandBuilder buffer,String message){ 
this.buffer = buffer;
this.message = message; 
} 
public void run(){ 
while(true){ 
buffer.log(message); 
//buffer.getcontents();
buffer.getcontents1();
try {
sleep(5000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} 
} 

}

輸出結果如清單 11 所示。

清單 11. 執行結果
start print StringBuffer
0123456789
end print StringBuffer
start print StringBuffer
start print StringBuilder
01234567890123456789
end print StringBuffer
start print StringBuilder
01234567890123456789
01234567890123456789
end print StringBuilder
end print StringBuilder
start print StringBuffer
012345678901234567890123456789
end print StringBuffer
start print StringBuilder
012345678901234567890123456789
end print StringBuilder

StringBuilder 資料並沒有按照預想的方式進行操作。StringBuilder 和 StringBuffer 的擴充策略是將原有的容量大小翻倍,以新的容量申請記憶體空間,建立新的 char 陣列,然後將原陣列中的內容複製到這個新的陣列中。因此,對於大物件的擴容會涉及大量的記憶體複製操作。如果能夠預先評估大小,會提高效能。

資料定義、運算邏輯優化

使用區域性變數

呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧 (Stack) 裡面,讀寫速度較快。其他變數,如靜態變數、例項變數等,都在堆 (heap) 中建立,讀寫速度較慢。清單 12 所示程式碼演示了使用區域性變數和靜態變數的操作時間對比。

清單 12. 區域性變數 VS 靜態變數
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 所示。

清單 13. 執行結果
0
15

以上兩段程式碼的執行時間分別為 0ms 和 15ms。由此可見,區域性變數的訪問速度遠遠高於類的成員變數。

位運算代替乘除法

位運算是所有的運算中最為高效的。因此,可以嘗試使用位運算代替部分算數運算,來提高系統的執行速度。最典型的就是對於整數的乘除運算優化。清單 14 所示程式碼是一段使用算數運算的實現。

清單 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 所示。

清單 15. 執行結果
1000
546
1000
63

兩段程式碼執行了完全相同的功能,在每次迴圈中,整數 1000 乘以 2,然後除以 2。第一個迴圈耗時 546ms,第二個迴圈耗時 63ms。

替換 switch

關鍵字 switch 語句用於多條件判斷,switch 語句的功能類似於 if-else 語句,兩者的效能差不多。但是 switch 語句有效能提升空間。清單 16 所示程式碼演示了 Switch 與 if-else 之間的對比。

清單 16.Switch 示例
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 所示。

清單 17. 執行結果
172
93

使用一個連續的陣列代替 switch 語句,由於對資料的隨機訪問非常快,至少好於 switch 的分支判斷,從上面例子可以看到比較的效率差距近乎 1 倍,switch 方法耗時 172ms,if-else 方法耗時 93ms。

一維陣列代替二維陣列

JDK 很多類庫是採用陣列方式實現的資料儲存,比如 ArrayList、Vector 等,陣列的優點是隨機訪問效能非常好。一維陣列和二維陣列的訪問速度不一樣,一維陣列的訪問速度要優於二維陣列。在效能敏感的系統中要使用二維陣列,儘量將二維陣列轉化為一維陣列再進行處理,以提高系統的響應速度。

清單 18. 陣列方式對比
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 所示。

清單 19. 執行結果
343
624
287
390

第一段程式碼操作的是一維陣列的賦值、取值過程,第二段程式碼操作的是二維陣列的賦值、取值過程。可以看到一維陣列方式比二維陣列方式快接近一半時間。而對於陣列內如果可以減少賦值運算,則可以進一步減少運算耗時,加快程式執行速度。

提取表示式

大部分情況下,程式碼的重複勞動由於計算機的高速執行,並不會對效能構成太大的威脅,但若希望將系統效能發揮到極致,還是有很多地方可以優化的。

清單 20. 提取表示式
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 所示。

清單 21. 執行結果
202
110

兩段程式碼的差別是提取了重複的公式,使得這個公式的每次迴圈計算只執行一次。分別耗時 202ms 和 110ms,可見,提取複雜的重複操作是相當具有意義的。這個例子告訴我們,在迴圈體內,如果能夠提取到迴圈體外的計算公式,最好提取出來,儘可能讓程式少做重複的計算。

優化迴圈

當效能問題成為系統的主要矛盾時,可以嘗試優化迴圈,例如減少迴圈次數,這樣也許可以加快程式執行速度。

清單 22. 減少迴圈次數
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 所示。

清單 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,而不去計算剩餘表示式。簡單地說,在布林表示式的計算中,只要表示式的值可以確定,就會立即返回,而跳過剩餘子表示式的計算。若使用位運算 (按位與、按位或) 代替邏輯與和邏輯或,雖然位運算本身沒有效能問題,但是位運算總是要將所有的子表示式全部計算完成後,再給出最終結果。因此,從這個角度看,使用位運算替代布林運算會使系統進行很多無效計算。

清單 24. 運算方式對比
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 所示。

清單 25. 執行結果
63
0

例項顯示布林計算大大優於位運算,但是,這個結果不能說明位運算比邏輯運算慢,因為在所有的邏輯與運算中,都省略了表示式“”Test_123″.contains(“123″)”的計算,而所有的位運算都沒能省略這部分系統開銷。

使用 arrayCopy()

資料複製是一項使用頻率很高的功能,JDK 中提供了一個高效的 API 來實現它。System.arraycopy() 函式是 native 函式,通常 native 函式的效能要優於普通的函式,所以,僅處於效能考慮,在軟體開發中,應儘可能呼叫 native 函式。ArrayList 和 Vector 大量使用了 System.arraycopy 來運算元據,特別是同一陣列內元素的移動及不同陣列之間元素的複製。arraycopy 的本質是讓處理器利用一條指令處理一個陣列中的多條記錄,有點像組合語言裡面的串操作指令 (LODSB、LODSW、LODSB、STOSB、STOSW、STOSB),只需指定頭指標,然後開始迴圈即可,即執行一次指令,指標就後移一個位置,操作多少資料就迴圈多少次。如果在應用程式中需要進行陣列複製,應該使用這個函式,而不是自己實現。具體應用如清單 26 所示。

清單 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 所示。

清單 27. 執行結果
0
23166

上面的例子顯示採用 arraycopy 方法執行復制會非常的快。原因就在於 arraycopy 屬於本地方法,原始碼如清單 28 所示。

清單 28.arraycopy 方法
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 程式設計優化有很多方面可以入手,作者將以系列的方式逐步介紹覆蓋所有領域。本文是該系列的第一篇文章,主要介紹了字串物件操作相關、資料定義方面的優化方案、運算邏輯優化及建議,從實際程式碼演示入手,對優化建議及方案進行了驗證。作者始終堅信,沒有什麼優化方案是百分百有效的,需要讀者根據實際情況進行選擇、實踐。

相關文章