其實String方面的面試題往深了延申的話,還是會延伸到JVM,所以還是希望讀者對JVM有一定的瞭解,這樣更便於理解String的設計。
String原始碼分析
String結構
/*
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:
*/
public final class String implements java.io.Serializable,
Comparable<String>, CharSequence
複製程式碼
原始碼裡可以看到String被final修飾並繼承了三個介面
原始碼註釋也說到字串是不變的; 它們的值在建立後無法更改.Stringbuffers支援可變字串。
因為String物件是不可變的,所以它們可以共享
複製程式碼
final
修飾類:類不可被繼承,也就是說,String類不可被繼承了
修飾方法:把方法鎖定,以訪任何繼承類修改它的涵義
修飾遍歷:初始化後不可更改
複製程式碼
Comparable和Serializable
Serializable介面裡為空
Comparable介面裡只有一個public int compareTo(T o);方法
這兩個介面不做敘述.
複製程式碼
CharSequence
介面中的方法
length(): int
charAt(): char
subSequence(int,int):CharSwquence
toString(): String
chars(): IntStream
codePoints(): IntStream
複製程式碼
我們發現這個介面中的方法很少,沒有我們常用的String方法,
那麼它應該是一個通用介面,會有很多實現類,包括StringBuilder和StringBuffer
複製程式碼
String構造方法
空參構造
public String() {
this.value = "".value;
}
複製程式碼
解析
String str=new String("abc");
1.先建立一個空的String物件
2.常量池中建立一個abc,並賦值給第二個String
3.將第二個String的引用傳遞給第一個String
注:如果常量池中有abc,則不用建立,直接把引用傳遞給第一個String
複製程式碼
String型別初始化
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
案例: String str=new String("str");
複製程式碼
字元陣列初始化
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
注:將傳過來的char陣列copy到value陣列裡
複製程式碼
位元組陣列初始化
byte型別的方法有8個,兩個過時的
剩下六個又分為指定編碼和不指定編碼
不指定編碼
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, 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是網路傳輸或儲存的序列化形式,
所以在很多傳輸和儲存的過程中需要將byte[]陣列和String進行相互轉化,
byte是位元組,char是字元,位元組流和字元流需要指定編碼,不然可能會亂碼,
bytes位元組流是使用charset進行編碼的,想要將他轉換成unicode的char[]陣列,
而又保證不出現亂碼,那就要指定其解碼方法
複製程式碼
StringBUilder構造
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());
}
注:很多時候我們不會這麼去構造,因為StringBuilder跟StringBuffer有toString方法
如果不考慮執行緒安全,優先選擇StringBuilder
複製程式碼
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;
}
複製程式碼
String重寫了父類Object的equals方法
先判斷地址是否相等(地址相等的情況下,肯定是一個值,直接返回true)
在判斷是否是String型別,不是則返回false
如果都是String,先判斷長度,
再比較值,把值賦給char陣列,遍歷兩個char陣列比較
複製程式碼
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;
}
複製程式碼
如果String的length==0或者hash值為0,則直接返回0
如果上述條件不滿足,則通過演算法計算hash值
複製程式碼
intern方法
public native String intern();
注:方法註釋會有寫到,意思就是呼叫方法時,
如果常量池有當前String的值,就返回這個值,沒有就加進去,返回這個值的引用
複製程式碼
String str1="a";
String str2="b";
String str3="ab";
String str4 = str1+str2;
String str5=new String("ab");
System.out.println(str5==str3);//堆記憶體比較字串池
//intern如果常量池有當前String的值,就返回這個值,沒有就加進去,返回這個值的引用
System.out.println(str5.intern()==str3);//引用的是同一個字串池裡的
System.out.println(str5.intern()==str4);//變數相加給一個新值,所以str4引用的是個新的
System.out.println(str4==str3);//變數相加給一個新值,所以str4引用的是個新的
false
true
false
false
複製程式碼
重點: --兩個字串常量或者字面量相加,不會new新的字串,其他相加則是新值,(如 String str5=str1+"b";)
因為在jvm翻譯為二進位制程式碼時,會自動優化,把兩個值後邊的結果先合併,再儲存為一個常量。
複製程式碼
String裡還有很多方法,substring,replace等等,我們就不一一舉例了
StringBuilder
StringBuilder和Stringbuffer這兩個類的方法都很想一樣,因此我們就那StringBuilder的原始碼作分析
等下再去看三者之間的關係和不同
複製程式碼
結構和構造
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
//空構造,初始大小16
public StringBuilder() {
super(16);
}
//給予一個初始化容量
public StringBuilder(int capacity) {
super(capacity);
}
//使用String進行建立
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
//String建立和CharSequence型別建立,額外多分配16個字元的空間,
//然後呼叫append將引數字元新增進來,(字串緩衝區)
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
}
複製程式碼
解析
我們可以看到方法內部都是在呼叫父類的方法,
通過繼承關係,我們是知道它的父類是AbstractStringBuilder,
父類裡實現類Appendable跟CharSequence 介面,所以它能夠跟String相互轉換
複製程式碼
父類AbstractStringBuilder
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
複製程式碼
解析
父類裡是隻有兩個構造方法,一個為空實現,一個為指定字元陣列的容量,
如果事先知道String的長度小於16,就可以節省記憶體空間,
他的陣列和String的不一樣,因為成員變數value陣列沒有被final修飾,
所以可以修改他的引用變數的值,即可以引用到新的陣列物件,
所以StringBuilder物件是可變的
複製程式碼
append
append有很多過載方法,原理都差不多
我們以String舉例
//傳入要追加的字串
public AbstractStringBuilder append(String str) {
//判斷字串是否為null
if (str == null)
return appendNull();
//不為null,獲得它的長度
int len = str.length();
//呼叫方法,把原先長度+追加字串長度的和傳入方法
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//判斷是否滿足擴充套件要求
private void ensureCapacityInternal(int minimumCapacity) {
// 和-原先字串長度是否>0 肯定是大於0的
if (minimumCapacity - value.length > 0)
//呼叫複製空間的方法,和當引數
expandCapacity(minimumCapacity);
}
//開始擴充
void expandCapacity(int minimumCapacity) {
//先把原先長度複製2倍多2
int newCapacity = value.length * 2 + 2;
//判斷newCapacity-和是否<0
if (newCapacity - minimumCapacity < 0)
//小於0的情況就是你複製的長度不夠,那就把和的長度給複製的長度
newCapacity = minimumCapacity;
//正常邏輯怎麼著都走不到這一步,新長度肯定是大於0
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//將陣列擴容拷貝
value = Arrays.copyOf(value, newCapacity);
}
複製程式碼
insert
insert同樣有很多過載方法,下面以char和String為例
insert的ensureCapacityInternal(count + 1);和上面一樣,不做講解了
public AbstractStringBuilder insert(int offset, char c) {
//檢查是否滿足擴充條件
ensureCapacityInternal(count + 1);
//拷貝陣列
System.arraycopy(value, offset, value, offset + 1, count - offset);
//進行復制
value[offset] = c;
count += 1;
return this;
}
public AbstractStringBuilder insert(int offset, String str) {
//判斷要插入的座標是否在字串內,不再則報陣列下標越界
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
//判斷要插入的是否為null
if (str == null)
str = "null";
//獲得要插入的字串長度
int len = str.length();
//檢查是否滿足擴充條件
ensureCapacityInternal(count + len);
//拷貝陣列
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
複製程式碼
StringBuffer
public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
}
跟StringBuilder差不多,只不過在所有的方法上面加了一個同步鎖
複製程式碼
equals與==
equals
String類重寫了父類equals的方法
我們先看下父類的
//直接判斷地址
public boolean equals(Object obj) {
return (this == obj);
}
複製程式碼
再看下String類的equals
public boolean equals(Object anObject) {
//地址相等肯定為true,就不用繼續往下走了
if (this == anObject) {
return true;
}
//地址不相等的情況下,比較兩個字元的內容是否一樣
//把字串方法char[]陣列裡,遍歷比較
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;
}
複製程式碼
==
==比較的是記憶體地址
基本資料型別比較值
引用資料型別比較地址值
(物件的引用,在堆空間,String在字串池,newString在堆空間)
複製程式碼
根據下面案例分析一下原始碼
建立方式 | 物件個數 | 引用指向 |
---|---|---|
String a="abc"; | 1 | 常量池 |
String b=new String("abc");; | 1 | 堆記憶體 (abc則是複製的常量池裡的abc) |
String c=new String() | 1 | 堆記憶體 |
String d="a"+"bc"; | 3 | 常量池(a一次,bc一次,和一次,d指向和) |
String e=a+b; | 3 | 堆記憶體 |
重點--兩個字串常量或者字面量相加,不會new新的字串,
變數相加則是會new新的字串,new出來的都在堆
總結
String被final修飾,一旦建立無法更改,每次更改則是在新建立物件
StringBuilder和StringBuffer則是可修改的字串
StringBuilder和StringBuffer的區別
StringBuffer 被synchronized 修飾,同步,執行緒安全
StringBuilder並沒有對方法進行加同步鎖,所以是非執行緒安全的。
如果程式不是多執行緒的,那麼使用StringBuilder效率高於StringBuffer。
複製程式碼