String是我們其中用的最多的一個類,但是我們有很多細節我們可能沒有去深深去研究,這裏主要通過閱讀源碼去了解這個類。瞭解這個類我們分三個步驟去研究:
1.String類信息,修飾的類關鍵字以及實現的接口,繼承的類,實現的接口實現了什麼樣的規範,父類主要完成什麼方法;
2.基本類屬性,包括靜態屬性,以及普通的屬性;
3.方法,可以瞭解這個類的用法,實現避免一些不必要的坑。
1.類屬性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
首先我們看看到String類被final這個關鍵字修飾了,說明String是不可以繼承的。這個時候我們就會有疑問這裏爲什麼用final修飾,其實簡單來講就是兩點:安全和效率。安全,首先這個基本類提供給用戶使用,就是希望這個類不輕易的而改變,容易出現不同的問題,還有點String還有native方法,不暴露成員變量。final關鍵字詳解
然後看這個String繼承的接口:java.io.Serializable, Comparable<String>, CharSequence。Serializable這個接口被繼承的類可以被序列化以及反序列化; CharSequence主要提供一些對字符串序列一些只讀的方法規範,例如,獲取字符串長度,以及索引值的字符等;Comparable<String>主要定義一個類的大小比較規範。我們可以大概看下實現方式:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
我們可以熊源碼中總結以下結論:1.先按照字符串優先逐個比較字符大小,如果遇到不一樣的字符,就直接判斷兩個不同的字符,直到最小串結束;2.然後比較兩個字符串的長度
2.成員屬性
從源碼中我們可以看到四個屬性,兩個靜態屬性:serialVersionUID,serialPersistentFields ,這兩個屬性和繼承的Serializable的接口有關係,這個serialVersionUID主要用來在JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidCastException) /** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
還有兩個普通的成員hash以及private final char value[]。首先了解一下這個hash的使用,這個hash值的只有在兩個地方纔會初始化,一個是被其他的String構造出新的String時候;還有一個是在第一次調用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;
}
我們這裏有一個疑問就是爲什麼要以31作爲一個權值呢??詳情可以看下這個博客String爲啥用31作爲權值然後我們主要看下value這個值被final修飾,也就意味着一旦String對象生成,value指向就是不可變的,這裏的不可變不是絕對的不可變,我們可以通過反射修改內存數據:
final String a="ab";
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
char[] o = (char[])value.get(a);
o[0]='b';
o[1]='A';
System.out.println(a);
除下使用反射去改變內存情況,,一個String生成就不會發生改變了,因此subString等一些寫操作基本都是new一個新的String對象(返回的String和this的一樣時返回本身),
3.String內存分配
其他的方法就不一一介紹,接下來介紹一下String和虛擬機的內存模型的關係。其實要你有了解過jvm(jdk1.7),jvm對String創建進行一個優化,就是使用字符串常量池,String使用一個字符串的時候,jvm首先會檢查運行時常量區,是否有String對象,如果就返回這個對象的引用,否則實例化並且把該String放入字符串常量區,結論:在字符串常量區,不可能出現兩份及以上的同樣的String實例。這樣有什麼好處呢?這樣的可以大大減少程序使用的內存。我們知道String是不變的(引用不可變),一份同樣的String沒有必要創建兩份,變這樣既減少了對象創建的時間以及使用的內存。接下來通過幾String創建分析內存的分佈。
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s="test";
char test[]={'t','e','s','t'};
String te="te";
String st="st";
String s1=new String(s);
String s2="te"+"st";
String s3="te"+new String("st");
String s4=new String(test);
String s5=te+st;
if (s==s1){
System.out.println("s==s1");
}
if (s==s2){
System.out.println("s==s2");
}
if (s==s3){
System.out.println("s==s3");
}
if (s==s4){
System.out.println("s==s4");
}
if (s==s5){
System.out.println("s==s5");
}
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);//查看value的分配方式
char[] sValue = (char[]) valueField.get(s);
char[] s1Value = (char[]) valueField.get(s1);
char[] s2Value = (char[]) valueField.get(s2);
char[] s3Value = (char[]) valueField.get(s3);
char[] s4Value = (char[]) valueField.get(s4);
char[] s5Value = (char[]) valueField.get(s5);
if (sValue==s1Value){
System.out.println("s1value is same");
}
if (sValue==s2Value){
System.out.println("s2value is same");
}
if (sValue==s3Value){
System.out.println("s3value is same");
}
if (sValue==s4Value){
System.out.println("s4value is same");
}
if (sValue==s5Value){
System.out.println("s5value is same");
}
}
}
運行結果:
s==s2
s1value is same
s2value is same
結果對比結果:s和s2指向同一個字符串常量,s,s1,s2的char value[]指向同一片內存地址;s1,s3,s4,s5都和s都引用不相等。
這裏我們就看String 這個“+”是怎麼起作用的,我們可以通過反編譯(jad -o -a -s d.java Test.class)得到我們想要的代碼(已經刪除字節碼註釋):
public class Test
{
public Test()
{
}
public static void main(String args[])
throws NoSuchFieldException, IllegalAccessException
{
String s = "test";
char test[] = {
't', 'e', 's', 't'
};
String te = "te";
String st = "st";
String s1 = new String(s);
String s2 = "test";
String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();
String s4 = new String(test);
String s5 = (new StringBuilder()).append(te).append(st).toString();
if(s == s1)
System.out.println("s==s1");
if(s == s2)
System.out.println("s==s2");
if(s == s3)
System.out.println("s==s3");
if(s == s4)
System.out.println("s==s4");
if(s == s5)
System.out.println("s==s5");
Field valueField = java/lang/String.getDeclaredField("value");
valueField.setAccessible(true);
char sValue[] = (char[])(char[])valueField.get(s);
char s1Value[] = (char[])(char[])valueField.get(s1);
char s2Value[] = (char[])(char[])valueField.get(s2);
char s3Value[] = (char[])(char[])valueField.get(s3);
char s4Value[] = (char[])(char[])valueField.get(s4);
char s5Value[] = (char[])(char[])valueField.get(s5);
if(sValue == s1Value)
System.out.println("s1value is same");
if(sValue == s2Value)
System.out.println("s2value is same");
if(sValue == s3Value)
System.out.println("s3value is same");
if(sValue == s4Value)
System.out.println("s4value is same");
if(sValue == s5Value)
System.out.println("s5value is same");
}
}
1. 1.s!=s1 s.value==s1.value
s是常量區String對象的引用,s1是使用new在堆裏面生成的對象引用,所以s!=s1;而s.value==s1.value相等是因爲構造方法是一個淺拷貝:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
因爲我們看String源代碼可以看到數據底層的value是指向同一個char[]實例的,相當於多實例出一個String對象,但是用了同一個底層數據,對資源是一種浪費,所以平時儘量減少這種方式使用。
2. 1.s!=s2 s.value==s2.value &s!=s5 s.value==s5.value&s!=s3 s.value==s3.value
分析:我們看編譯之後字節碼就可以發現,在編譯期間jvm就可以直接優化s2直接引用常量區的字符串常量:
String s2="te"+"st";優化爲String s2="test"
而使用引用被使用"+"重載:
String s3="te"+new String("st")
String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();
String s5=te+st
String s5 = (new StringBuilder()).append(te).append(st).toString();
對於s3和s5在編譯期間不能不確定性,所以沒有被編譯器優化,所以使用的是StringBuilder,通過StringBuilder源碼可以發現toString方法也是直接在堆上直接創建一個新的String對象,所以導致String以及屬性value內存都不相等。
@Override //StringBuilder.toString()
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
2. 1.s!=s4 s.value==s4.value
這個看源碼就可以知道String對象以及velue內存是直接從堆裏面分配的
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
總結:由以上實驗我們可以看到,不同的String構造,內存分配地址是不同的。編譯期間確定的String是分配在常量區的,運行期間初始化是分配在堆上的,從char[]初始化以及+拼接生成的String對象以及內部value是分配至堆上的。字符串常量區有且僅有一份字符串,不會出現重複的字符串,但是堆上可能存在多份一樣的字符串。