Java 源碼分析 — String 的設(shè)計

Tip:筆者馬上畢業(yè)了俺祠,準備開始 Java 的進階學(xué)習計劃绞惦。于是打算先從 String 類的源碼分析入手逼纸,作為后面學(xué)習的案例。這篇文章寄托著今后進階系列產(chǎn)出的愿望济蝉,希望能堅持下去樊展,不忘初心,讓自己保持那份對技術(shù)的熱愛堆生。


因為學(xué)習分析源碼专缠,所以借鑒了 HollisChuang 成神之路的大部分內(nèi)容,并在此基礎(chǔ)上對源碼進行了學(xué)習淑仆,在此感謝涝婉。

等風來

問題的引入

關(guān)于 String 字符串,對于Java開發(fā)者而言蔗怠,這無疑是一個非常熟悉的類墩弯。也正是因為經(jīng)常使用吩跋,其內(nèi)部代碼的設(shè)計才值得被深究。所謂知其然渔工,更得知其所以然锌钮。

舉個例子,假如想要寫個類去繼承 String引矩,這時 IDE 提示 String 為final類型不允許被繼承梁丘。

此時最先想到的肯定是 java 中類被 final 修飾的效果,其實由這一點也可以引出更多思考:
比如說 String 類被設(shè)計成 final 類型是出于哪些考慮旺韭?

在Java中氛谜,被 final 類型修飾的類不允許被其他類繼承,被 final 修飾的變量賦值后不允許被修改


定義

查看 String 類在 JDK 7 源碼中的定義:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
...
}

可以看出 String 是 final 類型的区端,表示該類不能被其他類繼承值漫,同時該類實現(xiàn)了三個接口:java.io.Serializable Comparable<String> CharSequence

對于 Sting 類,官方有如下注釋說明:

/*
Strings are constant; 
their values can not be changed after they are created.
Stringbuffers support mutable strings.
Because String objects are immutable they can be shared. Forexample:
*/

String 字符串是常量织盼,其值在實例創(chuàng)建后就不能被修改杨何,但字符串緩沖區(qū)支持可變的字符串,因為緩沖區(qū)里面的不可變字符串對象們可以被共享沥邻。(其實就是使對象的引用發(fā)生了改變)


屬性

/**
The value isused for character storage.
*/
private final char value[];

這是一個字符數(shù)組危虱,并且是 final 類型,用于存儲字符串內(nèi)容谋国。從 fianl 關(guān)鍵字可以看出,String 的內(nèi)容一旦被初始化后迁沫,其不能被修改的芦瘾。

看到這里也許會有人疑惑,String 初始化以后好像可以被修改啊集畅。比如找一個常見的例子:
String str = “hello”; str = “hi”
其實這里的賦值并不是對 str 內(nèi)容的修改近弟,而是將str指向了新的字符串。另外可以明確的一點:String 其實是基于字符數(shù)組 char[] 實現(xiàn)的挺智。

下面再來看 String 其他屬性:
比如緩存字符串的 hash Code祷愉,其默認值為 0:

/**
Cache the hashcode for the string
*/
private int hash;  //Default to 0

關(guān)于序列化 serialVersionUID:

/**
use serialVersionUID from JDK 1.0.2 for interoperability
*/
private static final long serialVersionUID = -6849794470754667710L;

/**
Class String is special cased with in the Serialization Stream Protocol.
*/
privates tatic final ObjectStreamField[] serialPersistentFields =  new ObjectStreamField[0]

因為 String 實現(xiàn)了 Serializable 接口,所以支持序列化和反序列化支持赦颇。Java 的序列化機制是通過在運行時判斷類的 serialVersionUID 來驗證版本一致性的二鳄。在進行反序列化時,JVM 會把傳來的字節(jié)流中的 serialVersionUID 與本地相應(yīng)實體(類)的 serialVersionUID 進行比較媒怯,如果相同就認為是一致的订讼,可以進行反序列化,否則就會出現(xiàn)序列化版本不一致的異常 (InvalidCastException)扇苞。


構(gòu)造方法

空的構(gòu)造器

public String(){
  this.value = "".value;
}

該構(gòu)造方法會創(chuàng)建空的字符序列欺殿,注意這個構(gòu)造方法的使用寄纵,因為創(chuàng)造不必要的字符串對象是不可變的。因此不建議采取下面的創(chuàng)建 String 對象:

String str = new String()
str = "sample";

