新版Java為什么要修改substring的實現


 

 

Java字符串的截取操作可以通過substring來完成。有意思的是,這個方法從jdk1.0開始,一直到1.6都沒有變化,但到了1.7實現方式卻發生了改變。你可能會認為之所以要對一個成熟且穩定的方法做修改,一定是因為新的實現更好、效率更高吧?然而正好相反,修改后的substring的效率變低了,並且占用了更多的內存,無論是從時間上還是空間上都比不上原有的實現。下面我們來做一個比較,看看到底哪一個更好,以及為什么新版Java中要對其進行修改。

原有實現

我們首先來看看原來的substring方法。前面是對參數進行檢查,重點是最后一句:

return ((beginIndex == 0) && (endIndex == count)) ? this :
    new String(offset + beginIndex, endIndex - beginIndex, value);

 

這里通過調用下面這個構造方法來創建一個新的字符串:

// 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;
}

 

我們知道,Java的字符串實際上是用一個字符數組來實現的,這個構造方法通過復用字符數組value,省去了數組拷貝的開銷,僅通過3個賦值語句就創建了一個新的字符串對象。從注釋也可以看出這個構造方法的意圖就是為了提升性能。

新的實現

我們再來看看1.7中新的substring實現。前面一堆還是參數檢查,直接看最后一句:

return ((beginIndex == 0) && (endIndex == value.length)) ? this
    : new String(value, beginIndex, subLen);

 

與原來的差不多,但是請注意,這次調用的是另一個構造方法:

public String(char[], int, int)

 

這個公有的構造方法和前面那個很相似(那個是包私有的),從方法簽名上看區別僅僅是參數順序不同。不過這只是表面現象,它們的內部實現卻是完全不同的,這個公有的構造方法不會復用char[]數組,而是將其拷貝到一個新數組,從而創建一個新字符串。

this.value = Arrays.copyOfRange(value, offset, offset+count);

 

對公有的構造方法來說,必須采用這種方式,如果仍然采用復用數組的方法,就會發生安全性問題,別人就可以對字符串中的字符進行任意的修改。后面會對此進行分析。

復用字符數組有沒有安全隱患

Java的字符串是不可變的,原因是作為字符串底層實現的字符數組是私有的,從外面無法訪問。另一方面,String類的每一個可以創建新字符串的公有方法(構造方法、valueOf等),如果其接受一個字符數組作為參數,就會對該數組執行拷貝操作,這就進一步保證了只有String對象才會持有它的字符數組,因此斷絕了從外部修改數組的一切可能。

如果不這么做就會帶來問題,字符串的不可變性也就不復存在了。比如下面這個假想的程序:

char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
String s = new String(0, arr.length, arr); // "hello world"
arr[0] = 'a'; // replace the first character with 'a'
System.out.println(s); // aello world

 

如果構造方法沒有對arr進行拷貝,那么其他人就可以在字符串外部修改該數組,由於它們引用的是同一個數組,因此對arr的修改就相當於修改了字符串。(可以通過反射來真正地實現這個假想的程序)

還有一些方法,比如原來的substring方法,它們沒有進行數組拷貝,而是直接復用另一個字符串的內部數組。這樣做會導致安全問題嗎?答案是不會,因為所有這些方法所執行的操作都是私有操作或包私有操作,屬於內部實現,因此只要不對外暴露這些操作的接口就仍然是安全的。

例如對substring來說,由於無論是原字符串還是新字符串,其value數組本身都是String對象的私有屬性,從外部是無法訪問的,因此對兩個字符串來說都很安全。


為何要修改substring

原來的substring在安全上沒有問題,而且性能很好,又能共享內部數組節約內存。這么看來,好像並沒有什么缺點。那為什么要放棄性能更好的實現方式,而采用性能差很多的數組拷貝的方式呢?難道是Oracle的工程師腦抽才會對substring做出這樣的修改嗎?

當然不是,原來的方法比新的好只是表面現象,因為雖然性能好,但是有一個嚴重的問題,那就是有可能會導致內存泄漏。看一個例子,假設一個方法從某個地方(文件、數據庫或網絡)取得了一個很長的字符串,然后對其進行解析並提取其中的一小段內容,這種情況經常發生在網頁抓取或進行日志分析的時候。下面是示例代碼。

String aLongString = ...; // a very long string
String aPart = data.substring(20, 40);
return aPart;

 

在這里aLongString只是臨時的,真正有用的是aPart,其長度只有20個字符,但是它的內部數組卻是從aLongString那里共享的,因此雖然aLongString本身可以被回收,但它的內部數組卻不能(如下圖)。這就導致了內存泄漏。如果一個程序中這種情況經常發生有可能會導致嚴重的后果,如內存溢出,或性能下降。

 


新的實現雖然損失了性能,而且浪費了一些存儲空間,但卻保證了字符串的內部數組可以和字符串對象一起被回收,從而防止發生內存泄漏,因此新的substring比原來的更健壯。

實際上前面所說的那個包私有的構造方法在1.7中已經被標記為Deprecated,並且實現也修改為直接調用“public String(char[], int, int)”。到了1.8這個構造方法就被刪除了。取而代之的是從1.7開始,增加了另一個共享版的構造方法,這個方法也是包私有的:

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

 

第二個參數目前沒有用到,始終為true,僅僅是為了和另一個公有構造方法“String(char[])”相區別才增加了這么一個參數。這個構造方法用來創建一個和原字符串一模一樣的字符串,而不是像以前一樣可以創建原字符串的一個子串。在這種情況下,共享數組不會導致內存泄漏問題,只是其用處大打折扣,因為只有很少的情況需要創建一個和原字符串一模一樣的字符串,多數情況只需使用原字符串即可。這就像構造方法“String(String)”一樣,應該很少有人會使用它來創建字符串吧。

總結

原來的substring性能好,但在一些情況下卻可能導致嚴重的內存泄漏。新的substring沒有內存泄漏的隱患,因此健壯性更好,但卻是通過犧牲性能換來的。

兩種實現孰優孰劣還真不好說,因為在大多數情況下都不會遇到所謂的嚴重內存泄漏的情況,因此大部分時候新的substring都不如原來的好。但對一個運行庫來說,健壯性可能更重要一些,畢竟它需要適用於任何可能遇到的情況。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM