String 是Java編程中的引用類型,不屬于基本類型,默認(rèn)值為null,在Java中是用來創(chuàng)建于操作字符串。
源碼如下所示:
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
說明:String的底層是由char 數(shù)組實(shí)現(xiàn)的漆羔。我們創(chuàng)建的字符串都是由char數(shù)組保存的,由于使用final修飾狱掂,因此char數(shù)組的引用不可變演痒,但并不代表數(shù)組內(nèi)容不可變,因此為了實(shí)現(xiàn)真正的數(shù)組不可變趋惨,還加上了private修飾符鸟顺。
問題1:從你的角度談?wù)凷tring 為什么要設(shè)計成不可變的?
本問題選自 從你的角度談?wù)凷tring 為什么要設(shè)計成不可變的器虾?
何為不可變讯嫂?
對于Java而言,除了基本類型兆沙,其余都是對象欧芽。在《JAVA并發(fā)編程實(shí)踐》一書中對 "不可變"給出了一個粗略的定義:
①其狀態(tài)不能再創(chuàng)建后再修改
②所有域都是final類型。
③在構(gòu)造函數(shù)構(gòu)造對象期間葛圃,this引用沒有泄漏千扔。
注:一憎妙,在第二點(diǎn)中,一個對象所有域都是final類型曲楚,該對象也有可能是可變對象厘唾。因為final關(guān)鍵字只是限制對象的域的引用不可變,但是無法限制通過該引用去修改其內(nèi)部狀態(tài)龙誊。二抚垃,不可變對象內(nèi)部域并不一定全部聲明是final類型,比如String類中的有一個名為 hash 的域并不是final類型载迄,這是因為①String類型的惰性計算 hashcode并存儲在hash域中,通過其他final類型域來保證每次的hashcode計算結(jié)果必定是相同的抡蛙。②每次對String類型的所有改變內(nèi)部存儲結(jié)構(gòu)的操作都會new一個新的String對象护昧。
不可變帶來的好處
安全性
由于String的不可變,那么在多線程的情況下對String的任何操作都是安全的粗截。
類加載器要用到字符串惋耙,不可變形提供了安全性,以便正確的類被加載熊昌。
使用常量池節(jié)省空間
只有當(dāng)字符串不可變時绽榛,字符串常量池才有可能實(shí)現(xiàn)。當(dāng)不同的字符串常量指向池中同一個字符串時婿屹,將會大大的降低heap使用空間灭美,String的intern方法才可能實(shí)現(xiàn)。
緩存hashcode
因為字符串的不可變昂利,在它創(chuàng)建的時候hashcode就被緩存了届腐,不需要重新計算,這使得Map中的鍵很適合做Map集合的鍵蜂奸,字符串的處理速度要快過其他的鍵對象犁苏。
對于String 源碼的 hash有這樣的說明
private int hash;//this is used to cache hash code.
不可變帶來的壞處
由于不可變,對字符串使用完后就扔扩所,所以會創(chuàng)建大量的對象垃圾围详。
密碼應(yīng)該存儲在字符數(shù)組中而不是String中
參考 為什么Java中的密碼優(yōu)先使用 char[] 而不是String?
由于String在Java中是不可變的祖屏,如果你將密碼以明文的形式保存成字符串助赞,那么它將一直留在內(nèi)存中,直到垃圾收集器把它清除袁勺。而由于字符串被放在字符串緩沖池中以方便重復(fù)使用嫉拐,所以它就可能在內(nèi)存中被保留很長時間,而這將導(dǎo)致安全隱患魁兼,因為任何能夠訪問內(nèi)存(memory dump內(nèi)存轉(zhuǎn)儲)的人都能清晰的看到文本中的密碼婉徘,這也是為什么你應(yīng)該總是使用加密的形式而不是明文來保存密碼漠嵌。由于字符串是不可變的,所以沒有任何方式可以修改字符串的值盖呼,因為每次修改都將產(chǎn)生新的字符串儒鹿,然而如果你使用char[]來保存密碼,你仍然可以將其中所有的元素都設(shè)置為空或者零几晤。所以將密碼保存到字符數(shù)組中很明顯的降低了密碼被竊取的風(fēng)險约炎。
當(dāng)然只使用字符數(shù)組也是不夠的,為了更安全你需要將數(shù)組內(nèi)容進(jìn)行轉(zhuǎn)化蟹瘾。 建議使用哈希的或者是加密過的密碼而不是明文圾浅,然后一旦完成驗證,就將它從內(nèi)存中清除掉憾朴。
問題2 String str = "Hello"與String str = new String("Hello")創(chuàng)建幾個對象
問題相關(guān):https://www.zhihu.com/question/29884421
Java虛擬機(jī)管理的內(nèi)存最主要的有三塊
1 堆(Heap):最大的一塊內(nèi)存狸捕。存在對象實(shí)例和數(shù)組。全局共享众雷。
2 棧(Stack):全稱“虛擬機(jī)棧(JVM Stacks)”灸拍。存放基本型,對象引用砾省。線程私有鸡岗。
3 方法區(qū)(Method Area):“類”被加載后的信息,常量编兄,靜態(tài)變量存放在這里轩性。全局共享。在HotSpot里也叫“永生代”狠鸳,但兩者不能等同炮姨。
這三塊區(qū)域的詳細(xì)劃分如下所示
注圖為JDK1.6及之前的版本,1.7版本以后將HotSpot的Interned Strings 移到了Heap堆中碰煌,1.8版本徹底取消了永久代
主要看圖中的Stack棧區(qū)里的“局部變量表(Local Variables)”和“操作數(shù)棧(Operand Stack)”舒岸。因為棧是線程私有的,每個方法被執(zhí)行的時候都會創(chuàng)建一個“棧幀(Stack Frame)”芦圾。而每個戰(zhàn)陣?yán)飳?yīng)的都維護(hù)著一個局部變量表和操作數(shù)棧蛾派。我們常說的基本類型和對象引用存在棧里,其實(shí)就是存在局部變量表中个少。而操作數(shù)棧是線程實(shí)際的操作臺洪乍。
中間的這個非堆(Non-Heap)可以粗略地理解為非堆里包含了永生代,而永生代里又包括了方法區(qū)夜焦。
和String最相關(guān)的是非堆(Non-Heap)的“運(yùn)行時常量池(Run-time Constant Pool)”壳澳。它是每個類私有的。每個class文件里的“常量池”在類被加載器加載后茫经,就會映射存放在這個地方巷波。另一個相關(guān)的是“字符串常量池(String Pool)”萎津,但和運(yùn)行時常量池不是一個概念。字符串常量是全局共享的抹镊,處于Interned String的位置锉屈,可以理解為在永生代里,方法區(qū)外垮耳。String.intern()方法圆凰,字符串駐留之后个唧,引用就放在這個String Pool。
分析完JVM虛擬機(jī)內(nèi)存劃分后可柿,我們來看class文件的信息劃分
String有兩種賦值方式尽楔,第一種是通過“字面量”賦值擂橘。比如下面代碼
String str = "Hello"; ①
第二種是通過new關(guān)鍵字創(chuàng)建新對象趴俘。比如下面這樣
String str = new String("Hello"); ②
在編譯class文件后呀打,除了版本、字段豌研、方法妹田、接口等描述信息外唬党,還有一個也叫“常量池(Constant Pool Table)”的東西鹃共。這個常量池與內(nèi)存中的常量池不一樣。class文件里的常量池主要存兩個東西:“字面量(Literal)”和“符號引用量(Symbolic References)”驶拱。字面量就包括類中定義的一些常量霜浴,因為String是不可改變的,由final關(guān)鍵字修飾過蓝纲,所以代碼①中的”Hello“字符串阴孟,就作為字面量(常量)寫在class的常量池里。
在程序運(yùn)行Class文件時税迷,class文件的信息就會被解析到內(nèi)存的方法區(qū)里永丝。class文件里常量池中大部分?jǐn)?shù)據(jù)會被加載到”運(yùn)行時常量池(Run-time Constant Pool)“。但是箭养,這個“進(jìn)入”過程慕嚷,并不會直接把所有類中定義的常量全部都加載進(jìn)來,而是會做個比較毕泌,如果需要加到字符串常量池中的字符串已經(jīng)存在喝检,那么就不需要再把字符串字面量加載進(jìn)來了。
在類加載階段撼泛,JVM會在堆中創(chuàng)建對應(yīng)這些class文件常量池中的字符串對象實(shí)例挠说,并在字符串常量池中駐留其引用,具體會在resolve階段進(jìn)行愿题。但在resovle階段并不會立即執(zhí)行這些動作损俭,JVM規(guī)范里明確指定resolve階段可以是lazy的蛙奖。
代碼①中”Hello“的一個引用會被存到同樣在非堆(Non-Heap)的字符串常量池(String Pool)里,而”Hello“本體還是和所有對象一樣撩炊,創(chuàng)建在Heap堆區(qū)外永,具體是在新生代的Eden區(qū)。因為一直有一個引用駐留在字符串常量池拧咳,所以不會被GC清理掉伯顶。這個Hello對象會生存到整個線程結(jié)束。
等到主線程開始創(chuàng)建str變量的時候骆膝,虛擬機(jī)就會到字符串常量池找有沒有equal("Hello")的String祭衩。如果有,那么就會在棧區(qū)當(dāng)前棧幀的局部變量表里創(chuàng)建str變量阅签,然后把字符串常量池里對Hello對象的引用復(fù)制給str變量掐暮。找不到的話,才會在Heap堆重新創(chuàng)建一個對象政钟,然后把引用駐留到字符串常量區(qū)路克,然后把引用復(fù)制給棧幀的局部變量表中的str。
針對第一個問題养交,看一下代碼
String str1 = "Hello";
String str2 = "Hello";
String str3 = "Hello";
三者的關(guān)系如下圖所示
但是使用new關(guān)鍵字就不一樣了
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
這時候精算,str1和str2還是和之前一樣。但是str3會因為new關(guān)鍵字在Heap堆申請一塊全新的內(nèi)存碎连,來創(chuàng)建新對象灰羽。雖然字面還是"Hello",但是完全不同的對象鱼辙,有不同的內(nèi)存地址廉嚼。注意,str3的Hello堆地址并不會加到String Table里去倒戏。
總結(jié)
①類似String a = "a"+"b"+"c" 基本等同于“abc”怠噪,不會產(chǎn)生中間垃圾對象,也就是說"a"杜跷,"b"傍念,"c"最終會消失掉 。
②字符串與引用存在字符串池中葱椭,而對象存在堆內(nèi)存中
③字符串常量池的作用是保存該字符串的引用與字符串捂寿,以防堆內(nèi)存中的對象被GC回收
④方法里的private int i=1這種是局部變量。會在調(diào)用方法的時候進(jìn)入棧區(qū)當(dāng)前棧幀的局部變量表孵运。
⑤String s = new String("xyz"); 在運(yùn)行時涉及幾個String實(shí)例秦陋?
答案:兩個,堆中有個xyz的一號本體治笨,然后字符串常量池里存的是這一號本體的地址驳概,比如0xfff赤嚼。然后new String("xyz")創(chuàng)建的是堆中的新對象,xyz的二號本體顺又,二號本體在字符串常量池中無引用更卒。
問題3 String a = ””、String b = null稚照、String c = new String()三者的區(qū)別
答:””與new String() 表示已經(jīng)new出一個對象蹂空,但是內(nèi)部為空,且已經(jīng)創(chuàng)建了對象的引用果录,是需要分配內(nèi)存空間的上枕。null 表示還沒有new出一個對象,即該對象的引用還沒創(chuàng)建弱恒,也沒有分配內(nèi)存地址辨萍。
我們來看看這道代碼題來更好的理解上面的解釋(jdk1.8.0_172)
public class Client {
private static void isString(String str){
if (str == null){
System.out.println("null");
}
if (str.isEmpty()){
System.out.println("isEmpty");
}
if (str.equals("")){
System.out.println("\"\"");
}
System.out.println("-----------");
}
public static void main(String[] args) {
String a = new String();
String b = "";//沒有空格
String c = null;
String d = " ";//有一個空格
isString(a);
isString(b);
isString(c);
isString(d);
System.out.println(d.length());
System.out.println(c+"adcd");
}
}
---output---
isEmpty
""
-----------
isEmpty
""
-----------
Exception in thread "main" java.lang.NullPointerException
null
at com.homework.demo_15.Client.isString(Client.java:11)
at com.homework.demo_15.Client.main(Client.java:24)
-----------
1
nulladcd
分析:
String a = new String():a分配了內(nèi)存空間但內(nèi)部值為空,注意是有值但是值為空返弹,稱為絕對空锈玉;
String b = "";//沒有空格:b分配了內(nèi)存空間,是有值且內(nèi)部值為空字符串义起,稱為相對空拉背;
String c = null:c沒有分配內(nèi)存空間且無值無內(nèi)容,也就是沒有被實(shí)例化并扇。此對象還不存在去团。
String d = " ";//有一個空格:d分配內(nèi)存空間抡诞,是有值且內(nèi)部值是一個空格
根據(jù)輸出結(jié)果穷蛹,我們可以有以下總結(jié)
①判斷一個String對象是否為空值,比如a與b判斷是否為空昼汗,可以使用 str.equals("") 與 str.isEmpty()(內(nèi)部實(shí)際調(diào)用length == 0 )肴熏。
②判斷一個String對象是否為null(空對象),可以使用 str == null 判斷顷窒,但是無法使用 str.isEmpty( )或 str.equals("") 判斷蛙吏,因為并未被實(shí)例化
③對于包含空格的字符串判斷,str.equals("") 與 str.isEmpty()判斷結(jié)果均為false鞋吉,str.length() 的值為空格個數(shù)
④String c = null 與任何對象進(jìn)行“+”操作鸦做,最后都會變成“null+對象.toString()”
問題4 如何理解String的intern方法?
參考問題:Java 中new String("字面量") 中 "字面量" 是何時進(jìn)入字符串常量池的?
答:編譯期生成的各種字面量和符號引用是運(yùn)行時常量池中比較重要的一部分來源谓着,但并不是全部泼诱。那么還有一種情況,可以在運(yùn)行期像運(yùn)行時常量池中增加常量赊锚,那就是String 的 intern 方法治筒。
intern方法作用:如果常量池中已經(jīng)有了這個字符串屉栓,那么直接返回常量池中它的引用,如果沒有耸袜,那就將它的引用與字符串保存一份到字符串常量池友多,然后直接返回這個引用。
String str3 = new String("Hello");
如上述代碼堤框,在堆內(nèi)存中創(chuàng)建“Hello”的對象域滥,并且“Hello”字符串也在堆內(nèi)存中。之所以運(yùn)行該代碼會產(chǎn)生兩個對象蜈抓,指的是在堆內(nèi)存中有一個“Hello”的一號本體骗绕,字符串常量池里存的是一號本體的地址。new String("Hello")創(chuàng)建在堆中的新對象资昧,這是“Hello”的二號本體酬土,且這個二號本體是不會加到StringTable里去的,要是想加到里去格带,需要使用String的intern方法撤缴,這也就是為什么不推薦使用new創(chuàng)建String實(shí)例。
解釋:什么叫StringTable
StringTable其實(shí)就是個簡單的哈希表,是HotSpot VM里用來實(shí)現(xiàn)字符串駐留功能的全局?jǐn)?shù)據(jù)結(jié)構(gòu)叽唱。
如果用Java語法來講屈呕,這個StringTable其實(shí)就是個HashSet<String> ---
它并不保留駐留String對象本身,而是存儲這些被駐留的String對象的引用棺亭。
在遇到String類型常量時虎眨,resolve的過程如果發(fā)現(xiàn)StringTable已經(jīng)有了內(nèi)容匹配的java.lang.String的引用,則直接返回這個引用镶摘,反之嗽桩,如果StringTable里尚未有內(nèi)容匹配的String實(shí)例的引用,則會在Java堆里創(chuàng)建一個對應(yīng)內(nèi)容的String對象凄敢,然后在StringTable記錄下這個引用碌冶,并返回這個引用出去。
根據(jù)intern的作用涝缝,我們來看看下面的代碼(jdk1.8.0_172)
String a = "hello"; //hello對象1
String b = new String("hello").intern(); // hello對象2扑庞,將堆中新建的對象"hello" 存入字符串常量池
System.out.println(a == b); // 輸出 true
在類加載階段,第一句JVM會在堆中創(chuàng)建hello對象1的實(shí)例拒逮,并且引用會在字符串常量池中駐留罐氨。接下來第二句new 會在堆中新開辟一個內(nèi)存空間創(chuàng)建hello對象2,然后再調(diào)用intern()方法滩援,JVM會查找常量池中是否有相同Unicode的字符串常量栅隐,結(jié)果是有,那么就會返回hello對象1的實(shí)例在字符串常量池駐留的引用,因此輸出為true约啊。
再看這個代碼(jdk1.8.0_172)
String s1=new String("he")+new String("llo"); //第一句
String s2=new String("h")+new String("ello");
String s3=s1.intern(); //第三句
String s4=s2.intern(); //第四句
System.out.println(s1==s3); //輸出true
System.out.println(s1==s4); //輸出true
分析:運(yùn)行到第一句時邑遏,會創(chuàng)建“he”與“l(fā)lo”的對象,并且會保存引用到字符串常量池中恰矩,這就是這兩個對象的一號本體记盒。然后new String()創(chuàng)建二號本體,然后在“+”起來外傅,內(nèi)部是通過StringBuilder對象的append方法組合起來纪吮,再通過調(diào)用StringBuilder對象的toString方法得到一個String對象(內(nèi)容是hello,注意這個toString方法會new一個String對象)萎胰,并將它賦值給s1碾盟。注意:并不會把hello的引用放入字符串常量池中。
再看第三句技竟,調(diào)用intern方法后發(fā)現(xiàn)字符串常量池中并沒有對應(yīng)的字符串“hello”冰肴,它會把第一句中的“hello”對象引用保存到字符串常量池,然后返回這個引用榔组,這個引用賦值給s3熙尉,此時s1與s3指向同一個對象
第四句中,調(diào)用intern方法后發(fā)現(xiàn)字符串常量池中有對應(yīng)的字符串“hello”搓扯,直接返回該引用給s4检痰,因此s1與s4指向同一個對象。
問題5 如何理解《深入理解java虛擬機(jī)》第二版中對String.intern()方法的講解中所舉的例子锨推?
String str1 = new StringBuilder("計算機(jī)").append("軟件").toString();
System.out.println(str1.intern() == str1); //輸出 true
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2); // 輸出 false
答:Java標(biāo)準(zhǔn)庫在JVM啟動過程中加載的部分铅歼,可能里面就有類引用"java"字符串字面量,這個字面量被初次引用的時候就會被intern换可,加入到字符串常量池中去椎椰。具體是在初始化 sun.misc.Version 類時被放進(jìn)字符串常量池StringTable里的
答案摘錄自https://www.zhihu.com/question/51102308/answer/124441115