這樣的結(jié)果顯而易見脖苏,會產(chǎn)生了不必要的對象程拭。

使用字符串類型的對象來初始化

public String(String original){
  this.value = original.value;
  this.hash = original.hash;
}

這里將直接將源 String 中的 value 和 hash 兩個屬性直接賦值給目標 String。因為 String 一旦定義之后是不可以改變的棍潘,所以也就不用擔心改變源 String 的值會影響到目標 String 的值恃鞋。

使用字符數(shù)組來構(gòu)造

public String(char value[]){
    this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count){
  if(offset<0){
    throw new StringIndexOutOfBoundsException(offset);
  }
  if(count<=0){
    if(count<0){
     throw new String IndexOutOfBoundsException(count);
    }
    if(offset <= value.length){
      this.value = "".value;
      return;
    }
  }

 //Note:offset or count might be near-1>>>1.
  if(offset > value.length - count){
      throw new StringIndexOutOfBoundsException(offset+count);
  }
 this.value=Arrays.copyOfRange(value,offset,offset+count);
}

這里值得注意的是:當我們使用字符數(shù)組創(chuàng)建 String 的時候,會用到 Arrays.copyOf 方法或 Arrays.copyOfRange 方法蜒谤。這兩個方法是將原有的字符數(shù)組中的內(nèi)容逐一的復(fù)制到 String 中的字符數(shù)組中山宾。會創(chuàng)建一個新的字符串對象,隨后修改的字符數(shù)組不影響新創(chuàng)建的字符串鳍徽。

使用字節(jié)數(shù)組來構(gòu)建 String

在 Java 中资锰,String 實例中保存有一個 char[] 字符數(shù)組,char[] 字符數(shù)組是以 unicode 碼來存儲的阶祭,String 和 char 為內(nèi)存形式绷杜。

byte 是網(wǎng)絡(luò)傳輸或存儲的序列化形式,所以在很多傳輸和存儲的過程中需要將 byte[] 數(shù)組和 String 進行相互轉(zhuǎn)化濒募。所以 String 提供了一系列重載的構(gòu)造方法來將一個字符數(shù)組轉(zhuǎn)化成 String鞭盟,提到 byte[] 和 String 之間的相互轉(zhuǎn)換就不得不關(guān)注編碼問題。

String(byte[] bytes, Charset charset)

該構(gòu)造方法是指通過 charset 來解碼指定的 byte 數(shù)組瑰剃,將其解碼成 unicode 的 char[] 數(shù)組齿诉,構(gòu)造成新的 String。

這里的 bytes 字節(jié)流是使用 charset 進行編碼的晌姚,想要將他轉(zhuǎn)換成 unicode 的 char[] 數(shù)組粤剧,而又保證不出現(xiàn)亂碼,那就要指定其解碼方式

同樣的挥唠,使用字節(jié)數(shù)組來構(gòu)造 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[] 構(gòu)造 String 的時候,使用的是下面這四種構(gòu)造方法(帶有 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[] 構(gòu)造 String 的時候唤锉,如果沒有指明解碼使用的字符集的話世囊,那么 StringCoding 的 decode 方法首先調(diào)用系統(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;
    }
}

使用 StringBuffer 和 StringBuilder 構(gòu)造一個 String
作為 String 的兩個“兄弟”茸习,StringBuffer 和 StringBuilder 也可以被當做構(gòu)造 String 的參數(shù)。

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());
}

當然壁肋,這兩個構(gòu)造方法是很少用到的号胚,因為當我們有了 StringBuffer 或者 StringBuilfer 對象之后可以直接使用他們的 toString 方法來得到 String籽慢。

關(guān)于效率問題,Java 的官方文檔有提到說使用StringBuilder 的 toString 方法會更快一些猫胁,原因是 StringBuffer 的 toString 方法是 synchronized 的箱亿,在犧牲了效率的情況下保證了線程安全。

StringBuilder 的 toString() 方法:

@Override
public String toString(){
  //Create a copy, don't share the array
  return new String(value,0,count);
}

StringBuffer 的 toString() 方法:

@Override
public synchronized String toString(){
  if (toStringCache == null){
    toStringCache = Arrays.copyOfRange(value, 0, count);
  }
  return new String(toStringCache, true);
}

