開始寫 Java 一年來,一直都是遇到什么問題再去解決,還沒有主動的深入的去學習過 Java 語言的特性和深入閱讀 JDK 的源碼。既然決定今后靠 Java
吃飯,還是得花些心思在上面,放棄一些打游戲的時間,系統深入的去學習。
Java String 是 Java 編程中最常用的類之一,也是 JDK 提供的最基礎的類。所以我決定先從 String 類入手,深入的研究一番來開個好頭。
類定義與類成員
打開 JDK 中的 String 源碼,最先應當關注 String 類的定義。
public final class String
implements java.io.Serializable, Comparable, CharSequence
不可繼承與不可變
寫過 Java 的人都知道, 當 final 關鍵字修飾類時,代表此類不可繼承。所以 String 類是不能被外部繼承。這時候我們可能會好奇,String 的設計者
為什么要把它設計成不可繼承的呢。我在知乎上找到了相關的問題和討論,
我覺得首位的回答已經說的很明白了。String 做為 Java 的最基礎的引用數據類型,最重要的一點就是不可變性,所以使用 final 就是為了**禁止繼承
破壞了 String 的不可變的性質**。
實現類的不可變性,不光是用 final 修飾類這么簡單,從源碼中可以看到,String 實際上是對一個字符數組的封裝,而字符數組是私有的,并且沒有提供
任何可以修改字符數組的方法,所以一旦初始化完成, String 對象便無法被修改。
序列化
從上面的類定義中我們看到了 String 實現了序列化的接口 Serializable,所以 String 是支持序列化和反序列化的。
什么是Java對象的序列化?相信很多和我一樣的 Java 菜鳥都有這樣疑問。深入分析Java的序列化與反序列化這篇文章中的這一段話
解釋的很好。
Java平臺允許我們在內存中創建可復用的Java對象,但一般情況下,
只有當JVM處于運行時,這些對象才可能存在,
即,這些對象的生命周期不會比JVM的生命周期更長。但在現實應用中,
就可能要求在JVM停止運行之后能夠保存(持久化)指定的對象,并在將來重新讀取被保存的對象。
Java對象序列化就能夠幫助我們實現該功能。
使用Java對象序列化,在保存對象時,會把其狀態保存為一組字節,在未來,再將這些字節組裝成對象。
必須注意地是,對象序列化保存的是對象的”狀態”,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量。
除了在持久化對象時會用到對象序列化之外,當使用RMI(遠程方法調用),或在網絡中傳遞對象時,都會用到對象序列化。
Java序列化API為處理對象序列化提供了一個標準機制,該API簡單易用。
在 String 源碼中,我們也可以看到支持序列化的類成員定義。
/** 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
* Object Serialization Specification, Section 6.2, "Stream Elements"
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
serialVersionUID 是一個序列化版本號,Java 通過這個 UID 來判定反序列化時的字節流與本地類的一致性,如果相同則認為一致,
可以進行反序列化,如果不同就會拋出異常。
serialPersistentFields 這個定義則比上一個少見許多,大概猜到是與序列化時的類成員有關系。為了弄懂這個字段的意義,我 google 百度齊上,也
僅僅只找到了 JDK 文檔對類 ObjectStreamField的一丁點描述, `A description of a Serializable field from a Serializable class.
An array of ObjectStreamFields is used to declare the Serializable fields of a class.` 大意是這個類用來描述序列化類的一個序列化字段,
如果定義一個此類的數組則可以聲明類需要被序列化的字段。但是還是沒有找到這個類的具體用法和作用是怎樣的。后來我仔細看了一下這個字段的定義,
與 serialVersionUID 應該是同樣通過具體字段名來定義各種規則的,然后我直接搜索了關鍵字 serialPersistentFields,終于找到了它的具體作用。
即,**默認序列化自定義包括關鍵字 transient 和靜態字段名 serialPersistentFields,transient 用于指定哪個字段不被默認序列化,
serialPersistentFields 用于指定哪些字段需要被默認序列化。如果同時定義了 serialPersistentFields 與 transient,transient 會被忽略。**
我自己也測試了一下,確實是這個效果。
知道了 serialPersistentFields 的作用以后,問題又來了,既然這個靜態字段是用來定義參與序列化的類成員的,那為什么在 String 中這個數組的長度定義為0?
經過一番搜索查找資料以后,還是沒有找到一個明確的解釋,期待如果有大佬看到能解答一下。
可排序
String 類還實現了 Comparable 接口,Comparable接口只有一個方法 public int compareTo(T o),實現了這個接口就意味著該類支持排序,
即可用 Collections.sort 或 Arrays.sort 等方法對該類的對象列表或數組進行排序。
在 String 中我們還可以看到這樣一個靜態變量,
public static final Comparator CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
從上面的源碼中可以看出,這個靜態成員是一個實現了 Comparator 接口的類的實例,而實現這個類的作用是比較兩個忽略大小寫的 String 的大小。
那么 Comparable 和 Comparator 有什么區別和聯系呢?同時 String 又為什么要兩個都實現一遍呢?
第一個問題這里就不展開了,總結一下就是,Comparable 是類的內部實現,一個類能且只能實現一次,而 Comparator 則是外部實現,可以通過不改變
類本身的情況下,為類增加更多的排序功能。
所以我們也可以為 String 實現一個 Comparator使用,具體可以參考Comparable與Comparator的區別這篇文章。
String 實現了兩種比較方法的意圖,實際上是一目了然的。實現 Comparable 接口為類提供了標準的排序方案,同時為了滿足大多數排序需求的忽略大小寫排序的情況,
String 再提供一個 Comparator 到公共靜態類成員中。如果還有其他的需求,那就只能我們自己實現了。
類方法
String 的方法大致可以分為以下幾類。
構造方法
功能方法
工廠方法
intern方法
關于 String 的方法的解析,這篇文章已經解析的夠好了,所以我這里也不再重復的說一遍了。不過
最后的 intern 方法值得我們去研究。
intern方法
字符串常量池
String 做為 Java 的基礎類型之一,可以使用字面量的形式去創建對象,例如 String s = "hello"。當然也可以使用 new 去創建 String 的對象,
但是幾乎很少看到這樣的寫法,久而久之我便習慣了第一種寫法,但是卻不知道背后大有學問。下面一段代碼可以看出他們的區別。
public class StringConstPool {
public static void main(String[] args) {
String s1 = "hello world";
String s2 = new String("hello world");
String s3 = "hello world";
String s4 = new String("hello world");
String s5 = "hello " + "world";
String s6 = "hel" + "lo world";
String s7 = "hello";
String s8 = s7 + " world";
System.out.println("s1 == s2: " + String.valueOf(s1 == s2) );
System.out.println("s1.equals(s2): " + String.valueOf(s1.equals(s2)));
System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
System.out.println("s1.equals(s3): " + String.valueOf(s1.equals(s3)));
System.out.println("s2 == s4: " + String.valueOf(s2 == s4));
System.out.println("s2.equals(s4): " + String.valueOf(s2.equals(s4)));
System.out.println("s5 == s6: " + String.valueOf(s5 == s6));
System.out.println("s1 == s8: " + String.valueOf(s1 == s8));
}
}
/* output
s1 == s2: false
s1.equals(s2): true
s1 == s3: true
s1.equals(s3): true
s2 == s4: false
s2.equls(s4): true
s5 == s6: true
s1 == s8: false
*/
從這段代碼的輸出可以看到,equals 比較的結果都是 true,這是因為 String 的 equals 比較的值( Object 對象的默認 equals 實現是比較引用,
String 對此方法進行了重寫)。== 比較的是兩個對象的引用,如果引用相同則返回 true,否則返回 false。s1==s2: false和 s2==s4: false
說明了 new 一個對象一定會生成一個新的引用返回。s1==s3: true 則證明了使用字面量創建對象同樣的字面量會得到同樣的引用。
s5 == s6 實際上和 s1 == s3 在 JVM 眼里是一樣的情況,因為早在編譯階段,這種常量的簡單運算就已經完成了。我們可以使用 javap 反編譯一下 class 文件去查看
編譯后的情況。
? ~ javap -c StringConstPool.class
Compiled from "StringConstPool.java"
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello world
2: astore_1
3: return
}
看不懂匯編也沒關系,因為注釋已經很清楚了......
s1 == s8 的情況就略復雜,s8 是通過變量的運算而得,所以無法在編譯時直接算出其值。而 Java 又不能重載運算符,所以我們在 JDK 的源碼里也
找不到相關的線索。萬事不絕反編譯,我們再通過反編譯看看實際上編譯器對此是否有影響。
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String world
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
通過反編譯的結果可以發現,String 的變量運算實際上在編譯后是由 StringBuilder 實現的,s8 = s7 + " world" 的代碼等價于
(new StringBuilder(s7)).append(" world").toString()。Stringbuilder 是可變的類,通過 append 方法 和 toString 將兩個 String 對象聚合
成一個新的 String 對象,所以到這里就不難理解為什么 s1 == s8 : false 了。
之所以會有以上的效果,是因為有字符串常量池的存在。字符串對象的分配和其他對象一樣是要付出時間和空間代價,而字符串又是程序中最常用的對象,JVM
為了提高性能和減少內存占用,引入了字符串的常量池,在使用字面量創建對象時, JVM 首先會去檢查常量池,如果池中有現成的對象就直接返回它的引用,如果
沒有就創建一個對象,并放到池里。因為字符串不可變的特性,所以 JVM 不用擔心多個變量引用同一個對象會改變對象的狀態。同時運行時實例創建的全局
字符串常量池中有一個表,總是為池中的每個字符串對象維護一個引用,所以這些對象不會被 GC 。
intern 方法的作用
上面說了很多都沒有涉及到主題 intern 方法,那么 intern 方法到作用到底是什么呢?首先查看一下源碼。
/**
* Returns a canonical representation for the string object.
*
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
*
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
*
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* The Java? Language Specification.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
Oracle JDK 中,intern 方法被 native 關鍵字修飾并且沒有實現,這意味著這部分到實現是隱藏起來了。從注釋中看到,這個方法的作用是如果常量池
中存在當前字符串,就會直接返回當前字符串,如果常量池中沒有此字符串,會將此字符串放入常量池中后再返回。通過注釋的介紹已經可以明白這個方法的作用了,
再用幾個例子證明一下。
public class StringConstPool {
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern();
System.out.println("s1 == s2: " + String.valueOf(s1 == s2));
System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
}
}
/* output
s1 == s2: false
s1 == s3: true
*/
這里就很容易的了解 intern 實際上就是把普通的字符串對象也關聯到常量池中。
當然 intern 的實現原理和最佳實踐等也是需要理解學習的,美團技術團隊的這篇深入解析String#intern
很深入也很詳細,推薦閱讀。