對一個字符串拼接有三種方法:加號、concat方法及StringBuilder(或StringBuffer ,由於StringBuffer的方法與StringBuilder相同,不在贅述)的append方法,其中加號是最常用的,其它兩種方式偶爾會出現在一些開源項目中,那這三者之間有什麼區別嗎?我們看看下面的例子:
1 public class Client56 { 2 public static void main(String[] args) { 3 // 加號拼接 4 String str = ""; 5 long start1 = System.currentTimeMillis(); 6 for (int i = 0; i < 100000; i++) { 7 str += "c"; 8 } 9 long end1 = System.currentTimeMillis(); 10 System.out.println("加號拼接耗時:" + (end1 - start1) + "ms"); 11 12 // concat拼接 13 str = ""; 14 long start2 = System.currentTimeMillis(); 15 for (int i = 0; i < 100000; i++) { 16 str = str.concat("c"); 17 } 18 long end2 = System.currentTimeMillis(); 19 System.out.println("concat拼接耗時:" + (end2 - start2) + "ms"); 20 21 // StringBuilder拼接 22 str = ""; 23 StringBuilder buffer = new StringBuilder(""); 24 long start3 = System.currentTimeMillis(); 25 for (int i = 0; i < 100000; i++) { 26 buffer.append("c"); 27 } 28 long end3 = System.currentTimeMillis(); 29 System.out.println("StringBuilder拼接耗時:" + (end3 - start3) + "ms"); 30 31 // StringBuffer拼接 32 str = ""; 33 StringBuffer sb = new StringBuffer(""); 34 long start4 = System.currentTimeMillis(); 35 for (int i = 0; i < 100000; i++) { 36 sb.append("c"); 37 } 38 long end4 = System.currentTimeMillis(); 39 System.out.println("StringBuffer拼接耗時:" + (end4 - start4) + "ms"); 40 41 } 42 }
上面是4種不同方式的字符串拼接方式,循環10萬次後檢查其執行時間,執行結果如下:
從上面的執行結果來看,在字符串拼接方式中,StringBuilder的append方法最快,StringBuffer的append方法次之(因為StringBuffer的append方法是線程安全的,同步方法自然慢一點),其次是concat方法,加號最慢,這是為何呢?
(1)、"+" 方法拼接字符串:雖然編輯器對字符串的加號做了優化,它會使用StringBuilder的append方法進行追加,按道理來說,其執行時間也應該是1ms,不過最終是通過toString方法轉換為String字符串的,例子中的"+" 拼接的代碼如下代碼相同
str= new StringBuilder(str).append("c").toString();
注意看,它與純粹使用StringBuilder的append方法是不同的:一是每次循環都會創建一個StringBuilder對象,二是每次執行完畢都要調用toString方法將其轉換為字符串——它的執行時間就耗費在這裡了!
(2)、concat方法拼接字符串:我們從源碼上看一下concat方法的實現,代碼如下:
public String concat(String str) { int otherLen = str.length(); //如果追加字符長度為0,則返回字符串本身 if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); //產生一個新的字符串 return new String(buf, true); }
其整體看上去就是一個數組拷貝,雖然在內存中處理都是原子性操作,速度非常快,不過,注意看最後的return語句,每次concat操作都會創建一個String對象,這就是concat速度慢下來的真正原因,它創建了10萬個String對象呀。
(3)、append方法拼接字符串:StringBuilder的append方法直接由父類AbstractStringBuilder實現,其代碼如下:
public StringBuilder append(String str) { super.append(str); return this; }
public AbstractStringBuilder append(String str) { //如果是null值,則把null作為字符串處理 if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); //字符串復制到目標數組 str.getChars(0, len, value, count); count += len; return this; }
看到沒,整個append方法都在做字符數組處理,加長,然後拷貝數組,這些都是基本的數據處理,沒有創建任何對象,所以速度也就最快了!注意:例子中是在隨後通過StringBuilder的toString方法返回了一個字符串,也就是說在10萬次循環結束後才生成了一個String對象。StringBuffer的處理和此類似,只是方法是同步的而已。
四者的實現方法不同,性能也就不同,但並不表示我們一定要使用StringBuilder,這是因為"+"非常符合我們的編碼習慣,適合閱讀,兩個字符串拼接,就用加號連一下,這很正常,也很友好,在大多數情況下我們都可以使用加號操作,只有在系統性能臨界(如在性能 " 增長一分則太長" 的情況下)的時候才可以考慮使用concat或append方法。而且,很多時候系統80% 的性能是消耗在20%的代碼上的,我們的精力應該更多的投入到算法和結構上。
注意:適當的場景使用適當的字符串拼接方式。
字符串的操作,諸如追加、合並、替換、倒敘、分割等,都是在編碼過程中經常用到的,而且Java也提供了append、replace、reverse、spit等方法來完成這些操作,它們使用起來確實方便,但是更多時候,需要使用正則表達式來完成復雜的處理,我們來看一個例子:統計一篇文章中英文單詞的數量,很簡單吧,代碼如下:
1 public class Client57 { 2 public static void main(String[] args) { 3 Scanner input = new Scanner(System.in); 4 while (input.hasNext()) { 5 String str = input.nextLine(); 6 // 使用split方法分割後統計 7 int wordsCount = str.split(" ").length; 8 System.out.println(str + "單詞數:" + wordsCount); 9 } 10 } 11 }
使用spit方法根據空格來分割單詞,然後計算分割後的數組長度,這種方法可靠嗎?我們看看輸出結果:
注意看輸出,除了第一個輸入"Today is Monday"正確外,其它的都是錯誤的!第二條輸入中的單詞"Monday"前有2個連續的空格,第三條輸入中"No"單詞前後都沒有空格,最後一個輸入則沒有把連寫符號" ' "考慮進去,這樣統計出來的單詞數量肯定是錯誤一堆,那怎麼做才合理呢?
如果考慮使用一個循環來處理這樣的"異常"情況,會使程序的穩定性變差,而且要考慮太多太多的因素,這讓程序的復雜性也大大提高了。那如何處理呢?可以考慮使用正則表達式,代碼如下:
1 public class Client57 { 2 public static void main(String[] args) { 3 Scanner input = new Scanner(System.in); 4 while (input.hasNext()) { 5 String str = input.nextLine(); 6 //正則表達式對象 7 Pattern p = Pattern.compile("\\b\\w+\\b"); 8 //生成匹配器 9 Matcher matcher =p.matcher(str); 10 int wordsCount = 0; 11 while(matcher.find()){ 12 wordsCount++; 13 } 14 System.out.println(str + "單詞數:" + wordsCount); 15 } 16 } 17 }
准不准確,我們看看相同的輸入,輸出結果如下:
每項的輸出都是准確的,而且程序也不復雜,先生成一個正則表達式對象,然後使用匹配器進行匹配,之後通過一個while循環統計匹配的數量。需要說明的是,在Java的正則表達式中"\b" 表示的是一個單詞的邊界,它是一個位置界定符,一邊為字符或數字,另外一邊為非字符或數字,例如"A"這樣一個輸入就有兩個邊界,即單詞"A"的左右位置,這也就說明了為什麼要加上"\w"(它表示的是字符或數字)。
正則表達式在字符串的查找,替換,剪切,復制,刪除等方面有著非凡的作用,特別是面對大量的文本字符需要處理(如需要讀取大量的LOG日志)時,使用正則表達式可以大幅地提高開發效率和系統性能,但是正則表達式是一個惡魔,它會使程序難以讀懂,想想看,寫一個包含^、$、\A、\s、\Q、+、?、()、{}、[]等符號的正則表達式,然後再告訴你這是一個" 這樣,這樣......"字符串查找,你是不是要崩潰了?這個代碼確實不好閱讀,你就要在正則上多下點功夫了。
注意:正則表達式是惡魔,威力巨大,但難以控制。
Java的亂碼問題由來已久,有經驗的開發人員肯定遇到過亂碼,有時從Web接收的亂碼,有時從數據庫中讀取的亂碼,有時是在外部接口中接收的亂碼文件,這些都讓我們困惑不已,甚至是痛苦不堪,看如下代碼:
1 public class Client58 { 2 public static void main(String[] args) throws UnsupportedEncodingException { 3 String str = "漢字"; 4 // 讀取字節 5 byte b[] = str.getBytes("UTF-8"); 6 // 重新生成一個新的字符串 7 System.out.println(new String(b)); 8 } 9 }
Java文件是通過IDE工具默認創建的,編碼格式是GBK,大家想想看上面的輸出結果會是什麼?可能是亂碼吧?兩個編碼格式不同。我們暫時不說結果,先解釋一下Java中的編碼規則。Java程序涉及的編碼包括兩部分:
(1)、Java文件編碼:如果我們使用記事本創建一個.java後綴的文件,則文件的編碼格式就是操作系統默認的格式。如果是使用IDE工具創建的,如Eclipse,則依賴於IDE的設置,Eclipse默認是操作系統編碼(Windows一般為GBK);
(2)、Class文件編碼:通過javac命令生成的後綴名為.class的文件是UTF-8編碼的UNICODE文件,這在任何操作系統上都是一樣的,只要是.class文件就會使UNICODE格式。需要說明的是,UTF是UNICODE的存儲和傳輸格式,它是為了解決UNICODE的高位占用冗余空間而產生的,使用UTF編碼就意味著字符集使用的是UNICODE.
再回到我們的例子上,getBytes方法會根據指定的字符集取出字節數組(這裡按照UNICODE格式來提取),然後程序又通過new String(byte [] bytes)重新生成一個字符串,來看看String的這個構造函數:通過操作系統默認的字符集解碼指定的byte數組,構造一個新的String,結果已經很清楚了,如果操作系統是UTF-8的話,輸出就是正確的,如果不是,則會是亂碼。由於這裡使用的是默認編碼GBK,那麼輸出的結果也就是亂碼了。我們再詳細分解一下運行步驟:
步驟1:創建Client58.java文件:該文件的默認編碼格式GBK(如果是Eclipse,則可以在屬性中查看到)。
步驟2:編寫代碼(如上);
步驟3:保存,使用javac編譯,注意我們沒有使用"javac -encoding GBK Client58.java" 顯示聲明Java的編碼方式,javac會自動按照操作系統的編碼(GBK)讀取Client58.java文件,然後將其編譯成.class文件。
步驟4:生成.class文件。編譯結束,生成.class文件,並保存到硬盤上,此時 .class文件使用的UTF-8格式編碼的UNICODE字符集,可以通過javap 命令閱讀class文件,其中" 漢字"變量也已經由GBK轉變成UNICODE格式了。
步驟5:運行main方法,提取"漢字"的字節數組。"漢字" 原本是按照UTF-8格式保存的,要再提取出來當然沒有任何問題了。
步驟6:重組字符串,讀取操作系統默認的編碼GBK,然後重新編碼變量b的所有字節。問題就在這裡產生了:因為UNICODE的存儲格式是兩個字節表示一個字符(注意:這裡是指UCS-2標准),雖然GBK也是兩個字節表示一個字符,但兩者之間沒有映射關系,只要做轉換只能讀取映射表,不能實現自動轉換----於是JVM就按照默認的編碼方式(GBK)讀取了UNICODE的兩個字節。
步驟7:輸出亂碼,程序運行結束,問題清楚了,解決方案也隨之產生,方案有兩個。
步驟8:修改代碼,明確指定編碼即可,代碼如下:
System.out.println(new String(b,"UTF-8"));
步驟9:修改操作系統的編碼方式,各個操作系統的修改方式不同,不再贅述。
我們可以把字符串讀取字節的過程看做是數據傳輸的需要(比如網絡、存儲),而重組字符串則是業務邏輯的需求,這樣就可以是亂碼重現:通過JDBC讀取的字節數組是GBK的,而業務邏輯編碼時采用的是UTF-8,於是亂碼就產生了。對於此類問題,最好的解決辦法就是使用統一的編碼格式,要麼都用GBK,要麼都用UTF-8,各個組件、接口、邏輯層、都用UTF-8,拒絕獨樹一幟的情況。
問題清楚了,我麼看看以下代碼:
1 public class Client58 { 2 public static void main(String[] args) throws UnsupportedEncodingException { 3 String str = "漢字"; 4 // 讀取字節 5 byte b[] = str.getBytes("GB2312"); 6 // 重新生成一個新的字符串 7 System.out.println(new String(b)); 8 } 9 }
僅僅修改了讀取字節的編碼方式(修改成了GB2312),結果會怎樣呢?又或者將其修改成GB18030,結果又是怎樣的呢?結果都是"漢字",不是亂碼。這是因為GB2312是中文字符集的V1.0版本,GBK是V2.0版本,GB18030是V3.0版本,版本是向下兼容的,只是它們包含的漢字數量不同而已,注意UNICODE可不在這個序列之內。
注意:一個系統使用統一的編碼。
在Java 中一涉及中文處理就會冒出很多問題來,其中排序也是一個讓人頭疼的課題,我們看如下代碼:
1 public class Client59 { 2 public static void main(String[] args) { 3 String[] strs = { "張三(Z)", "李四(L)", "王五(W)" }; 4 Arrays.sort(strs); 5 int i = 0; 6 for (String str : strs) { 7 System.out.println((++i) + "、" + str); 8 } 9 } 10 }
上面的代碼定義了一個數組,然後進行升序排序,我們期望的結果是按照拼音升序排列,即為李四、王五、張三,但是結果卻不是這樣的:
這是按照什麼排的序呀,非常混亂!我們知道Arrays工具類的默認排序是通過數組元素的compareTo方法進行比較的,那我們來看String類的compareTo的主要實現:
1 public int compareTo(String anotherString) { 2 int len1 = value.length; 3 int len2 = anotherString.value.length; 4 int lim = Math.min(len1, len2); 5 char v1[] = value; 6 char v2[] = anotherString.value; 7 8 int k = 0; 9 while (k < lim) { 10 char c1 = v1[k]; 11 char c2 = v2[k]; 12 if (c1 != c2) { 13 return c1 - c2; 14 } 15 k++; 16 } 17 return len1 - len2; 18 }
上面的代碼先取得字符串的字符數組,然後一個一個地比較大小,注意這裡是字符比較(減號操作符),也就是UNICODE碼值比較,查一下UNICODE代碼表,"張" 的碼值是5F20,"李"是674E,這樣一看,"張" 排在 "李" 前面也就很正確了---但這明顯與我們的意圖沖突了。這一點在JDK的文檔中也有說明:對於非英文的String排序可能會出現不准確的情況,那該如何解決這個問題呢?Java推薦使用collator類進行排序,那好,我們把代碼修改一下:
public class Client59 { public static void main(String[] args) { String[] strs = { "張三(Z)", "李四(L)", "王五(W)" }; //定義一個中文排序器 Comparator c = Collator.getInstance(Locale.CHINA); Arrays.sort(strs,c); int i = 0; for (String str : strs) { System.out.println((++i) + "、" + str); } } }
輸出結果:
1、李四(L)
2、王五(W)
3、張三(Z)
這確實是我們期望的結果,應該不會錯了吧!但是且慢,中國的漢字博大精深,Java是否都能精確的排序呢?最主要的一點是漢字中有象形文字,音形分離,是不是每個漢字都能按照拼音的順序排好呢?我們寫一個復雜的漢字來看看:
1 public class Client59 { 2 public static void main(String[] args) { 3 String[] strs = { "犇(B)", "鑫(X)", "淼(M)" }; 4 //定義一個中文排序器 5 Comparator c = Collator.getInstance(Locale.CHINA); 6 Arrays.sort(strs,c); 7 int i = 0; 8 for (String str : strs) { 9 System.out.println((++i) + "、" + str); 10 } 11 } 12 }
輸出結果如下:
輸出結果又亂了,不要責怪Java,它們已盡量為我們考慮了,只是因為我們的漢字文化太博大精深了,要做好這個排序確實有點為難它,更深層次的原因是Java使用的是UNICODE編碼,而中文UNICODE字符集來源於GB18030的,GB18030又是從GB2312發展起來,GB2312是一個包含了7000多個字符的字符集,它是按照拼音排序,並且是連續的,之後的GBK、GB18030都是在其基礎上擴充而來的,所以要讓它們完整的排序也就難上加難了。
如果排序對象是經常使用的漢字,使用Collator類排序完全可以滿足我們的要求,畢竟GB2312已經包含了大部分的漢字,如果需要嚴格排序,則要使用一些開源項目來自己實現了,比如pinyin4j可以把漢字轉換為拼音,然後我們自己來實現排序算法,不過此時你會發現要考慮的諸如算法、同音字、多音字等眾多問題。
注意:如果排序不是一個關鍵算法,使用Collator類即可。