其實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。