針對 Java 程序編寫過程當中的實際問題,本文分爲兩部分,首先對字符串相關操做、數據切分、處理超大 String 對象等提出解決方案及優化建議,並給出具體代碼示例;而後對數據定義、運算邏輯優化等方面提出解決方案及優化建議,並給出具體代碼示例。 因爲本文所嘗試的實驗都是基於聯想 L430 筆記本,i5-3320CPU,4GB 內存基礎上的,其餘機器上運行代碼可能結果有所不一樣,請以本身的實驗環境爲準。java
字符串對象正則表達式
字符串對象或者其等價對象 (如 char 數組),在內存中老是佔據最大的空間塊,所以如何高效地處理字符串,是提升系統總體性能的關鍵。算法
String 對象能夠認爲是 char 數組的延伸和進一步封裝,它主要由 3 部分組成:char 數組、偏移量和 String 的長度。char 數組表示 String 的內容,它是 String 對象所表示字符串的超集。String 的真實內容還須要由偏移量和長度在這個 char 數組中進行定位和截取。數組
String 有 3 個基本特色:安全
1. 不變性;多線程
2. 針對常量池的優化;app
3. 類的 final 定義。函數
不 變性指的是 String 對象一旦生成,則不能再對它進行改變。String 的這個特性能夠泛化成不變 (immutable) 模式,即一個對象的狀態在對象被建立以後就再也不發生變化。不變模式的主要做用在於當一個對象須要被多線程共享,而且訪問頻繁時,能夠省略同步和鎖等待的時 間,從而大幅提升系統性能。性能
針對常量池的優化指的是當兩個 String 對象擁有相同的值時,它們只引用常量池中的同一個拷貝,當同一個字符串反覆出現時,這個技術能夠大幅度節省內存空間。優化
下面代碼 str一、str二、str4 引用了相同的地址,可是 str3 卻從新開闢了一塊內存空間,雖然 str3 單獨佔用了堆空間,可是它所指向的實體和 str1 徹底同樣。代碼以下清單 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 所示。
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 方法能夠確保正常運行。
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 所示。
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 類來處理字符串時性能的差距。
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 所示:
39015 16 15
當一個 StringTokenizer 對象生成後,經過它的 nextToken() 方法即可以獲得下一個分割的字符串,經過 hasMoreToken 方法能夠知道是否有更多的字符串須要處理。對比發現 split 的耗時很是的長,採用 StringTokenizer 對象處理速度很快。咱們嘗試本身實現字符串分割算法,使用 substring 方法和 indexOf 方法組合而成的字符串分割算法能夠幫助很快切分字符串並替換內容。
因爲 String 是不可變對象,所以,在須要對字符串進行修改操做時 (如字符串鏈接、替換),String 對象會生成新的對象,因此其性能相對較差。可是 JVM 會對代碼進行完全的優化,將多個鏈接操做的字符串在編譯時合成一個單獨的長字符串。
以 上實例運行結果差別較大的緣由是 split 算法對每個字符進行了對比,這樣當字符串較大時,須要把整個字符串讀入內存,逐一查找,找到符合條件的字符,這樣作較爲耗時。而 StringTokenizer 類容許一個應用程序進入一個令牌(tokens),StringTokenizer 類的對象在內部已經標識化的字符串中維持了當前位置。一些操做使得在現有位置上的字符串提早獲得處理。 一個令牌的值是由得到其曾經建立 StringTokenizer 類對象的字串所返回的。
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 所示。
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 所示。
375 187 0
雖然第一種方法編譯器判斷 String 的加法運行成 StringBuilder 實現,可是編譯器沒有作出足夠聰明的判斷,每次循環都生成了新的 StringBuilder 實例從而大大下降了系統性能。
StringBuffer 和 StringBuilder 都實現了 AbstractStringBuilder 抽象類,擁有幾乎相同的對外借口,二者的最大不一樣在於 StringBuffer 對幾乎全部的方法都作了同步,而 StringBuilder 並無任何同步。因爲方法同步須要消耗必定的系統資源,所以,StringBuilder 的效率也好於 StringBuffer。 可是,在多線程系統中,StringBuilder 沒法保證線程安全,不能使用。代碼如清單 10 所示。
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 所示。
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 數組,而後將原數組中的內容複製到這個新的數組中。所以,對於大對象的擴容會涉及大量的內存複製操做。若是可以預先評估大小,會提升性能。