其他更多java基礎文章:
java基礎學習(目錄)
距離上次寫文章已經(jīng)好一段時間了,主要是工作忙起來,看書的時間就少了,看String的進度就斷斷續(xù)續(xù),在讀源碼的過程中,我搜了幾篇很有學習價值的文章,放在下面,可以在閱讀完本文之后閱讀一下,有些地方我可能講的不夠清楚,下面文章里的大神講的更仔細。
學習資料:
String類API中文
深入解析String#intern
Java 中new String("字面量") 中 "字面量" 是何時進入字符串常量池的?
new一個String對象的時候,如果常量池沒有相應的字面量真的會去它那里創(chuàng)建一個嗎?我表示懷疑。
String的方法
String的底層是由char數(shù)組構成的
private final char value[];
由于底層char數(shù)組是final的,所以String對象是不可變的。
String的構造方法
我們先講一下主要的幾種構造方法:
1. 參數(shù)為String類型
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
這里將直接將源 String 中的 value 和 hash 兩個屬性直接賦值給目標 String。因為 String 一旦定義之后是不可以改變的,所以也就不用擔心改變源 String 的值會影響到目標 String 的值。
2. 參數(shù)為字符數(shù)組
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count)
這里值得注意的是:當我們使用字符數(shù)組創(chuàng)建 String 的時候,會用到 Arrays.copyOf 方法或 Arrays.copyOfRange 方法。這兩個方法是將原有的字符數(shù)組中的內(nèi)容逐一的復制到 String 中的字符數(shù)組中。會創(chuàng)建一個新的字符串對象,隨后修改的字符數(shù)組不影響新創(chuàng)建的字符串。
3.參數(shù)為字節(jié)數(shù)組
在 Java 中,String 實例中保存有一個 char[] 字符數(shù)組,char[] 字符數(shù)組是以 unicode 碼來存儲的,String 和 char 為內(nèi)存形式。
byte 是網(wǎng)絡傳輸或存儲的序列化形式,所以在很多傳輸和存儲的過程中需要將 byte[] 數(shù)組和 String 進行相互轉化。所以 String 提供了一系列重載的構造方法來將一個字符數(shù)組轉化成 String,提到 byte[] 和 String 之間的相互轉換就不得不關注編碼問題。
String(byte[] bytes, Charset charset)
該構造方法是指通過 charset 來解碼指定的 byte 數(shù)組,將其解碼成 unicode 的 char[] 數(shù)組,構造成新的 String。
這里的 bytes 字節(jié)流是使用 charset 進行編碼的,想要將他轉換成 unicode 的 char[] 數(shù)組,而又保證不出現(xiàn)亂碼,那就要指定其解碼方式
同樣的,使用字節(jié)數(shù)組來構造 String 也有很多種形式,按照是否指定解碼方式分的話可以分為兩種:
public String(byte bytes[]){
this(bytes, 0, bytes.length);
}
public String(byte bytes[], int offset, int length){
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
如果我們在使用 byte[] 構造 String 的時候,使用的是下面這四種構造方法(帶有 charsetName 或者 charset 參數(shù))的一種的話,那么就會使用 StringCoding.decode 方法進行解碼,使用的解碼的字符集就是我們指定的 charsetName 或者 charset。
String(byte bytes[])
String(byte bytes[], int offset, int length)
String(byte bytes[], Charset charset)
String(byte bytes[], String charsetName)
String(byte bytes[], int offset, int length, Charset charset)
String(byte bytes[], int offset, int length, String charsetName)
我們在使用 byte[] 構造 String 的時候,如果沒有指明解碼使用的字符集的話,那么 StringCoding 的 decode 方法首先調用系統(tǒng)的默認編碼格式,如果沒有指定編碼格式則默認使用 ISO-8859-1 編碼格式進行編碼操作。主要體現(xiàn)代碼如下:
static char[] decode(byte[] ba, int off, int len){
String csn = Charset.defaultCharset().name();
try{ //use char set name decode() variant which provide scaching.
return decode(csn, ba, off, len);
} catch(UnsupportedEncodingException x){
warnUnsupportedCharset(csn);
}
try{
return decode("ISO-8859-1", ba, off, len); }
catch(UnsupportedEncodingException x){
//If this code is hit during VM initiali zation, MessageUtils is the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 char set not available: " + x.toString());
// If we can not find ISO-8859-1 (are quired encoding) then things are seriously wrong with the installation.
System.exit(1);
return null;
}
}
4.參數(shù)為StringBuilder或StringBuffer
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
基本不用,用StringBuffer.toString方法。
4. 特殊的protected構造方法
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
從代碼中我們可以看出,該方法和 String(char[] value) 有兩點區(qū)別:
第一個區(qū)別:該方法多了一個參數(shù):boolean share,其實這個參數(shù)在方法體中根本沒被使用。注釋說目前不支持 false,只使用 true。那可以斷定,加入這個 share 的只是為了區(qū)分于 String(char[] value) 方法,不加這個參數(shù)就沒辦法定義這個函數(shù),只有參數(shù)是不同才能進行重載。
第二個區(qū)別:具體的方法實現(xiàn)不同。我們前面提到過 String(char[] value) 方法在創(chuàng)建 String 的時候會用到 Arrays 的 copyOf 方法將 value 中的內(nèi)容逐一復制到 String 當中,而這個 String(char[] value, boolean share) 方法則是直接將 value 的引用賦值給 String 的 value。那么也就是說,這個方法構造出來的 String 和參數(shù)傳過來的 char[] value 共享同一個數(shù)組。
為什么 Java 會提供這樣一個方法呢?
性能好:這個很簡單,一個是直接給數(shù)組賦值(相當于直接將 String 的 value 的指針指向char[]數(shù)組),一個是逐一拷貝,當然是直接賦值快了。
節(jié)約內(nèi)存:該方法之所以設置為 protected,是因為一旦該方法設置為公有,在外面可以訪問的話,如果構造方法沒有對 arr 進行拷貝,那么其他人就可以在字符串外部修改該數(shù)組,由于它們引用的是同一個數(shù)組,因此對 arr 的修改就相當于修改了字符串,那就破壞了字符串的不可變性。
安全的:對于調用他的方法來說,由于無論是原字符串還是新字符串,其 value 數(shù)組本身都是 String 對象的私有屬性,從外部是無法訪問的,因此對兩個字符串來說都很安全。
在 Java 7 之前有很多 String 里面的方法都使用上面說的那種“性能好的、節(jié)約內(nèi)存的、安全”的構造函數(shù)。
比如:substring
replace
concat
valueOf
等方法,實際上它們使用的是 public String(char[], ture) 方法來實現(xiàn)。
但是在 Java 7 中,substring 已經(jīng)不再使用這種“優(yōu)秀”的方法了
public String substring(int beginIndex, int endIndex){
if(beginIndex < 0){
throw new StringIndexOutOfBoundsException(beginIndex);
}
if(endIndex > value.length){
throw new StringIndexOutOfBoundsException(endIndex);
}
intsubLen = endIndex-beginIndex;
if(subLen < 0){
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this : newString(value, beginIndex, subLen);
}
為什么呢?
雖然這種方法有很多優(yōu)點,但是他有一個致命的缺點,對于 sun 公司的程序員來說是一個零容忍的 bug,那就是他很有可能造成內(nèi)存泄露。
看一個例子,假設一個方法從某個地方(文件、數(shù)據(jù)庫或網(wǎng)絡)取得了一個很長的字符串,然后對其進行解析并提取其中的一小段內(nèi)容,這種情況經(jīng)常發(fā)生在網(wǎng)頁抓取或進行日志分析的時候。
下面是示例代碼:
String aLongString = "...averylongstring...";
String aPart = data.substring(20, 40);
return aPart;
在這里 aLongString 只是臨時的,真正有用的是 aPart,其長度只有 20 個字符,但是它的內(nèi)部數(shù)組卻是從 aLongString 那里共享的,因此雖然 aLongString 本身可以被回收,但它的內(nèi)部數(shù)組卻不能釋放。這就導致了內(nèi)存泄漏。如果一個程序中這種情況經(jīng)常發(fā)生有可能會導致嚴重的后果,如內(nèi)存溢出,或性能下降。
新的實現(xiàn)雖然損失了性能,而且浪費了一些存儲空間,但卻保證了字符串的內(nèi)部數(shù)組可以和字符串對象一起被回收,從而防止發(fā)生內(nèi)存泄漏,因此新的 substring 比原來的更健壯。
其他方法
length() 返回字符串長度
isEmpty() 返回字符串是否為空
charAt(int index) 返回字符串中第(index+1)個字符(數(shù)組索引)
char[] toCharArray() 轉化成字符數(shù)組
trim()去掉兩端空格
toUpperCase()轉化為大寫
toLowerCase()轉化為小寫
boolean matches(String regex) 判斷字符串是否匹配給定的regex正則表達式
boolean contains(CharSequence s) 判斷字符串是否包含字符序列 s
String[] split(String regex, int limit) 按照字符 regex將字符串分成 limit 份
String[] split(String regex) 按照字符 regex 將字符串分段
詳細可查看String類API中文翻譯
需要注意
String concat(String str) 拼接字符串
String replace(char oldChar, char newChar) 將字符串中的
oldChar 字符換成 newChar 字符
以上兩個方法都使用了 String(char[] value, boolean share) concat 方法和 replace 方法,他們不會導致元數(shù)組中有大量空間不被使用,因為他們一個是拼接字符串,一個是替換字符串內(nèi)容,不會將字符數(shù)組的長度變得很短,所以使用了共享的 char[] 字符數(shù)組來優(yōu)化。
getBytes
在創(chuàng)建 String 的時候,可以使用 byte[] 數(shù)組,將一個字節(jié)數(shù)組轉換成字符串,同樣,我們可以將一個字符串轉換成字節(jié)數(shù)組,那么 String 提供了很多重載的 getBytes 方法。
public byte[] getBytes(){
return StringCoding.encode(value, 0, value.length);
}
但是,值得注意的是,在使用這些方法的時候一定要注意編碼問題。比如:
String s = "你好,世界!"; byte[] bytes = s.getBytes();
這段代碼在不同的平臺上運行得到結果是不一樣的。由于沒有指定編碼方式,所以在該方法對字符串進行編碼的時候就會使用系統(tǒng)的默認編碼方式。
在中文操作系統(tǒng)中可能會使用 GBK 或者 GB2312 進行編碼,在英文操作系統(tǒng)中有可能使用 iso-8859-1 進行編碼。這樣寫出來的代碼就和機器環(huán)境有很強的關聯(lián)性了,為了避免不必要的麻煩,要指定編碼方式。
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException{
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
比較方法
boolean equals(Object anObject); 比較對象
boolean contentEquals(String Buffersb); 與字符串比較內(nèi)容
boolean contentEquals(Char Sequencecs); 與字符比較內(nèi)容
boolean equalsIgnoreCase(String anotherString);忽略大小寫比較字符串對象
int compareTo(String anotherString); 比較字符串
int compareToIgnoreCase(String str); 忽略大小寫比較字符串
boolean regionMatches(int toffset, String other, int ooffset, int len)局部匹配
boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 可忽略大小寫局部匹配
字符串有一系列方法用于比較兩個字符串的關系。 前四個返回 boolean 的方法很容易理解,前三個比較就是比較 String 和要比較的目標對象的字符數(shù)組的內(nèi)容,一樣就返回 true, 不一樣就返回false,核心代碼如下:
int n = value.length;
while (n-- ! = 0) {
if (v1[i] != v2[i])
return false;
i++;
}
v1 v2 分別代表 String 的字符數(shù)組和目標對象的字符數(shù)組。 第四個和前三個唯一的區(qū)別就是他會將兩個字符數(shù)組的內(nèi)容都使用 toUpperCase 方法轉換成大寫再進行比較,以此來忽略大小寫進行比較。相同則返回 true,不想同則返回 false
equals方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
通過源碼的代碼,我們可以了解它比較的流程:字符串相同:地址相同;地址不同,但是內(nèi)容相同
這是一種提高效率的方法,也就是將比較快速的部分(地址,比較對象類型)放在前面比較,速度慢的部分(比較字符數(shù)組)放在后面執(zhí)行。
StringBuffer 需要考慮線程安全問題,加鎖之后再調用
contentEquals()方法
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
public boolean contentEquals(StringBuffer sb);
實際調用了contentEquals(CharSequence cs)
方法;
AbstractStringBuilder和String都是接口CharSequence的實現(xiàn),通過判斷輸入是AbstractStringBuilder還是String的實例,執(zhí)行不同的方法;
下面這個是 equalsIgnoreCase 代碼的實現(xiàn):
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true : (anotherString != null) && (anotherString.value.length == value.length) && regionMatches(true, 0, anotherString, 0, value.length);
}
看到這段代碼,眼前為之一亮。使用一個三目運算符和 && 操作代替了多個 if 語句。
Hashcode()方法
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
hashCode 的實現(xiàn)其實就是使用數(shù)學公式:
[圖片上傳失敗...(image-e09e8e-1544065735387)]
為什么要使用這個公式,就是在存儲數(shù)據(jù)計算 hash 地址的時候,我們希望盡量減少有同樣的 hash 地址。如果使用相同 hash 地址的數(shù)據(jù)過多,那么這些數(shù)據(jù)所組成的 hash 鏈就更長,從而降低了查詢效率。
所以在選擇系數(shù)的時候要選擇盡量長的系數(shù)并且讓乘法盡量不要溢出的系數(shù),因為如果計算出來的 hash 地址越大,所謂的“沖突”就越少,查找起來效率也會提高。
選擇31作為因子的原因: 為什么 String hashCode 方法選擇數(shù)字31作為乘子
substring
前面我們介紹過,java 7 中的 substring 方法使用
String(value, beginIndex, subLen)
方法創(chuàng)建一個新的 String 并返回,這個方法會將原來的 char[] 中的值逐一復制到新的 String 中,兩個數(shù)組并不是共享的,雖然這樣做損失一些性能,但是有效地避免了內(nèi)存泄露。
replaceFirst、replaceAll、replace區(qū)別
String replaceFirst(String regex, String replacement)
String replaceAll(String regex, String replacement)
String replace(Char Sequencetarget, Char Sequencereplacement)
public String replace(char oldChar, char newChar){
if(oldChar != newChar){
int len = value.length;
int i = -1;
char[] val = value; /*avoid get field opcode*/
while (++i < len){
if (val[i] == oldChar){
break;
}
}
if( i < len ){
char buf[] = new char[len];
for (intj=0; j<i; j++){
buf[j] = val[j];
}
while (i < len){
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf,true);
}
}
return this;
}
replace 的參數(shù)可以是 char 或者 CharSequence,即可以支持字符的替換, 也支持字符串的替換。當參數(shù)為CharSequence時,實際調用的是replaceAll方法,所以replace方法是全部替換。
replaceAll 和 replaceFirst 的參數(shù)是 regex,即基于規(guī)則表達式的替換。區(qū)別是一個全部替換,一個只替換第一個。
intern()方法
public native String intern();
intern方法是Native調用,它的作用是在方法區(qū)中的常量池里尋找等值的對象,如果沒有找到則在常量池中存放當前字符串的引用并返回該引用,否則直接返回常量池中已存在的String對象引用。