Java字元串String 我們知道Java的字元竄是Immutable(不可變)的,一旦創建就不能更改其內容了;平常我們對字元串的操作是最多的,其實對字元串的操作,返回的字元串都是新建的字元串對象,原來並沒有被改動,這跟C 是一模一樣的; 既然字元串是不可變數,當我們對字元串進行各種操作時的效率肯 ...
Java字元串String
我們知道Java的字元竄是Immutable(不可變)的,一旦創建就不能更改其內容了;平常我們對字元串的操作是最多的,其實對字元串的操作,返回的字元串都是新建的字元串對象,原來並沒有被改動,這跟C#是一模一樣的;
既然字元串是不可變數,當我們對字元串進行各種操作時的效率肯定是有影響的,比如我們平時最常用的 + 運算符:
public class ConcatString{
public static void main(String[] args) {
var name = "marson";
var s = "abc" + name + "shine" + 47+ "nancy" + "summer zhu";
print(s);
}
}
這段代碼我相信在我們日常開發中很容易遇見,它這裡還沒開始相加,就開闢了6段字元串對象,然後+起來又形成新的String對象,所以可以想象,當我們遇到大量(長度未可知且預知高於一定值的)字元串拼接時,會產生多少新的對象,對記憶體,性能造成不小的影響。
所以這時候就有了StringBuilder
StringBuilder
StringBuilder的目的就是為瞭解決String的不變數的問題的,StringBuilder在內部維護初始容量為16(可動態擴展)的對象,它是一個變數,所以它append字元串時返回的對象是同一個。所以存在大量的字元串拼接時,StringBuilder是可以明顯優於String;
在JAVA SE5前,StringBuffer充當StringBuilder的角色,但是StringBuffer是線程安全的,細扣源碼就會發現,裡面含有大量的關鍵字synchronized,所以性能開銷也比較大。
下麵是StringBuilder的demo
public void UsingStringBuilder(){
var sb = new StringBuilder();
sb.append("abc").append("marson").append("shine")
.append("summer").append("zhu");
System.out.println(sb);
}
StringBuilder隱藏的陷阱
下麵我們來學習《Java編程思想》一書中提到的一種StringBuilder場景,貼下代碼:
public class InfiniteRecursion {
@Override
public String toString() {
return "InfiniteRecursion address: " + this + "\n";
}
public static void main(String[] args) {
var v = new ArrayList<InfiniteRecursion>();
for (int i = 0; i < 10; i++) {
v.add(new InfiniteRecursion());
}
System.out.println(v);
}
}
這種情況稍微不註意,就會犯下上面這段代碼一樣的錯誤——StackOverflowError
這是由於無限遞歸導致的堆棧記憶體溢出的錯誤,因為InfinitialRecursion類覆寫了toString,並且返回一個字元串+拼接操作符。儘管拼接的對象是this對象,但是由於是字元串的拼接,所以jvm會自動轉型為String類型,從而再次調用toString,最後導致錯誤出現。
關於字元串池——intern
Java關於字元串對象,其實有一個裝載字元串的容器——字元串池(pool of strings),新建的String對象,只要池中不存在,那麼就可以存進去,並生成唯一個引用,當我們新建一個內容一樣的字元串內容,我們可以直接引用池中的字元串對象,進而減小新建字元串帶來的開銷提高應用程式性能,而String的實例方法intern就是這個作用:
public class StringIntern {
public static void main(String[] args) {
var s = "MarsonShine";
var ss = new String("MarsonShine");
var sss = ss.intern();
System.out.println("s == ss: " + (s == ss));// false
System.out.println("s == sss: "+(s == sss));// true
System.out.println("ss == sss: "+(ss == sss));// false
}
}
String VS StringBuilder
最後我們來比較一下String與StringBuilder拼接字元串的性能對比來結束我們這個話題
public class StringVsStringBuilder {
private static final String INIT_STRING = "abcdefghijklmn1234567890";
public static void main(String[] args) {
var sw = new Stopwatch();
sw.start();
var str = "";
for (int i = 0; i < 100000; i++) {
str += INIT_STRING;
}
sw.end();
System.out.println("String + 運行時間:" + sw.ElapsedMilliseconds() + " ms");
sw.restart();
var sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(INIT_STRING);
}
sw.end();
System.out.println("StringBuilder append 運行時間:" + sw.ElapsedMilliseconds() + " ms");
}
}
這個類裡面分別用String,StringBuilder對定長的字元串對象INIT_STRING多次拼接
測試結果肯定也如大家所料,後者時間要遠遠小於前者的。但是當拼接的字元串比較少時,其差別就微乎其微了,理論上在少量字元串的拼接過程中,StringBuilder的性能是要遜色於String的,但是在我電腦上經過大量的測試,發現StringBuilder的性能始終要強與String的,我都有些懷疑是不是我Stopwatch輔助類寫錯了 - -;
最後我來把這個段代碼附上吧
package performance;
public class Stopwatch {
private long startTime;
private long endTime;
public void start(){
startTime = System.currentTimeMillis();
}
public void end(){
endTime = System.currentTimeMillis();
}
public void restart(){
startTime = System.currentTimeMillis();
}
public long ElapsedMilliseconds(){
return endTime - startTime;
}
}
後記
因為我想弄清楚Java中的+操作符實際上是怎麼調用的,運行的過程是怎麼樣的,是不是跟C#一樣調用的是concat方法?
後來我通過反編譯java代碼發現string的+操作符在JVM變成了動態指令調用:
invokedynamic 指令去調用java.lang.invoke.makeConcatWithConstants方法,然後根據MethodHandler以及MethodType信息生成CallSite信息去執行具體的函數,但就CallSite調用那個過程我沒搞清楚,調試斷點也摸不清楚(Idiea玩不轉 - -)
有瞭解的同學希望告訴我下^_^