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ù)的方式缅阳。