一個特殊的保護類型的構(gòu)造方法
String 除了提供了很多公有的供程序員使用的構(gòu)造方法以外弃秆,還提供了一個保護類型的構(gòu)造方法(Java 7)届惋,我們看一下他是怎么樣的:

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)容逐一復(fù)制到 String 當中塌忽,而這個 String(char[] value, boolean share) 方法則是直接將 value 的引用賦值給 String 的 value拍埠。那么也就是說,這個方法構(gòu)造出來的 String 和參數(shù)傳過來的 char[] value 共享同一個數(shù)組土居。

為什么 Java 會提供這樣一個方法呢枣购?

  • 性能好:這個很簡單,一個是直接給數(shù)組賦值(相當于直接將 String 的 value 的指針指向char[]數(shù)組)擦耀,一個是逐一拷貝棉圈,當然是直接賦值快了。

  • 節(jié)約內(nèi)存:該方法之所以設(shè)置為 protected埂奈,是因為一旦該方法設(shè)置為公有迄损,在外面可以訪問的話定躏,如果構(gòu)造方法沒有對 arr 進行拷貝账磺,那么其他人就可以在字符串外部修改該數(shù)組,由于它們引用的是同一個數(shù)組痊远,因此對 arr 的修改就相當于修改了字符串垮抗,那就破壞了字符串的不可變性。

  • 安全的:對于調(diào)用他的方法來說碧聪,由于無論是原字符串還是新字符串冒版,其 value 數(shù)組本身都是 String 對象的私有屬性,從外部是無法訪問的逞姿,因此對兩個字符串來說都很安全辞嗡。

Java 7 加入的新特性

在 Java 7 之前有很多 String 里面的方法都使用上面說的那種“性能好的捆等、節(jié)約內(nèi)存的、安全”的構(gòu)造函數(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è)一個方法從某個地方(文件瞳购、數(shù)據(jù)庫或網(wǎng)絡(luò))取得了一個很長的字符串话侄,然后對其進行解析并提取其中的一小段內(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ù)組卻不能釋放缠捌。這就導(dǎo)致了內(nèi)存泄漏锄贷。如果一個程序中這種情況經(jīng)常發(fā)生有可能會導(dǎo)致嚴重的后果,如內(nèi)存溢出曼月,或性能下降谊却。

新的實現(xiàn)雖然損失了性能,而且浪費了一些存儲空間哑芹,但卻保證了字符串的內(nèi)部數(shù)組可以和字符串對象一起被回收炎辨,從而防止發(fā)生內(nèi)存泄漏,因此新的 substring 比原來的更健壯聪姿。

其他方法

length() 返回字符串長度

public int length(){
  return value.length;
}

isEmpty() 返回字符串是否為空

public boolean isEmpty(){
  return value.length == 0;
}

charAt(int index) 返回字符串中第(index+1)個字符(數(shù)組索引)

public char charAt(int index){
  if((index < 0) || (index >= value.length)){
    throw new StringIndexOutOfBoundsException(index);
  }
  return value[index];
}

char[] toCharArray() 轉(zhuǎn)化成字符數(shù)組
trim()去掉兩端空格
toUpperCase()轉(zhuǎn)化為大寫
toLowerCase()轉(zhuǎn)化為小寫

需要注意
String concat(String str) 拼接字符串
String replace(char oldChar, char newChar) 將字符串中的
oldChar 字符換成 newChar 字符

以上兩個方法都使用了 String(char[] value, boolean share) concat 方法和 replace 方法碴萧,他們不會導(dǎo)致元數(shù)組中有大量空間不被使用,因為他們一個是拼接字符串末购,一個是替換字符串內(nèi)容破喻,不會將字符數(shù)組的長度變得很短,所以使用了共享的 char[] 字符數(shù)組來優(yōu)化盟榴。

boolean matches(String regex) 判斷字符串是否匹配給定的regex正則表達式
boolean contains(CharSequence s) 判斷字符串是否包含字符序列 s
String[] split(String regex, int limit) 按照字符 regex將字符串分成 limit 份
String[] split(String regex) 按照字符 regex 將字符串分段

getBytes

在創(chuàng)建 String 的時候曹质,可以使用 byte[] 數(shù)組,將一個字節(jié)數(shù)組轉(zhuǎn)換成字符串,同樣羽德,我們可以將一個字符串轉(zhuǎn)換成字節(jié)數(shù)組几莽,那么 String 提供了很多重載的 getBytes 方法。

public byte[] getBytes(){
  return StringCoding.encode(value, 0, value.length);
}

