一、字符串連接的效率問題
使用String連接字符串時為什么慢?
小知識點
java中對數組進行初始化后,該數組所占的內存空間、數組長度都是不可變的。
創建一個字符串,為字符串對象分配內存空間,會耗費掉一定的時間(CPU)與空間(內存)代價,作為最基礎的數據類型,大量頻繁的創建字符串,極大程度地影響程序的性能。
過多無用的中間對象
每次連接字符串時都會創建一個新的String對象,隨着拼接次數的增多,這個對象會越來越大。 如,進行100次拼接需要創建100個String對象才能夠達到目的。
StringBuilder在連接時為什么效率更高?
字符數組的擴容機制:
private void ensureCapacityInternal(int minimumCapacity) { // 最小所需容量minimumCapacity是否比原數組長度要長 // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } private int newCapacity(int minCapacity) { // 計算擴容之后的容量newCapacity // overflow-conscious code int newCapacity = (value.length << 1) + 2; // 擴容后還小於所需的最小容量 if (newCapacity - minCapacity < 0) { // 設置新容量為最小所需容量minimumCapacity newCapacity = minCapacity; } // newCapacity是否溢出,newCapacity是否比數組所能分配的最大容量 MAX_ARRAY_SIZE 還要大。 return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; } private int hugeCapacity(int minCapacity) { // 最小所需容量minCapacity大於Integer.MAX_VALUE時拋出內存溢出異常 if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } // 如果minCapacity介於MAX_ARRAY_SIZE和Integer.MAX_VALUE之間,則新的容量為minCapacity,否則直接使用MAX_ARRAY_SIZE作為新的容量。 return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }
向原StringBuilder對象中追加字符串時:
1.追加對象str為null時追加'null'字符
2.確認是否需要進行擴容操作
- 最小所需容量minimumCapacity是否比原數組長度要長,即當原數組長度不能滿足所需最小容量時進行擴容操作。
- 計算擴容之后的容量newCapacity,newCapacity = (value.length * 2) + 2。
- 擴容后是否還小於所需的最小容量,如果小於則直接設置新容量為最小所需容量minimumCapacity。
- newCapacity是否溢出,newCapacity是否比數組所能分配的最大容量 MAX_ARRAY_SIZE 還要大。如果是的話則判斷,最小所需容量minCapacity大於Integer.MAX_VALUE時拋出內存溢出異常,如果minCapacity介於MAX_ARRAY_SIZE和Integer.MAX_VALUE之間,則新的容量為minCapacity,否則直接使用MAX_ARRAY_SIZE作為新的容量。
3.str.getChars()將str追加到value的末尾
效率高的原因
- 擴容機制保證了,只有在滿足擴容條件
minimumCapacity - value.length > 0
時才會進行擴容生成新的數組,所以大部分情況都是在對原數組進行操作,避免了產生過多的無用char[]對象,節省了系統資源的開銷。
代碼
/** * 比較字符串連接速度 * * @Author: lingyejun * @Date: 2019/8/17 * @Describe: * @Modified By: */ public class LinkCompare { /** * 原始字符串連接 * * @param times */ public static void linkByString(int times) { Long startTime = System.currentTimeMillis(); String initStr = ""; for (int i = 0; i < times; i++) { initStr = initStr + i; } Long endTime = System.currentTimeMillis(); System.out.println("String 連接 " + times + " 次 消耗:" + (endTime - startTime) + "ms"); } /** * 使用StringBuilder連接字符串 * * @param times */ public static void linkByStringBuilder(int times) { Long startTime = System.currentTimeMillis(); StringBuilder initStr = new StringBuilder(); for (int i = 0; i < times; i++) { initStr.append(i); } Long endTime = System.currentTimeMillis(); System.out.println("StringBuilder 連接 " + times + " 次 消耗:" + (endTime - startTime) + "ms"); } /** * 使用StringBuffer連接字符串 * * @param times */ public static void linkByStringBuffer(int times) { Long startTime = System.currentTimeMillis(); StringBuffer initStr = new StringBuffer(); for (int i = 0; i < times; i++) { initStr.append(i); } Long endTime = System.currentTimeMillis(); System.out.println("StringBuffer 連接 " + times + " 次 消耗:" + (endTime - startTime) + "ms"); } public static void main(String[] args) { // 100000000 linkByStringBuilder(40000); //-XX:+PrintGCDetails //linkByString(40000); } }
二、StringBuilder和String Buffer的線程安全比較
驗證StringBuffer的線程安全性
線程不安全的原因
public StringBuilder append(String str) { super.append(str); return this; } public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
測試代碼
import java.util.ArrayList; import java.util.List; /** * StringBuilder和StringBuffer的並發測驗 * * @Author: lingyejun * @Date: 2019/8/17 * @Describe: * @Modified By: */ public class SecurityCompare { public void stringBuilderTest() { // 初始化StringBuilder StringBuilder stringBuilder = new StringBuilder(); // joinList List<StringBuilderThread> joinList = new ArrayList<>(); // 模擬並發場景 for (int i = 0; i < 1000; i++) { StringBuilderThread sbt = new StringBuilderThread(stringBuilder); sbt.start(); joinList.add(sbt); } // 等待append線程執行完畢后再執行主線程 for (StringBuilderThread thread : joinList) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } // 打印最終的結果 System.out.println("StringBuilder 並發append的結果: " + stringBuilder.length()); } public void stringBufferTest() { // 初始化StringBuffer StringBuffer stringBuffer = new StringBuffer(); // joinList List<StringBufferThread> joinList = new ArrayList<>(); // 模擬並發場景 for (int i = 0; i < 1000; i++) { StringBufferThread sbf = new StringBufferThread(stringBuffer); sbf.start(); joinList.add(sbf); } // 等待append線程執行完畢后再執行主線程 for (StringBufferThread thread : joinList) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } // 打印最終的結果 System.out.println("StringBuffer 並發append的結果: " + stringBuffer.length()); } public static void main(String[] args) { SecurityCompare securityCompare = new SecurityCompare(); securityCompare.stringBuilderTest(); securityCompare.stringBufferTest(); } public static class StringBuilderThread extends Thread { private StringBuilder stringBuilder; public StringBuilderThread(StringBuilder stringBuilder) { this.stringBuilder = stringBuilder; } @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } stringBuilder.append("a"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } private static class StringBufferThread extends Thread { private StringBuffer stringBuffer; public StringBufferThread(StringBuffer stringBuffer) { this.stringBuffer = stringBuffer; } @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } stringBuffer.append("a"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
三、結論
- String為固定長度的字符串,StringBuilder和StringBuffer為變長字符串。
- StringBuffer是線程安全的,StringBuilder是非線程安全的。
- StringBuilder和StringBuffer的默認初始容量是16,可以提前預估好字符串的長度,進一步減少擴容帶來的額外開銷。