Java中String對象的深入理解
一、 String認識你,你認識它么?
假如面試的時候問你,什么是String(或者談?wù)勀銓tring的理解)?你會如何回答?“String是基礎(chǔ)對象類型之一,是Java語言中重要的數(shù)據(jù)類型之一”。恐怕這是大多數(shù)人的回答,能力強些的可能會說,String底層是用char[ ]數(shù)組來實現(xiàn)的;如果面試官讓你再繼續(xù)呢?估計很多人會一臉尷尬,腦海里極力搜索關(guān)于String的相關(guān)知識,最后也只能恨自己平時對String關(guān)注的太少。下面就讓我們一步一步地去認識String。
首先,來看一個面試經(jīng)常遇到,錯誤率又很高的問題:
1 String str1 = “java”; 2 String str2 = new String(“java”); 3 String str3= str2.intern(); 4 System.out.println(str1 == str2); 5 System.out.println(str2 == str3); 6 System.out.println(str1 == str3);
答案先不揭曉,各位先想一下,咱們繼續(xù)往下看:
二、String對象的實現(xiàn)
我們把String對象的實現(xiàn)分為三個階段來分析:java7之前的版本、java7/8版本、java8之后的版本。
1、 java7之前的版本中,String對象中主要由四個成員變量:char[]、偏移量offset、字符數(shù)量count、哈希值hash。String對象通過offset和count來定位char[],這么做可以高效、快速地共享數(shù)組對象,節(jié)省內(nèi)存空間,但這種方式很有可能會導(dǎo)致內(nèi)存泄漏。
2、 java7/8版本中,String 去除了offset 和 count 兩個變量。這樣的好處是String 對象占用的內(nèi)存稍微少了些,同時,String.substring()方法也不再共享char[],從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題。
3、 java8之后的版本中,char[] 屬性改為了 byte[] 屬性,增加了一個新的屬性coder,它是一個編碼格式的標識。為什么這么做呢?我們知道一個char字符占16位,2 個字節(jié)。這種情況下,存儲單字節(jié)編碼內(nèi)的字符(占一個字節(jié)的字符)就顯得非常浪費。JDK1.9 的String類為了節(jié)約內(nèi)存空間,于是使用了占8位,1個字節(jié)的 byte 數(shù)組來存放字符串。而新屬性coder的作用是,在計算字符串長度或者使用 indexOf()函數(shù)時,我們需要根據(jù)這個字段,判斷如何計算字符串長度。coder屬性默認有0和1兩個值,0代表Latin-1(單字節(jié)編碼),1代表UTF-16。如果 String判斷字符串只包含了Latin-1,則coder屬性值為0,反之則為1。
三、String是不可變對象
1、為什么String是不可變對象很多人背面試題的時候想必都對此很熟悉,那為什么String對象是不可變的呢?你有想過這其中的原因么?通過源碼我們知道,String類被final關(guān)鍵字修飾了,而且變量char[]也被final修飾了。Java語法告訴我們:被final修飾的類不可被繼承,被final修飾的變量不可被改變,一旦賦值了初始值,該final變量的值就不能被重新賦值,即不可更改,而char[]被 final+private修飾,說明String對象不可被更改。即String對象一旦創(chuàng)建成功,就不能再對它進行改變。
2、為什么String被設(shè)計成不可變對象首先,是為了保證String對象的安全性,避免被惡意篡改。比如將值為“abc”的引用賦值給str對象,即String str = “abc”,如果此時有人惡意將“abc”改為“abcd”或其他值就會造成意想不到的錯誤。
其次,確保屬性值hash不頻繁變動,保證其唯一性。
3、為實現(xiàn)字符串常量池提供方便舉一個反例來證明String對象的不可變性
針對String對象不可變性,有人可能會說:對于一個String str =“hello”,然后改為String str =“world”,這個時候str的值變成了“world”,str值確實改變了,為什么還說String對象不可變呢?
首先,我們來解釋一下對象和引用。對象在內(nèi)存中是一塊內(nèi)存地址,str則是一個指向該內(nèi)存地址的引用,所以在這個例子中,第一次賦值的時候,創(chuàng)建了一個“hello”對象,str引用指向“hello”地址;第二次賦值的時候,又重新創(chuàng)建了一個對象“world”,str引用指向了“world”,但“hello”對象依然存在于內(nèi)存中。也就是說str并不是對象,而只是一個對象引用。真正的對象依然還在內(nèi)存中,沒有被改變。所以在Java中要比較兩個對象是否相等,通常是用“==”,而要判斷兩個對象的值是否相等,則需要用equals方法來判斷。
四、String常量池
在java中,創(chuàng)建字符串通常有兩種方式:一種是通過字符串常量池的形式,比如String str = “abcd”;另一種是直接通過new的形式,如String string = new String(“abcd”);
針對第一種方式創(chuàng)建字符串時,JVM首先會檢查該對象是否存在于字符串常量池中,如果存在,就返回該引用,否則在常量池中創(chuàng)建新的字符串對象,然后將引用返回。這種方式可以減少同一個值的字符串對象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。
采用new形式創(chuàng)建字符串時,首先在編譯類文件時,"abcd"常量字符串將會放入到常量結(jié)構(gòu)中,在類加載時,“abcd"將會在常量池中創(chuàng)建;其次,在調(diào)用new時,JVM命令將會調(diào)用String的構(gòu)造函數(shù),同時引用常量池中的"abcd”字符串,在堆內(nèi)存中創(chuàng)建一個 String對象;最后,string將引用String對象。
五、String.intern()方法詳解
先來看一個示例:
String a =new String("abc").intern(); String b = new String("abc").intern(); System.out.print(a==b);
你覺得輸出的是false還是true?
答案是:true
在字符串常量中,默認會將對象放入常量池中;在字符串變量中,對象是會創(chuàng)建在堆內(nèi)存中,同時也會在常量池中創(chuàng)建一個字符串對象,復(fù)制到堆內(nèi)存對象中,并返回堆內(nèi)存對象引用。如果調(diào)用intern()方法,會去查看字符串常量池中是否有等于該對象的字符串,如果沒有,就在常量池中新增該對象,并返回該對象引用;如果有,就返回常量池中的字符串引用。堆內(nèi)存中原有的對象由于沒有引用指向它,將會通過垃圾回收器回收。
所以針對上面的例子中,在一開始創(chuàng)建a變量時,會在堆內(nèi)存中創(chuàng)建一個對象,同時會在加載類時,在常量池中創(chuàng)建一個字符串對象,在調(diào)用intern()方法之后,會去常量池中查找是否有等于該字符串的對象,有就返回引用。在創(chuàng)建b字符串變量時,也會在堆中創(chuàng)建一個對象,此時常量池中有該字符串對象,就不再創(chuàng)建。調(diào)用 intern 方法則會去常量池中判斷是否有等于該字符串的對象,發(fā)現(xiàn)有等于"abc"字符串的對象,就直接返回引用。而在堆內(nèi)存中的對象,由于沒有引用指向它,將會被垃圾回收。所以a和b引用的是同一個對象。
看完這些內(nèi)容后,文章開頭的問題,相比你也有了答案了。分別是:false、false、true。
六、String、StringBuffer和StringBuilder的區(qū)別
1.對象的可變與不可變String是不可變對象,原因上面的內(nèi)容已經(jīng)解釋過了,這里不再贅述。
StringBuilder與StringBuffer都繼承自AbstractStringBuilder類,在AbstractStringBuilder中也是使用字符數(shù)組保存數(shù)據(jù),這兩種對象都是可變的。如下:
char[ ] value;
2.是否是線程安全String中的對象是不可變的,也就可以理解為常量,所以是線程安全。
AbstractStringBuilder是StringBuilder與StringBuffer的公共父類,定義了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer對方法加了同步鎖或者對調(diào)用的方法加了同步鎖,所以是線程安全的??慈缦略创a:
1 public synchronized StringBuffer reverse() { 2 super.reverse(); 3 return this; 4 } 5 6 public int indexOf(String str) { 7 return indexOf(str, 0); //存在 public synchronized int indexOf(String str, int fromIndex) 方法 8 }
StringBuilder并沒有對方法進行加同步鎖,所以是非線程安全的。
3.StringBuilder與StringBuffer共同點StringBuilder與StringBuffer有公共的抽象父類AbstractStringBuilder。
抽象類與接口的一個區(qū)別是:抽象類中可以定義一些子類的公共方法,子類只需要增加新的功能,不需要重復(fù)寫已經(jīng)存在的方法;而接口中只是對方法的申明和常量的定義。
StringBuilder、StringBuffer的方法都會調(diào)用AbstractStringBuilder中的公共方法,如super.append(…)。只是StringBuffer會在方法上加synchronized關(guān)鍵字,進行同步。
如果程序不是多線程的,那么使用StringBuilder效率高于StringBuffer。
下面來幾道測試題,看看自己對String究竟掌握了多少
七、測試題
test1、如下代碼中創(chuàng)建了幾個對象
1 String str1 = "abc"; 2 String str2 = new String("abc");
對于1中的 String str1 = “abc”,首先會檢查字符串常量池中是否含有字符串a(chǎn)bc,如果有則直接指向,如果沒有則在字符串常量池中添加abc字符串并指向它.所以這種方法最多創(chuàng)建一個對象,有可能不創(chuàng)建對象。
對于2中的String str2 = new String(“abc”),首先會在堆內(nèi)存中申請一塊內(nèi)存存儲字符串a(chǎn)bc,str2指向其內(nèi)存塊對象。同時還會檢查字符串常量池中是否含有abc字符串,若沒有則添加abc到字符串常量池中。所以 new String()可能會創(chuàng)建兩個對象。
所以如果以上兩行代碼在同一個程序中,則1中創(chuàng)建了1個對象,2中創(chuàng)建了1個對象。如果將這兩行代碼的順序調(diào)換一下,則String str2 = new String(“abc”)創(chuàng)建了兩個對象,而 String str1 = "abc"沒有創(chuàng)建對象。
test2、看看下面的代碼創(chuàng)建了多少個對象:
1 String temp="apple"; 2 for(int i=0;i<1000;i++) { 3 temp=temp+i; 4 }
答案:1001個對象。
test3、下面的代碼創(chuàng)建了多少個對象:
1 String temp = new String("apple") 2 for(int i=0;i<1000;i++) { 3 temp = temp+i; 4 }
答案:1002個對象。
test4:
1 String ok = "ok"; 2 String ok1 = new String("ok"); 3 System.out.println(ok == ok1);//fasle
ok指向字符串常量池,ok1指向new出來的堆內(nèi)存塊,new的字符串在編譯期是無法確定的。所以輸出false。
test5:
1 String ok = "apple1"; 2 String ok1 = "apple"+1; 3 System.out.println(ok==ok1);//true
編譯期ok和ok1都是確定的,字符串都為apple1,所以ok和ok1都指向字符串常量池里的字符串a(chǎn)pple1。指向同一個對象,所以為true。
test6:
1 String ok = "apple1"; 2 int temp = 1; 3 String ok1 = "apple"+temp; 4 System.out.println(ok==ok1);//false
主要看ok和ok1能否在編譯期確定,ok是確定的,放進并指向常量池,而ok1含有變量導(dǎo)致不確定,所以不是同一個對象.輸出false。
test7:
1 String ok = "apple1"; 2 final int temp = 1; 3 String ok1 = "apple"+temp; 4 System.out.println(ok==ok1);//true
ok確定,加上final后使得ok1也在編譯期能確定,所以輸出true。
test8:
1 public static void main(String[] args) { 2 String ok = "apple1"; 3 final int temp = getTemp(); 4 String ok1 = "apple"+temp; 5 System.out.println(ok==ok1);//false 6 } 7 8 public static int getTemp(){ 9 return 1; 10 }
ok一樣是確定的。而ok1不能確定,需要運行代碼獲得temp,所以不是同一個對象,輸出false。
以上內(nèi)容如有不對的地方,還請各位指正!多謝!
以上就是Java中String對象的深入理解的詳細內(nèi)容,更多關(guān)于Java String對象的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java并發(fā)編程ThreadLocalRandom類詳解
這篇文章主要介紹了Java并發(fā)編程ThreadLocalRandom類詳解,通過提出問題為什么需要ThreadLocalRandom展開詳情,感興趣的朋友可以參考一下2022-06-06java swing實現(xiàn)電影購票系統(tǒng)
這篇文章主要為大家詳細介紹了java swing實現(xiàn)電影購票系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01Java中?springcloud.openfeign應(yīng)用案例解析
使用OpenFeign能讓編寫Web?Service客戶端更加簡單,使用時只需定義服務(wù)接口,然后在上面添加注解,OpenFeign也支持可拔插式的編碼和解碼器,這篇文章主要介紹了Java中?springcloud.openfeign應(yīng)用案例解析,需要的朋友可以參考下2024-06-06Java并發(fā)工具類之CountDownLatch詳解
這篇文章主要介紹了Java并發(fā)工具類之CountDownLatch詳解,CountDownLatch可以使一個獲多個線程等待其他線程各自執(zhí)行完畢后再執(zhí)行,CountDownLatch可以解決那些一個或者多個線程在執(zhí)行之前必須依賴于某些必要的前提業(yè)務(wù)先執(zhí)行的場景,需要的朋友可以參考下2023-12-12