但是宅静,值得注意的是银觅,在使用這些方法的時候一定要注意編碼問題。比如:
String s = "你好坏为,世界究驴!"; byte[] bytes = s.getBytes();
這段代碼在不同的平臺上運行得到結(jié)果是不一樣的。由于沒有指定編碼方式匀伏,所以在該方法對字符串進行編碼的時候就會使用系統(tǒng)的默認編碼方式洒忧。

在中文操作系統(tǒng)中可能會使用 GBK 或者 GB2312 進行編碼,在英文操作系統(tǒng)中有可能使用 iso-8859-1 進行編碼够颠。這樣寫出來的代碼就和機器環(huán)境有很強的關(guā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) 可忽略大小寫局部匹配

字符串有一系列方法用于比較兩個字符串的關(guān)系笑跛。 前四個返回 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 方法轉(zhuǎn)換成大寫再進行比較翻屈,以此來忽略大小寫進行比較陈哑。相同則返回 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;
}

該方法首先判斷 this == anObject 伸眶?惊窖,也就是說判斷要比較的對象和當前對象是不是同一個對象,如果是直接返回 true赚抡,如不是再繼續(xù)比較爬坑,然后在判斷 anObject 是不是 String 類型的纠屋,如果不是涂臣,直接返回 false,如果是再繼續(xù)比較,到了能終于比較字符數(shù)組的時候赁遗,他還是先比較了兩個數(shù)組的長度署辉,不一樣直接返回 false,一樣再逐一比較值岩四。 雖然代碼寫的內(nèi)容比較多哭尝,但是可以很大程度上提高比較的效率。值得學(xué)習F驶汀2酿小!

StringBuffer 需要考慮線程安全問題耕姊,加鎖之后再調(diào)用

contentEquals 有兩個重載:

  • contentEquals((CharSequence) sb) 方法
    contentEquals((CharSequence) sb) 分兩種情況桶唐,一種是 cs instanceof AbstractStringBuilder,另外一種是參數(shù)是 String 類型茉兰。具體比較方式幾乎和 equals 方法類似尤泽,先做“宏觀”比較,在做“微觀”比較规脸。

下面這個是 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ù)學(xué)公式:s[0] * 31^(n-1) + s[1] * 31^(n-2) + ... + s[n-1]

所謂“沖突”闹丐,就是在存儲數(shù)據(jù)計算 hash 地址的時候,我們希望盡量減少有同樣的 hash 地址被因。如果使用相同 hash 地址的數(shù)據(jù)過多妇智,那么這些數(shù)據(jù)所組成的 hash 鏈就更長,從而降低了查詢效率氏身。

所以在選擇系數(shù)的時候要選擇盡量長的系數(shù)并且讓乘法盡量不要溢出的系數(shù)巍棱,因為如果計算出來的 hash 地址越大,所謂的“沖突”就越少蛋欣,查找起來效率也會提高航徙。

現(xiàn)在很多虛擬機里面都有做相關(guān)優(yōu)化,使用 31 的原因可能是為了更好的分配 hash 地址陷虎,并且 31 只占用 5 bits到踏。

在 Java 中,整型數(shù)是 32 位的尚猿,也就是說最多有 2^32 = 4294967296 個整數(shù)窝稿,將任意一個字符串,經(jīng)過 hashCode 計算之后凿掂,得到的整數(shù)應(yīng)該在這 4294967296 數(shù)之中伴榔。那么纹蝴,最多有 4294967297 個不同的字符串作 hashCode 之后,肯定有兩個結(jié)果是一樣的踪少。

hashCode 可以保證相同的字符串的 hash 值肯定相同塘安,但是 hash 值相同并不一定是 value 值就相同。

substring
前面我們介紹過援奢,java 7 中的 substring 方法使用
String(value, beginIndex, subLen) 方法創(chuàng)建一個新的 String 并返回兼犯,這個方法會將原來的 char[] 中的值逐一復(fù)制到新的 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,即可以支持字符的替換, 也支持字符串的替換
  • replaceAll 和 replaceFirst 的參數(shù)是 regex栽连,即基于規(guī)則表達式的替換

比如可以通過 replaceAll (“\d”, “*”)把一個字符串所有的數(shù)字字符都換成星號;

