1, substring截取超大字符串可能造成的“內存洩漏”
2,+ 操作符的優化和局限
3,StringBuilder和StringBuffer
4,split和StringTokenizer做簡單字符分割效率的比較
我們知道,String對象內保存著一個char數組。但是char數組未必和String所代表的字符集等長,而可能是一個“超集”。String有一個私有的構造函數:
// Package private constructor which shares value array for speed. String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }
這個構造函數允許你只使用value[]的一部分作為String的字符集,它並不會截取value[]的一部分來創建一個新的char數組,而是把它整個保存起來了。
接著來看substring函數的實現:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
substring正是用我們上面提到的構造函數來構造返回的String的,Java這麼做有利有弊:
1)如果我們要從一個大字符串中截取許多小字符串,那麼這些小字符串共享一個大的char[]。那麼,這麼做是非常高效的,避免了重新分配內存的時間空間開銷。
2)但是,如果我們只從中截取一個或少數幾個很小的字符串,原String將丟棄,而這些小字符串卻被長期保存,這樣我們就造成了某種意義上的內存洩漏 -- 我們以為原String的內存被GC釋放了,然而並沒有,它的主要部分 — 巨大的char數組仍被他的子String引用著,雖然只有其中很小的一部分被它們使用了。
對於這種洩漏,解決辦法很簡單,使用以下語法
str2 = new String(str1.substring(5,100));
構造函數String(String)會為新的String創建一個新的char[]。但是前提是,我們意識到了substring可能導致的問題。
我們知道,對於以下語法:
str1 += "abc"; str1 = str1 + "abc";
Java將創建一個新的String對象和字符串數組,把原字符串和”abc”拷貝拼接到新的字符串數組中。如果反復進行這樣字符串的累加操作,自然是非常低效的,這種情況按照最佳實踐,應該使用StringBuilder。
但事實上,Java已經對+操作進行了優化。看下面的代碼:
String temp = "ABC" + 200 + 'D';
編譯器已經把該代碼優化編譯成了:
String temp = new StringBuilder().append( "ABC" ).append( 200 ).append('D').toString();
(注:
另外,如果代碼簡單的多個字符串相加:
String temp = "Hello" + “ ” + “World”;
編譯器直接優化為
String temp = "Hello World”;
)
所以,連續累加效率並不比使用StringBuilder效率差,因為它本來就是用一個StringBuilder對象連續的append來實現的。
但是,如果是:
for(int i=0; i<100; i++) { temp+="abc"; }
編譯器並沒有辦法把以上for循環裡面多次迭代的‘+’操作優化為只使用一個StringBuilder對象的連續append操作。因此,還是非常低效的。
簡而言之,如果所有的字符串拼接可以在一行裡面用‘+’完成,那麼是沒有效率問題的;否則,最好使用StringBuilder。
StringBuilder和StringBuffer用法基本沒什麼區別,但是StringBuilder不是線程安全的,StringBuffer是線程安全的。StringBuffer在所有用於字符操作的public方法都加了鎖--使用了synchronized關鍵字。
我們來測試一下單線程下StringBuilder和StringBuffer的效率,以下代碼:
public static void main(String[] args){ long t1 = System.nanoTime(); StringBuffer stringBuffer = new StringBuffer(); for(int i=0; i<1000000; i++) { stringBuffer.append("a"); } stringBuffer.toString(); long t2 = System.nanoTime(); System.out.println("StringBuffer :"+ (t2-t1)); t1 = System.nanoTime(); StringBuilder stringBuilder = new StringBuilder(); for(int i=0; i<1000000; i++) { stringBuilder.append("a"); } stringBuilder.toString(); t2 = System.nanoTime(); System.out.println("StringBuilder:"+ (t2-t1)); }
結果:
StringBuffer :33979818 StringBuilder:14061978
單線程情況下,StringBuilder要快一倍多。
那多線程情況StringBuffer效率如何呢?下面代碼測試:
long t1 = System.nanoTime(); final StringBuffer stringBuffer = new StringBuffer(); ExecutorService executor = Executors.newFixedThreadPool(3); CountDownLatch countDownLatch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { executor.execute(new Runnable() { @Override public void run() { for (int i = 0; i < 333333; i++) { stringBuffer.append("a"); } } }); countDownLatch.countDown(); } stringBuffer.toString(); countDownLatch.await(); long t2 = System.nanoTime(); System.out.println("StringBuffer :"+ (t2-t1));
結果:
StringBuffer :2603076
雖然我們使用了3個工作線程,但是效率幾乎比單線程沒有什麼提升,這就是使用鎖在多線程的結果--鎖在多線程中的協調,導致線程的頻繁切換,大大降低效率。
雖然我實在不知道有什麼場景需要用到多線程的字符串拼裝。假設有,並且對性能有很嚴格的要求,我覺得可以考慮使用一些無鎖的多線程編程框架,例如Disruptor--一個無鎖的RingBuffer框架,使用多個生產者線程往Ring buffer中投遞String對象,在消費者中用StringBuilder進行組裝。(類似log4j 2的異步日志處理)
很多文章都說split比StringTokenizer效率高很多,開始也深以為然,但是卻發現它們的測試代碼都存在很嚴重的問題。自己做了一下測試
StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < 1000000; i++) { stringBuilder.append(i); stringBuilder.append(","); } String str = stringBuilder.toString(); long t1 = System.nanoTime(); String[] strArray = str.split(","); long t2 = System.nanoTime(); System.out.println("split :" + (t2 - t1)); String str1 = stringBuilder.toString(); t1 = System.nanoTime(); StringTokenizer stringTokenizer = new StringTokenizer(str1, ","); //List<String> strList = new ArrayList<String>(1000000); //或者 String[] strArray1 = new String[stringTokenizer.countTokens()]; for (int i = 0; i < 1000000; i++) { String subStr = stringTokenizer.nextToken(); //strList.add(subStr); //或者strArray1[i] =subStr; } t2 = System.nanoTime(); System.out.println("token :" + (t2 - t1));
結果:
split :248539389 token :53191452
StringTokenizer 比split快4倍。
但是上面的比較在某些情況下並不公平,split會返回一個數組,而StringTokenizer 的next方法只能逐個浏覽token。如果要求StringTokenizer 也把返回的子字符串保存在List中,那麼結果如何呢?把上面代碼段中的注釋掉的代碼打開,使StringTokenizer 也要把tokens保存在List或Array中,再進行測試。
結果:
split :254496592 token :303926083
這種情況下StringTokenizer 的效率還差一些。因此,不能一概而論split或StringTokenizer 誰的效率高,還要看如果使用。如果需要把結果放在Array或List當中,split更簡單還有效率。(可見2種算法效率並沒有本質差別,差就差在Array或List的使用上,具體還要從JDK的源代碼去分析)