Java之String
開篇
下面這段代碼的輸出:
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
System.out.println(str1==str2);
System.out.println(str2==str3);
System.out.println(str1==str3);
String對象的內(nèi)部實現(xiàn)
圖示:
- 在 Java6 以及之前的版本中秘案,String 對象是對 char 數(shù)組進(jìn)行了封裝實現(xiàn)的對象,主要有四個成員變量:char 數(shù)組、偏移量 offset、字符數(shù)量 count、哈希值 hash倡勇。
String 對象是通過 offset 和 count 兩個屬性來定位 char[] 數(shù)組,獲取字符串嘉涌。這么做可以高效妻熊、快速地共享數(shù)組對象,同時節(jié)省內(nèi)存空間仑最,但這種方式很有可能會導(dǎo)致內(nèi)存泄漏扔役。
從 Java7 版本開始到 Java8 版本,Java 對 String 類做了一些改變警医。String 類中不再有 offset 和 count 兩個變量了亿胸。這樣的好處是 String 對象占用的內(nèi)存稍微少了些,同時,String.substring 方法也不再共享 char[]损敷,從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題葫笼。
從 Java9 版本開始,將 char[] 字段改為了 byte[] 字段拗馒,又維護(hù)了一個新的屬性 coder路星,它是一個編碼格式的標(biāo)識。
為什么這樣修改诱桂?
一個 char 字符占 16 位洋丐,2 個字節(jié)。這個情況下挥等,存儲單字節(jié)編碼內(nèi)的字符(占一個字節(jié)的字符)就顯得非常浪費友绝。JDK1.9 的 String 類為了節(jié)約內(nèi)存空間,于是使用了占 8 位肝劲,1 個字節(jié)的 byte 數(shù)組來存放字符串迁客。
而新屬性 coder 的作用是,在計算字符串長度或者使用 indexOf()函數(shù)時辞槐,我們需要根據(jù)這個字段掷漱,判斷如何計算字符串長度。coder 屬性默認(rèn)有 0 和 1 兩個值榄檬,0 代表 Latin-1(單字節(jié)編碼)卜范,1 代表 UTF-16。如果 String 判斷字符串只包含了 Latin-1鹿榜,則 coder 屬性值為 0海雪,反之則為 1。
String 對象的不可變性
String 類被 final 關(guān)鍵字修飾了舱殿,而且變量 char 數(shù)組也被 final 修飾了
類被 final 修飾代表該類不可繼承奥裸,而 char[] 被 final+private 修飾,代表了 String 對象不可被更改怀薛。Java 實現(xiàn)的這個特性叫作 String 對象的不可變性刺彩,即 String 對象一旦創(chuàng)建成功,就不能再對它進(jìn)行改變枝恋。
優(yōu)點
保證 String 對象的安全性创倔。假設(shè) String 對象是可變的,那么 String 對象將可能被惡意修改焚碌。
保證 hash 屬性值不會頻繁變更畦攘,確保了唯一性,使得類似 HashMap 容器才能實現(xiàn)相應(yīng)的 key-value 緩存功能十电。
可以實現(xiàn)字符串常量池知押。在 Java 中叹螟,通常有兩種創(chuàng)建字符串對象的方式,一種是通過字符串常量的方式創(chuàng)建台盯,如 String str=“abc”罢绽;另一種是字符串變量通過 new 形式的創(chuàng)建,如 String str = new String(“abc”)静盅。
當(dāng)代碼中使用第一種方式創(chuàng)建字符串對象時良价,JVM 首先會檢查該對象是否在字符串常量池中,如果在蒿叠,就返回該對象引用明垢,否則新的字符串將在常量池中被創(chuàng)建。這種方式可以減少同一個值的字符串對象的重復(fù)創(chuàng)建市咽,節(jié)約內(nèi)存痊银。
String str = new String(“abc”) 這種方式,首先在編譯類文件時施绎,"abc"常量字符串將會放入到常量結(jié)構(gòu)中溯革,在類加載時,“abc"將會在常量池中創(chuàng)建粘姜;其次鬓照,在調(diào)用 new 時,JVM 命令將會調(diào)用 String 的構(gòu)造函數(shù)孤紧,同時引用常量池中的"abc” 字符串,在堆內(nèi)存中創(chuàng)建一個 String 對象拒秘;最后号显,str 將引用 String 對象。
使用
字符串常量的累計
public static void main(String[] args) {
//字符串常量的累計
String s = "a" + "b" + "c";
System.out.println(s);
}
首先會生成 a 對象躺酒,再生成 ab 對象押蚤,最后生成 abc 對象,從理論上來說羹应,這段代碼是低效的揽碘。
實際運行中,發(fā)現(xiàn)只有一個對象生成
查看字節(jié)碼园匹,編譯器自動優(yōu)化了這行代碼
// public static void main(java.lang.String[]);
// descriptor: ([Ljava/lang/String;)V
// flags: ACC_PUBLIC, ACC_STATIC
// Code:
// stack=2, locals=2, args_size=1
// 0: ldc #2 // String abc
// 2: astore_1
// 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
// 6: aload_1
// 7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 10: return
字符串變量的累計
public static void main(String[] args) {
//字符串變量的累計
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
}
反編譯class文件:
//public class T5
//{
//
// public T5()
// {
// }
//
// public static void main(String args[])
// {
// String str = "abcdef";
// for(int i = 0; i < 1000; i++)
// str = (new StringBuilder()).append(str).append(i).toString();
//
// }
編譯器同樣對這段代碼進(jìn)行了優(yōu)化雳刺。Java 在進(jìn)行字符串的拼接時,偏向使用StringBuilder裸违,這樣可以提高程序的效率掖桦。
String.intern
JDK文檔:
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @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();
從注釋中看到,這個方法的作用是如果常量池 中存在當(dāng)前字符串供汛,就會直接返回當(dāng)前字符串枪汪,如果常量池中沒有此字符串涌穆,會將此字符串放入常量池中后再返回
例子
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
/*
false
true
*/
}
圖示:
圖中綠色線條代表 string 對象的內(nèi)容指向。 藍(lán)色線條代表地址指向雀久。
jdk7 的版本中宿稀,字符串常量池已經(jīng)從 Perm 區(qū)移到正常的 Java Heap 區(qū)域
s3和s4字符串
String s3 = new String("1") + new String("1");,這句代碼中現(xiàn)在生成了2最終個對象赖捌,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的對象原叮。中間還有2個匿名的new String("1")我們不去討論它們。此時s3引用對象內(nèi)容是”11”巡蘸,但此時常量池中是沒有 “11”對象的奋隶。
接下來s3.intern();這一句代碼,是將 s3中的“11”字符串放入 String 常量池中悦荒,因為此時常量池中不存在“11”字符串唯欣,所以在常量池中生成一個 “11” 的對象,關(guān)鍵點是 jdk7 中常量池不在 Perm 區(qū)域了搬味,這塊做了調(diào)整境氢。常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用碰纬。這份引用指向 s3 引用的對象萍聊。 也就是說引用地址是相同的。
最后String s4 = "11"; 這句代碼中”11”是顯示聲明的悦析,因此會直接去常量池中創(chuàng)建寿桨,創(chuàng)建的時候發(fā)現(xiàn)已經(jīng)有這個對象了,此時也就是指向 s3 引用對象的一個引用强戴。所以 s4 引用就指向和 s3 一樣了亭螟。因此最后的比較 s3 == s4 是 true
s 和 s2 對象
String s = new String("1"); 第一句代碼,生成了2個對象骑歹。常量池中的“1” 和 JAVA Heap 中的字符串對象预烙。s.intern(); 這一句是 s 對象去常量池中尋找后發(fā)現(xiàn) “1” 已經(jīng)在常量池里了。
接下來String s2 = "1"; 這句代碼是生成一個 s2的引用指向常量池中的“1”對象道媚。 結(jié)果就是 s 和 s2 的引用地址明顯不同
調(diào)整下代碼:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
/*
false
false
*/
}
圖示:
圖中綠色線條代表 string 對象的內(nèi)容指向扁掸。 藍(lán)色線條代表地址指向。
第一段代碼和第二段代碼的改變就是 s3.intern(); 的順序是放在String s4 = "11";后了最域。這樣谴分,首先執(zhí)行String s4 = "11";聲明 s4 的時候常量池中是不存在“11”對象的,執(zhí)行完畢后羡宙,“11“對象是 s4 聲明產(chǎn)生的新對象狸剃。然后再執(zhí)行s3.intern();時,常量池中“11”對象已經(jīng)存在了狗热,因此 s3 和 s4 的引用是不同的钞馁。
第二段代碼中的 s 和 s2 代碼中虑省,s.intern();,這一句往后放也不會有什么影響了僧凰,因為對象池中在執(zhí)行第一句代碼String s = new String("1");的時候已經(jīng)生成“1”對象了探颈。下邊的s2聲明都是直接從常量池中取地址引用的。 s 和 s2 的引用地址是不會相等的训措。
參考資料
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html