Java 7 源码学习系列(一)——String
String表示字符串,Java中所有字符串的字面值都是String類的實例,例如“ABC”。字符串是常量,在定義之后不能被改變,字符串緩沖區支持可變的字符串。因為 String 對象是不可變的,所以可以共享它們。例如:
String str = "abc";相當于
char data[] = {'a', 'b', 'c'}; String str = new String(data);?
這里還有一些其他使用字符串的例子:
System.out.println("abc");String cde = "cde";System.out.println("abc" + cde);String c = "abc".substring(2,3);String d = cde.substring(1, 2);String類提供了檢查字符序列中單個字符的方法,比如有比較字符串,搜索字符串,提取子字符串,創建一個字符串的副本、字符串的大小寫轉換等。實例映射是基于Character類中指定的Unicode標準的。?Java語言提供了對字符串連接運算符的特別支持(+),該符號也可用于將其他類型轉換成字符串。字符串的連接實際上是通過StringBuffer或者StringBuilder的append()方法來實現的,字符串的轉換通過toString方法實現,該方法由 Object 類定義,并可被 Java 中的所有類繼承。 除非另有說明,傳遞一個空參數在這類構造函數或方法會導致NullPointerException異常被拋出。String表示一個字符串通過UTF-16(unicode)格式,補充字符通過代理對(參見Character類的 Unicode Character Representations 獲取更多的信息)表示。索引值參考字符編碼單元,所以補充字符在String中占兩個位置。
定義?屬性?構造方法
使用字符數組、字符串構造一個String
使用字節數組構造一個String
使用StringBuffer和StringBuider構造一個String
一個特殊的保護類型的構造方法
其他方法
getBytes
比較方法
hashCode
substring
replaceFirst、replaceAll、replace區別
copyValueOf 和 valueOf
intern
String對“+”的重載
String.valueOf和Integer.toString的區別
參考資料
一、定義
public final class String implements java.io.Serializable, Comparable<String>, CharSequence{}從該類的聲明中我們可以看出String是final類型的,表示該類不能被繼承,同時該類實現了三個接口:java.io.Serializable、?Comparable<String>、?CharSequence
二、屬性
private final char value[];這是一個字符數組,并且是final類型,他用于存儲字符串內容,從fianl這個關鍵字中我們可以看出,String的內容一旦被初始化了是不能被更改的。 雖然有這樣的例子: String s = “a”; s = “b” 但是,這并不是對s的修改,而是重新指向了新的字符串, 從這里我們也能知道,String其實就是用char[]實現的。
private int hash;緩存字符串的hash Code,默認值為 0
private static final long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];因為String實現了Serializable接口,所以支持序列化和反序列化支持。Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidCastException)。
三、構造方法
String類作為一個java.lang包中比較常用的類,自然有很多重載的構造方法.在這里介紹幾種典型的構造方法:
1.使用字符數組、字符串構造一個String
我們知道,其實String就是使用字符數組(char[])實現的。所以我們可以使用一個字符數組來創建一個String,那么這里值得注意的是,當我們使用字符數組創建String的時候,會用到Arrays.copyOf方法和Arrays.copyOfRange方法。這兩個方法是將原有的字符數組中的內容逐一的復制到String中的字符數組中。同樣,我們也可以用一個String類型的對象來初始化一個String。這里將直接將源String中的value和hash兩個屬性直接賦值給目標String。因為String一旦定義之后是不可以改變的,所以也就不用擔心改變源String的值會影響到目標String的值。
當然,在使用字符數組來創建一個新的String對象的時候,不僅可以使用整個字符數組,也可以使用字符數組的一部分,只要多傳入兩個參數int offset和int count就可以了。
2.使用字節數組構造一個String
在Java中,String實例中保存有一個char[]字符數組,char[]字符數組是以unicode碼來存儲的,String 和 char 為內存形式,byte是網絡傳輸或存儲的序列化形式。所以在很多傳輸和存儲的過程中需要將byte[]數組和String進行相互轉化。所以,String提供了一系列重載的構造方法來將一個字符數組轉化成String,提到byte[]和String之間的相互轉換就不得不關注編碼問題。String(byte[] bytes, Charset charset)是指通過charset來解碼指定的byte數組,將其解碼成unicode的char[]數組,夠造成新的String。
這里的bytes字節流是使用charset進行編碼的,想要將他轉換成unicode的char[]數組,而又保證不出現亂碼,那就要指定其解碼方式
同樣使用字節數組來構造String也有很多種形式,按照是否指定解碼方式分的話可以分為兩種:
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的時候,使用的是下面這四種構造方法(帶有charsetName或者charset參數)的一種的話,那么就會使用StringCoding.decode方法進行解碼,使用的解碼的字符集就是我們指定的charsetName或者charset。 我們在使用byte[]構造String的時候,如果沒有指明解碼使用的字符集的話,那么StringCoding的decode方法首先調用系統的默認編碼格式,如果沒有指定編碼格式則默認使用ISO-8859-1編碼格式進行編碼操作。主要體現代碼如下:
static char[] decode(byte[] ba, int off, int len) {String csn = Charset.defaultCharset().name();try {// use charset name decode() variant which provides caching.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 initialization, MessageUtils is// the only way we will be able to get any kind of error message.MessageUtils.err("ISO-8859-1 charset not available: "+ x.toString());// If we can not find ISO-8859-1 (a required encoding) then things// are seriously wrong with the installation.System.exit(1);return null;}}3.使用StringBuffer和StringBuider構造一個String
作為String的兩個“兄弟”,StringBuffer和StringBuider也可以被當做構造String的參數。
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或者StringBuilfer對象之后可以直接使用他們的toString方法來得到String。關于效率問題,Java的官方文檔有提到說使用StringBuilder的toString方法會更快一些,原因是StringBuffer的toString方法是synchronized的,在犧牲了效率的情況下保證了線程安全。
public String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);}this.value = Arrays.copyOfRange(value, offset, offset+count);4.一個特殊的保護類型的構造方法
String除了提供了很多公有的供程序員使用的構造方法以外,還提供了一個保護類型的構造方法(Java 7),我們看一下他是怎么樣的:
String(char[] value, boolean share) {// assert share : "unshared not supported";this.value = value; }從代碼中我們可以看出,該方法和?String(char[] value)有兩點區別,第一個,該方法多了一個參數:?boolean share,其實這個參數在方法體中根本沒被使用,也給了注釋,目前不支持使用false,只使用true。那么可以斷定,加入這個share的只是為了區分于String(char[] value)方法,不加這個參數就沒辦法定義這個函數,只有參數不能才能進行重載。那么,第二個區別就是具體的方法實現不同。我們前面提到過,String(char[] value)方法在創建String的時候會用到 會用到Arrays的copyOf方法將value中的內容逐一復制到String當中,而這個String(char[] value, boolean share)方法則是直接將value的引用賦值給String的value。那么也就是說,這個方法構造出來的String和參數傳過來的char[] value共享同一個數組。 那么,為什么Java會提供這樣一個方法呢? 首先,我們分析一下使用該構造函數的好處:
首先,性能好,這個很簡單,一個是直接給數組賦值(相當于直接將String的value的指針指向char[]數組),一個是逐一拷貝。當然是直接賦值快了。
其次,共享內部數組節約內存
但是,該方法之所以設置為protected,是因為一旦該方法設置為公有,在外面可以訪問的話,那就破壞了字符串的不可變性。例如如下YY情形:
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的修改就相當于修改了字符串。
所以,從安全性角度考慮,他也是安全的。對于調用他的方法來說,由于無論是原字符串還是新字符串,其value數組本身都是String對象的私有屬性,從外部是無法訪問的,因此對兩個字符串來說都很安全。
在Java 7 之有很多String里面的方法都使用這種“性能好的、節約內存的、安全”的構造函數。比如:substring、replace、concat、valueOf等方法(實際上他們使用的是public String(char[], int, int)方法,原理和本方法相同,已經被本方法取代)。
但是在Java 7中,substring已經不再使用這種“優秀”的方法了,為什么呢??雖然這種方法有很多優點,但是他有一個致命的缺點,對于sun公司的程序員來說是一個零容忍的bug,那就是他很有可能造成內存泄露。 看一個例子,假設一個方法從某個地方(文件、數據庫或網絡)取得了一個很長的字符串,然后對其進行解析并提取其中的一小段內容,這種情況經常發生在網頁抓取或進行日志分析的時候。下面是示例代碼。
String aLongString = "...a very long string..."; String aPart = data.substring(20, 40); return aPart;在這里aLongString只是臨時的,真正有用的是aPart,其長度只有20個字符,但是它的內部數組卻是從aLongString那里共享的,因此雖然aLongString本身可以被回收,但它的內部數組卻不能(如下圖)。這就導致了內存泄漏。如果一個程序中這種情況經常發生有可能會導致嚴重的后果,如內存溢出,或性能下降。
新的實現雖然損失了性能,而且浪費了一些存儲空間,但卻保證了字符串的內部數組可以和字符串對象一起被回收,從而防止發生內存泄漏,因此新的substring比原來的更健壯。
額、、、扯了好遠,雖然substring方法已經為了其魯棒性放棄使用這種share數組的方法,但是這種share數組的方法還是有一些其他方法在使用的,這是為什么呢?首先呢,這種方式構造對應有很多好處,其次呢,其他的方法不會將數組長度變短,也就不會有前面說的那種內存泄露的情況(內存泄露是指不用的內存沒有辦法被釋放,比如說concat方法和replace方法,他們不會導致元數組中有大量空間不被使用,因為他們一個是拼接字符串,一個是替換字符串內容,不會將字符數組的長度變得很短!)。
四、其他方法
length()?返回字符串長度
isEmpty()?返回字符串是否為空
charAt(int index)?返回字符串中第(index+1)個字符
char[] toCharArray()?轉化成字符數組
trim()?去掉兩端空格
toUpperCase()?轉化為大寫
toLowerCase()?轉化為小寫
String concat(String str) //拼接字符串
String replace(char oldChar, char newChar) //將字符串中的oldChar字符換成newChar字符
//以上兩個方法都使用了String(char[] value, boolean share);
boolean matches(String regex) //判斷字符串是否匹配給定的regex正則表達式
boolean contains(CharSequence s) //判斷字符串是否包含字符序列s
String[] split(String regex, int limit)?按照字符regex將字符串分成limit份。
String[] split(String regex)
String string = "h,o,l,l,i,s,c,h,u,a,n,g"; String[] splitAll = string.split(","); String[] splitFive = string.split(",",5); splitAll = [h, o, l, l, i, s, c, h, u, a, n, g] splitFive = [h, o, l, l, i,s,c,h,u,a,n,g]getBytes
在創建String的時候,可以使用byte[]數組,將一個字節數組轉換成字符串,同樣,我們可以將一個字符串轉換成字節數組,那么String提供了很多重載的getBytes方法。但是,值得注意的是,在使用這些方法的時候一定要注意編碼問題。比如:
String s = "你好,世界!"; byte[] bytes = s.getBytes();這段代碼在不同的平臺上運行得到結果是不一樣的。由于我們沒有指定編碼方式,所以在該方法對字符串進行編碼的時候就會使用系統的默認編碼方式,比如在中文操作系統中可能會使用GBK或者GB2312進行編碼,在英文操作系統中有可能使用iso-8859-1進行編碼。這樣寫出來的代碼就和機器環境有很強的關聯性了,所以,為了避免不必要的麻煩,我們要指定編碼方式。如使用以下方式:
String s = "你好,世界!"; byte[] bytes = s.getBytes("utf-8");比較方法
boolean equals(Object anObject); boolean contentEquals(StringBuffer sb); boolean contentEquals(CharSequence cs); 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和要比較的目標對象的字符數組的內容,一樣就返回true,不一樣就返回false,核心代碼如下:
int n = value.length;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}v1 v2分別代表String的字符數組和目標對象的字符數組。 第四個和前三個唯一的區別就是他會將兩個字符數組的內容都使用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;}該方法首先判斷this == anObject ?,也就是說判斷要比較的對象和當前對象是不是同一個對象,如果是直接返回true,如不是再繼續比較,然后在判斷anObject是不是String類型的,如果不是,直接返回false,如果是再繼續比較,到了能終于比較字符數組的時候,他還是先比較了兩個數組的長度,不一樣直接返回false,一樣再逐一比較值。 雖然代碼寫的內容比較多,但是可以很大程度上提高比較的效率。值得學習~~!!!
contentEquals有兩個重載,StringBuffer需要考慮線程安全問題,再加鎖之后調用contentEquals((CharSequence) sb)方法。contentEquals((CharSequence) sb)則分兩種情況,一種是cs instanceof AbstractStringBuilder,另外一種是參數是String類型。具體比較方式幾乎和equals方法類似,先做“宏觀”比較,在做“微觀”比較。
下面這個是equalsIgnoreCase代碼的實現:
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
hashCode的實現其實就是使用數學公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]s[i]是string的第i個字符,n是String的長度。那為什么這里用31,而不是其它數呢? 計算機的乘法涉及到移位計算。當一個數乘以2時,就直接拿該數左移一位即可!選擇31原因是因為31是一個素數!
所謂素數:
質數又稱素數。指在一個大于1的自然數中,除了1和此整數自身外,沒法被其他自然數整除的數。
素數在使用的時候有一個作用就是如果我用一個數字來乘以這個素數,那么最終的出來的結果只能被素數本身和被乘數還有1來整除!如:我們選擇素數3來做系數,那么3*n只能被3和n或者1來整除,我們可以很容易的通過3n來計算出這個n來。這應該也是一個原因!?(本段表述有問題,感謝 @沉淪 的提醒)
在存儲數據計算hash地址的時候,我們希望盡量減少有同樣的hash地址,所謂“沖突”。如果使用相同hash地址的數據過多,那么這些數據所組成的hash鏈就更長,從而降低了查詢效率!所以在選擇系數的時候要選擇盡量長的系數并且讓乘法盡量不要溢出的系數,因為如果計算出來的hash地址越大,所謂的“沖突”就越少,查找起來效率也會提高。
31可以 由i*31== (i<<5)-1來表示,現在很多虛擬機里面都有做相關優化,使用31的原因可能是為了更好的分配hash地址,并且31只占用5bits!
在java乘法中如果數字相乘過大會導致溢出的問題,從而導致數據的丟失.
而31則是素數(質數)而且不是很長的數字,最終它被選擇為相乘的系數的原因不過與此!
在Java中,整型數是32位的,也就是說最多有2^32= 4294967296個整數,將任意一個字符串,經過hashCode計算之后,得到的整數應該在這4294967296數之中。那么,最多有 4294967297個不同的字符串作hashCode之后,肯定有兩個結果是一樣的,?hashCode可以保證相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。
substring
public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }前面我們介紹過,java 7 中的substring方法使用String(value, beginIndex, subLen)方法創建一個新的String并返回,這個方法會將原來的char[]中的值逐一復制到新的String中,兩個數組并不是共享的,雖然這樣做損失一些性能,但是有效地避免了內存泄露。
replaceFirst、replaceAll、replace區別
String replaceFirst(String regex, String replacement) String replaceAll(String regex, String replacement) String replace(CharSequence target, CharSequence replacement)1)replace的參數是char和CharSequence,即可以支持字符的替換,也支持字符串的替換 2)replaceAll和replaceFirst的參數是regex,即基于規則表達式的替換,比如,可以通過replaceAll(“\d”, “*”)把一個字符串所有的數字字符都換成星號;?相同點是都是全部替換,即把源字符串中的某一字符或字符串全部換成指定的字符或字符串,?如果只想替換第一次出現的,可以使用 replaceFirst(),這個方法也是基于規則表達式的替換,但與replaceAll()不同的是,只替換第一次出現的字符串;?另外,如果replaceAll()和replaceFirst()所用的參數據不是基于規則表達式的,則與replace()替換字符串的效果是一樣的,即這兩者也支持字符串的操作;
copyValueOf 和 valueOf
String的底層是由char[]實現的:通過一個char[]類型的value屬性!早期的String構造器的實現呢,不會拷貝數組的,直接將參數的char[]數組作為String的value屬性。然后test[0] = 'A';將導致字符串的變化。為了避免這個問題,提供了copyValueOf方法,每次都拷貝成新的字符數組來構造新的String對象。但是現在的String對象,在構造器中就通過拷貝新數組實現了,所以這兩個方面在本質上已經沒區別了。
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); }可以看到這些方法可以將六種基本數據類型的變量轉換成String類型。
intern()方法
public native String intern();該方法返回一個字符串對象的內部化引用。 眾所周知:String類維護一個初始為空的字符串的對象池,當intern方法被調用時,如果對象池中已經包含這一個相等的字符串對象則返回對象池中的實例,否則添加字符串到對象池并返回該字符串的引用。
String對“+”的重載
我們知道,Java是不支持重載運算符,String的“+”是java中唯一的一個重載運算符,那么java使如何實現這個加號的呢?我們先看一段代碼:
public static void main(String[] args) {String string="hollis";String string2 = string + "chuang"; }然后我們將這段代碼反編譯:
public static void main(String args[]){String string = "hollis";String string2 = (new StringBuilder(String.valueOf(string))).append("chuang").toString(); }看了反編譯之后的代碼我們發現,其實String對“+”的支持其實就是使用了StringBuilder以及他的append、toString兩個方法。
String.valueOf和Integer.toString的區別
接下來我們看以下這段代碼,我們有三種方式將一個int類型的變量變成呢過String類型,那么他們有什么區別?
1.int i = 5; 2.String i1 = "" + i; 3.String i2 = String.valueOf(i); 4.String i3 = Integer.toString(i);1、第三行和第四行沒有任何區別,因為String.valueOf(i)也是調用Integer.toString(i)來實現的。 2、第二行代碼其實是String i1 = (new StringBuilder()).append(i).toString();,首先創建一個StringBuilder對象,然后再調用append方法,再調用toString方法。
參考資料:
open jdk
Java? Platform, Standard Edition 7 API Specification
Java7為什么要修改substring的實現
關于hashcode 里面 使用31 系數的問題
from:?http://www.hollischuang.com/archives/99#String.valueOf%E5%92%8CInteger.toString%E7%9A%84%E5%8C%BA%E5%88%AB
總結
以上是生活随笔為你收集整理的Java 7 源码学习系列(一)——String的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 源码学习系列(三)——Inte
- 下一篇: Java NIO浅析