相同點是都是全部替換险领,即把源字符串中的某一字符或字符串全部換成指定的字符或字符串,如果只想替換第一次出現(xiàn)的秒紧,可以使用 replaceFirst()绢陌,這個方法也是基于規(guī)則表達式的替換。另外,如果replaceAll() 和r eplaceFirst() 所用的參數(shù)據(jù)不是基于規(guī)則表達式的熔恢,則與replace()替換字符串的效果是一樣的脐湾,即這兩者也支持字符串的操作。

copyValueOf 和 valueOf
String 的底層是由 char[] 實現(xiàn)的叙淌,早期的 String 構(gòu)造器的實現(xiàn)呢狸捕,不會拷貝數(shù)組的虽抄,直接將參數(shù)的 char[] 數(shù)組作為 String 的 value 屬性奔穿。字符數(shù)組將導(dǎo)致字符串的變化磷瘤。

為了避免這個問題,提供了 copyValueOf 方法茂洒,每次都拷貝成新的字符數(shù)組來構(gòu)造新的 String 對象孟岛。

現(xiàn)在的 String 對象,在構(gòu)造器中就通過拷貝新數(shù)組實現(xiàn)了督勺,所以這兩個方面在本質(zhì)上已經(jīng)沒區(qū)別了渠羞。

valueOf()有很多種形式的重載,包括:

 public static String valueOf(boolean b) {
       return b ? "true" : "false";
 } 

public static String valueOf(char c) {
       char data[] = {c};
       return new String(data, true);
 }

 public static String valueOf(int i) {
       return Integer.toString(i);
 }

 public static String valueOf(long l) {
       return Long.toString(l);
 }

 public static String valueOf(float f) {
       return Float.toString(f);
 } 

public static String valueOf(double d) {
     return Double.toString(d);
}

可以看到這些方法可以將六種基本數(shù)據(jù)類型的變量轉(zhuǎn)換成 String 類型智哀。

intern()方法
public native String intern(); 該方法返回一個字符串對象的內(nèi)部化引用次询。
String 類維護一個初始為空的字符串的對象池,當 intern 方法被調(diào)用時瓷叫,如果對象池中已經(jīng)包含這一個相等的字符串對象則返回對象池中的實例屯吊,否則添加字符串到對象池并返回該字符串的引用送巡。

String 對 “+” 的重載

我們知道,Java 是不支持重載運算符雌芽,String 的 “+” 是 java 中唯一的一個重載運算符,那么 java 使如何實現(xiàn)這個加號的呢辨嗽?我們先看一段代碼:

public static void main(String[] args) {
     String string = "hello";
     String string2 = string + "world";
}

然后我們將這段代碼的實際執(zhí)行情況貼出來看看:

public static void main(String args[]){
     String string = "hollo";
     String string2 = (new StringBuilder(String.valueOf(string))).append("world").toString();
}

看了反編譯之后的代碼我們發(fā)現(xiàn)世落,其實 String 對 “+” 的支持其實就是使用了 StringBuilder 以及他的 append、toString 兩個方法糟需。

String.valueOf和Integer.toString的區(qū)別
接下來我們看以下這段代碼屉佳,我們有三種方式將一個 int 類型的變量變成呢過String類型,那么他們有什么區(qū)別洲押?

int i = 5;
String i1 = "" + i;
String i2 = String.valueOf(i);
String i3 = Integer.toString(i);

第三行和第四行沒有任何區(qū)別武花,因為 String.valueOf(i) 也是調(diào)用
Integer.toString(i) 來實現(xiàn)的。
第二行代碼其實是 String i1 = (new StringBuilder()).append(i).toString();

首先創(chuàng)建了一個 StringBuilder 對象杈帐,然后再調(diào)用 append 方法体箕,再調(diào)用 toString 方法。


switch 對字符串支持的實現(xiàn)

還是先上代碼:

public class switchDemoString {
     public static void main(String[] args) {
         String str = "world";
         switch (str) {
         case "hello": 
              System.out.println("hello");
              break;
         case "world":
             System.out.println("world");
             break;
         default: break;
       }
    }
}

對編譯后的代碼進行反編譯:

public static void main(String args[]) {
       String str = "world";
       String s;
       switch((s = str).hashCode()) {
          case 99162322:
               if(s.equals("hello"))
                   System.out.println("hello");
               break;
          case 113318802:
               if(s.equals("world"))
                   System.out.println("world");
               break;
          default: break;
       }
  }

看到這個代碼挑童,你知道原來字符串的 switch 是通過 equals() 和 hashCode() 方法來實現(xiàn)的累铅。記住,switch 中只能使用整型站叼,比如 byte娃兽,short,char(ackii碼是整型) 以及 int尽楔。還好 hashCode() 方法返回的是 int 而不是 long投储。

