摘要:所以我決定先從類入手,深入的研究一番來開個(gè)好頭。之所以會(huì)有以上的效果,是因?yàn)橛凶址A砍氐拇嬖凇M瑫r(shí)運(yùn)行時(shí)實(shí)例創(chuàng)建的全局字符串常量池中有一個(gè)表,總是為池中的每個(gè)字符串對(duì)象維護(hù)一個(gè)引用,所以這些對(duì)象不會(huì)被。
開始寫 Java 一年來,一直都是遇到什么問題再去解決,還沒有主動(dòng)的深入的去學(xué)習(xí)過 Java 語言的特性和深入閱讀 JDK 的源碼。既然決定今后靠 Java
吃飯,還是得花些心思在上面,放棄一些打游戲的時(shí)間,系統(tǒng)深入的去學(xué)習(xí)。
Java String 是 Java 編程中最常用的類之一,也是 JDK 提供的最基礎(chǔ)的類。所以我決定先從 String 類入手,深入的研究一番來開個(gè)好頭。
類定義與類成員打開 JDK 中的 String 源碼,最先應(yīng)當(dāng)關(guān)注 String 類的定義。
public final class String implements java.io.Serializable, Comparable不可繼承與不可變, CharSequence
寫過 Java 的人都知道, 當(dāng) final 關(guān)鍵字修飾類時(shí),代表此類不可繼承。所以 String 類是不能被外部繼承。這時(shí)候我們可能會(huì)好奇,String 的設(shè)計(jì)者
為什么要把它設(shè)計(jì)成不可繼承的呢。我在知乎上找到了相關(guān)的問題和討論,
我覺得首位的回答已經(jīng)說的很明白了。String 做為 Java 的最基礎(chǔ)的引用數(shù)據(jù)類型,最重要的一點(diǎn)就是不可變性,所以使用 final 就是為了**禁止繼承
破壞了 String 的不可變的性質(zhì)**。
實(shí)現(xiàn)類的不可變性,不光是用 final 修飾類這么簡(jiǎn)單,從源碼中可以看到,String 實(shí)際上是對(duì)一個(gè)字符數(shù)組的封裝,而字符數(shù)組是私有的,并且沒有提供
任何可以修改字符數(shù)組的方法,所以一旦初始化完成, String 對(duì)象便無法被修改。
從上面的類定義中我們看到了 String 實(shí)現(xiàn)了序列化的接口 Serializable,所以 String 是支持序列化和反序列化的。
什么是Java對(duì)象的序列化?相信很多和我一樣的 Java 菜鳥都有這樣疑問。深入分析Java的序列化與反序列化這篇文章中的這一段話
解釋的很好。
Java平臺(tái)允許我們?cè)趦?nèi)存中創(chuàng)建可復(fù)用的Java對(duì)象,但一般情況下,
只有當(dāng)JVM處于運(yùn)行時(shí),這些對(duì)象才可能存在,
即,這些對(duì)象的生命周期不會(huì)比JVM的生命周期更長。但在現(xiàn)實(shí)應(yīng)用中,
就可能要求在JVM停止運(yùn)行之后能夠保存(持久化)指定的對(duì)象,并在將來重新讀取被保存的對(duì)象。
Java對(duì)象序列化就能夠幫助我們實(shí)現(xiàn)該功能。
使用Java對(duì)象序列化,在保存對(duì)象時(shí),會(huì)把其狀態(tài)保存為一組字節(jié),在未來,再將這些字節(jié)組裝成對(duì)象。
必須注意地是,對(duì)象序列化保存的是對(duì)象的”狀態(tài)”,即它的成員變量。由此可知,對(duì)象序列化不會(huì)關(guān)注類中的靜態(tài)變量。
除了在持久化對(duì)象時(shí)會(huì)用到對(duì)象序列化之外,當(dāng)使用RMI(遠(yuǎn)程方法調(diào)用),或在網(wǎng)絡(luò)中傳遞對(duì)象時(shí),都會(huì)用到對(duì)象序列化。
Java序列化API為處理對(duì)象序列化提供了一個(gè)標(biāo)準(zhǔn)機(jī)制,該API簡(jiǎn)單易用。
在 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 是一個(gè)序列化版本號(hào),Java 通過這個(gè) UID 來判定反序列化時(shí)的字節(jié)流與本地類的一致性,如果相同則認(rèn)為一致,
可以進(jìn)行反序列化,如果不同就會(huì)拋出異常。
serialPersistentFields 這個(gè)定義則比上一個(gè)少見許多,大概猜到是與序列化時(shí)的類成員有關(guān)系。為了弄懂這個(gè)字段的意義,我 google 百度齊上,也
僅僅只找到了 JDK 文檔對(duì)類 ObjectStreamField的一丁點(diǎn)描述, `A description of a Serializable field from a Serializable class.
An array of ObjectStreamFields is used to declare the Serializable fields of a class.` 大意是這個(gè)類用來描述序列化類的一個(gè)序列化字段,
如果定義一個(gè)此類的數(shù)組則可以聲明類需要被序列化的字段。但是還是沒有找到這個(gè)類的具體用法和作用是怎樣的。后來我仔細(xì)看了一下這個(gè)字段的定義,
與 serialVersionUID 應(yīng)該是同樣通過具體字段名來定義各種規(guī)則的,然后我直接搜索了關(guān)鍵字 serialPersistentFields,終于找到了它的具體作用。
即,**默認(rèn)序列化自定義包括關(guān)鍵字 transient 和靜態(tài)字段名 serialPersistentFields,transient 用于指定哪個(gè)字段不被默認(rèn)序列化,
serialPersistentFields 用于指定哪些字段需要被默認(rèn)序列化。如果同時(shí)定義了 serialPersistentFields 與 transient,transient 會(huì)被忽略。**
我自己也測(cè)試了一下,確實(shí)是這個(gè)效果。
知道了 serialPersistentFields 的作用以后,問題又來了,既然這個(gè)靜態(tài)字段是用來定義參與序列化的類成員的,那為什么在 String 中這個(gè)數(shù)組的長度定義為0?
經(jīng)過一番搜索查找資料以后,還是沒有找到一個(gè)明確的解釋,期待如果有大佬看到能解答一下。
String 類還實(shí)現(xiàn)了 Comparable 接口,Comparable
即可用 Collections.sort 或 Arrays.sort 等方法對(duì)該類的對(duì)象列表或數(shù)組進(jìn)行排序。
在 String 中我們還可以看到這樣一個(gè)靜態(tài)變量,
public static final ComparatorCASE_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; } }
從上面的源碼中可以看出,這個(gè)靜態(tài)成員是一個(gè)實(shí)現(xiàn)了 Comparator 接口的類的實(shí)例,而實(shí)現(xiàn)這個(gè)類的作用是比較兩個(gè)忽略大小寫的 String 的大小。
那么 Comparable 和 Comparator 有什么區(qū)別和聯(lián)系呢?同時(shí) String 又為什么要兩個(gè)都實(shí)現(xiàn)一遍呢?
第一個(gè)問題這里就不展開了,總結(jié)一下就是,Comparable 是類的內(nèi)部實(shí)現(xiàn),一個(gè)類能且只能實(shí)現(xiàn)一次,而 Comparator 則是外部實(shí)現(xiàn),可以通過不改變
類本身的情況下,為類增加更多的排序功能。
所以我們也可以為 String 實(shí)現(xiàn)一個(gè) Comparator使用,具體可以參考Comparable與Comparator的區(qū)別這篇文章。
String 實(shí)現(xiàn)了兩種比較方法的意圖,實(shí)際上是一目了然的。實(shí)現(xiàn) Comparable 接口為類提供了標(biāo)準(zhǔn)的排序方案,同時(shí)為了滿足大多數(shù)排序需求的忽略大小寫排序的情況,
String 再提供一個(gè) Comparator 到公共靜態(tài)類成員中。如果還有其他的需求,那就只能我們自己實(shí)現(xiàn)了。
String 的方法大致可以分為以下幾類。
構(gòu)造方法
功能方法
工廠方法
intern方法
關(guān)于 String 的方法的解析,這篇文章已經(jīng)解析的夠好了,所以我這里也不再重復(fù)的說一遍了。不過
最后的 intern 方法值得我們?nèi)パ芯俊?/p>
intern方法
字符串常量池
String 做為 Java 的基礎(chǔ)類型之一,可以使用字面量的形式去創(chuàng)建對(duì)象,例如 String s = "hello"。當(dāng)然也可以使用 new 去創(chuàng)建 String 的對(duì)象,
但是幾乎很少看到這樣的寫法,久而久之我便習(xí)慣了第一種寫法,但是卻不知道背后大有學(xué)問。下面一段代碼可以看出他們的區(qū)別。
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 比較的結(jié)果都是 true,這是因?yàn)?String 的 equals 比較的值( Object 對(duì)象的默認(rèn) equals 實(shí)現(xiàn)是比較引用,
String 對(duì)此方法進(jìn)行了重寫)。== 比較的是兩個(gè)對(duì)象的引用,如果引用相同則返回 true,否則返回 false。s1==s2: false和 s2==s4: false
說明了 new 一個(gè)對(duì)象一定會(huì)生成一個(gè)新的引用返回。s1==s3: true 則證明了使用字面量創(chuàng)建對(duì)象同樣的字面量會(huì)得到同樣的引用。
s5 == s6 實(shí)際上和 s1 == s3 在 JVM 眼里是一樣的情況,因?yàn)樵缭诰幾g階段,這種常量的簡(jiǎn)單運(yùn)算就已經(jīng)完成了。我們可以使用 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 }
看不懂匯編也沒關(guān)系,因?yàn)樽⑨屢呀?jīng)很清楚了......
s1 == s8 的情況就略復(fù)雜,s8 是通過變量的運(yùn)算而得,所以無法在編譯時(shí)直接算出其值。而 Java 又不能重載運(yùn)算符,所以我們?cè)?JDK 的源碼里也
找不到相關(guān)的線索。萬事不絕反編譯,我們?cè)偻ㄟ^反編譯看看實(shí)際上編譯器對(duì)此是否有影響。
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 }
通過反編譯的結(jié)果可以發(fā)現(xiàn),String 的變量運(yùn)算實(shí)際上在編譯后是由 StringBuilder 實(shí)現(xiàn)的,s8 = s7 + " world" 的代碼等價(jià)于
(new StringBuilder(s7)).append(" world").toString()。Stringbuilder 是可變的類,通過 append 方法 和 toString 將兩個(gè) String 對(duì)象聚合
成一個(gè)新的 String 對(duì)象,所以到這里就不難理解為什么 s1 == s8 : false 了。
之所以會(huì)有以上的效果,是因?yàn)橛凶址A砍氐拇嬖凇W址畬?duì)象的分配和其他對(duì)象一樣是要付出時(shí)間和空間代價(jià),而字符串又是程序中最常用的對(duì)象,JVM
為了提高性能和減少內(nèi)存占用,引入了字符串的常量池,在使用字面量創(chuàng)建對(duì)象時(shí), JVM 首先會(huì)去檢查常量池,如果池中有現(xiàn)成的對(duì)象就直接返回它的引用,如果
沒有就創(chuàng)建一個(gè)對(duì)象,并放到池里。因?yàn)樽址豢勺兊奶匦裕?JVM 不用擔(dān)心多個(gè)變量引用同一個(gè)對(duì)象會(huì)改變對(duì)象的狀態(tài)。同時(shí)運(yùn)行時(shí)實(shí)例創(chuàng)建的全局
字符串常量池中有一個(gè)表,總是為池中的每個(gè)字符串對(duì)象維護(hù)一個(gè)引用,所以這些對(duì)象不會(huì)被 GC 。
上面說了很多都沒有涉及到主題 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 關(guān)鍵字修飾并且沒有實(shí)現(xiàn),這意味著這部分到實(shí)現(xiàn)是隱藏起來了。從注釋中看到,這個(gè)方法的作用是如果常量池
中存在當(dāng)前字符串,就會(huì)直接返回當(dāng)前字符串,如果常量池中沒有此字符串,會(huì)將此字符串放入常量池中后再返回。通過注釋的介紹已經(jīng)可以明白這個(gè)方法的作用了,
再用幾個(gè)例子證明一下。
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 實(shí)際上就是把普通的字符串對(duì)象也關(guān)聯(lián)到常量池中。
當(dāng)然 intern 的實(shí)現(xiàn)原理和最佳實(shí)踐等也是需要理解學(xué)習(xí)的,美團(tuán)技術(shù)團(tuán)隊(duì)的這篇深入解析String#intern
很深入也很詳細(xì),推薦閱讀。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/77126.html
摘要:一名年工作經(jīng)驗(yàn)的程序員應(yīng)該具備的技能,這可能是程序員們比較關(guān)心的內(nèi)容。數(shù)據(jù)結(jié)構(gòu)和算法分析數(shù)據(jù)結(jié)構(gòu)和算法分析,對(duì)于一名程序員來說,會(huì)比不會(huì)好而且在工作中能派上用場(chǎng)。 一名3年工作經(jīng)驗(yàn)的Java程序員應(yīng)該具備的技能,這可能是Java程序員們比較關(guān)心的內(nèi)容。我這里要說明一下,以下列舉的內(nèi)容不是都要會(huì)的東西—-但是如果你掌握得越多,最終能得到的評(píng)價(jià)、拿到的薪水勢(shì)必也越高。 1、基本語法 這包括...
摘要:什么是字節(jié)碼程序通過編譯之后生成文件就是字節(jié)碼集合正是有這樣一種中間碼字節(jié)碼,使得等函數(shù)語言只用實(shí)現(xiàn)一個(gè)編譯器即可運(yùn)行在上。 什么是字節(jié)碼? java程序通過javac編譯之后生成文件.class就是字節(jié)碼集合,正是有這樣一種中間碼(字節(jié)碼),使得scala/groovy/clojure等函數(shù)語言只用實(shí)現(xiàn)一個(gè)編譯器即可運(yùn)行在JVM上。看看一段簡(jiǎn)單代碼。 public long ...
摘要:此的功能是從應(yīng)用程序中隱藏在底層存儲(chǔ)機(jī)制中執(zhí)行操作所涉及的所有復(fù)雜性。這正是模式試圖解決的問題。將模式與一起使用開發(fā)人員普遍認(rèn)為的發(fā)布將模式的功能降級(jí)為零,因?yàn)樵撃J街皇菍?shí)體經(jīng)理提供的另一層抽象和復(fù)雜性。在這種情況下,模式有其自己的位置。 案例概述 數(shù)據(jù)訪問對(duì)象(DAO)模式是一種結(jié)構(gòu)模式,它允許我們使用抽象API將應(yīng)用程序/業(yè)務(wù)層與持久層(通常是關(guān)系數(shù)據(jù)庫,但它可以是任何其他持久性機(jī)...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00