通過這個很容易記住 hashCode 返回的是 int 這個事實。仔細看下可以發(fā)現(xiàn)阔馋,進行 switch 的實際是哈希值玛荞,然后通過使用 equals 方法比較進行安全檢查,這個檢查是必要的呕寝,因為哈铣迥啵可能會發(fā)生碰撞。

因此性能是不如使用枚舉進行 switch 或者使用純整數(shù)常量壁涎,但這也不是很差凡恍。因為 Java 編譯器只增加了一個 equals 方法,如果你比較的是字符串字面量的話會非痴颍快嚼酝,比如 ”abc” ==”abc” 。如果你把 hashCode() 方法的調(diào)用也考慮進來了竟坛,那么還會再多一次的調(diào)用開銷闽巩,因為字符串一旦創(chuàng)建了钧舌,它就會把哈希值緩存起來。
因此如果這個 siwtch 語句是用在一個循環(huán)里的涎跨,比如逐項處理某個值洼冻,或者游戲引擎循環(huán)地渲染屏幕,這里 hashCode() 方法的調(diào)用開銷其實不會很大隅很。

其實 swich 只支持一種數(shù)據(jù)類型撞牢,那就是整型,其他數(shù)據(jù)類型都是轉(zhuǎn)換成整型之后在使用 switch 的叔营。

總結(jié)

  • 一旦 String 對象在內(nèi)存(堆)中被創(chuàng)建出來屋彪,就無法被修改。

特別要注意的是绒尊,String 類的所有方法都沒有改變字符串本身的值畜挥,都是返回了一個新的對象。

  • 如果你需要一個可修改的字符串婴谱,應(yīng)該使用 StringBuffer 或者
    StringBuilder蟹但。

否則會有大量時間浪費在垃圾回收上,因為每次試圖修改都有新的String 對象被創(chuàng)建出來谭羔。

  • 如果你只需要創(chuàng)建一個字符串矮湘,你可以使用雙引號的方式,如果你需要在堆中創(chuàng)建一個新的對象口糕,你可以選擇構(gòu)造函數(shù)的方式缅阳。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市景描,隨后出現(xiàn)的幾起案子十办,更是在濱河造成了極大的恐慌,老刑警劉巖超棺,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件向族,死亡現(xiàn)場離奇詭異,居然都是意外死亡棠绘,警方通過查閱死者的電腦和手機件相,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來氧苍,“玉大人夜矗,你說我怎么就攤上這事∪门埃” “怎么了紊撕?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赡突。 經(jīng)常有香客問我对扶,道長区赵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任浪南,我火速辦了婚禮笼才,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘络凿。我一直安慰自己骡送,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布喷众。 她就那樣靜靜地躺著各谚,像睡著了一般紧憾。 火紅的嫁衣襯著肌膚如雪到千。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天赴穗,我揣著相機與錄音憔四,去河邊找鬼。 笑死般眉,一個胖子當著我的面吹牛了赵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甸赃,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼柿汛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了埠对?” 一聲冷哼從身側(cè)響起络断,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎项玛,沒想到半個月后貌笨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡襟沮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年锥惋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片开伏。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡膀跌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出固灵,到底是詐尸還是另有隱情淹父,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布怎虫,位于F島的核電站暑认,受9級特大地震影響困介,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蘸际,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一座哩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粮彤,春花似錦根穷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惫周,卻和暖如春尘惧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背递递。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工喷橙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人登舞。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓贰逾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親菠秒。 傳聞我的和親對象是個殘疾皇子疙剑,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法践叠,內(nèi)部類的語法言缤,繼承相關(guān)的語法,異常的語法酵熙,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,212評論 0 4
  • (一)Java部分 1轧简、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,071評論 0 62
  • 立冬,彈指間匾二,我們已經(jīng)從冬天走回了冬天哮独。 再兩個節(jié)氣就回冬至了吧? 想到近一年前的冬至那天察藐,第一次外出觀察物候皮璧,第...
    天凈樹梓閱讀 657評論 0 6
  • 昨日是個滿意的一天。低開高走并收獲很多分飞。臨睡前悴务,更是收到非常好的一篇文章。周沖寫的,如果你不是生活在愛中讯檐,你就生活...
    whalecao閱讀 356評論 0 0