真的懂Java的String嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「學(xué)習(xí)Java的小姐姐」,作者學(xué)習(xí)Java的小姐姐0618。轉(zhuǎn)載本文請(qǐng)聯(lián)系學(xué)習(xí)Java的小姐姐公眾號(hào)。
1.String的特性
1.1不變性
我們常常聽人說,HashMap 的 key 建議使用不可變類,比如說 String 這種不可變類。這里說不可變指的是類值一旦被初始化,就不能再被改變了,如果被修改,將會(huì)是新的類,我們寫個(gè)demo 來演示一下。
- public class test {
- public static void main(String[] args){
- String str="hello";
- str=str+"world";
- }
- }
從代碼上來看,s 的值好像被修改了,但從 debug 的日志來看,其實(shí)是 s 的內(nèi)存地址已經(jīng)被修了,也就說 s =“world” 這個(gè)看似簡(jiǎn)單的賦值,其實(shí)已經(jīng)把 s 的引用指向了新的 String,debug 截圖顯示內(nèi)存地址已經(jīng)被修改,兩張截圖如下,我們可以看到標(biāo)紅的地址值已經(jīng)修改了。
用示意圖來表示堆內(nèi)存,即見下圖。
我們可以看下str的地址已經(jīng)改了,說了生成了兩個(gè)字符串,String類的官方注釋為Strings are constant; their values cannot be changed after they are created. 簡(jiǎn)單翻譯下為字符串是常量;它們的值在創(chuàng)建后不能更改。
下面為String的相關(guān)代碼,如下代碼,我們可以看到:
1. String 被 final 修飾,說明 String 類絕不可能被繼承了,也就是說任何對(duì) String 的操作方法,都不會(huì)被繼承覆寫,即可保證雙親委派機(jī)制,保證基類的安全性。
2. String 中保存數(shù)據(jù)的是一個(gè) char 的數(shù)組 value。我們發(fā)現(xiàn) value 也是被 final 修飾的,也就是說 value 一旦被賦值,內(nèi)存地址是絕對(duì)無法修改的,而且 value 的權(quán)限是 private 的,外部絕對(duì)訪問不到,String沒有開放出可以對(duì) value 進(jìn)行賦值的方法,所以說 value 一旦產(chǎn)生,內(nèi)存地址就根本無法被修改。
- //char類型的final數(shù)組
- private final char value[];
- //hash值
- private int hash;
- private static final long serialVersionUID = -6849794470754667710L;
1.2相等判斷
相等判斷邏輯寫的很清楚明了,如果有人問如何判斷兩者是否相等時(shí),我們可以從兩者的底層結(jié)構(gòu)出發(fā),這樣可以迅速想到一種貼合實(shí)際的思路和方法,就像 String 底層的數(shù)據(jù)結(jié)構(gòu)是 char 的數(shù)組一樣,判斷相等時(shí),就挨個(gè)比較 char 數(shù)組中的字符是否相等即可。(這里先挖個(gè)坑,攜程問過類似題目)
- public boolean equals(Object anObject) {
- //如果地址相等,則直接返回true
- if (this == anObject) {
- return true;
- }
- //如果為String字符串,則進(jìn)行下面的邏輯判斷
- if (anObject instanceof String) {
- //將對(duì)象轉(zhuǎn)化為String
- String anotherString = (String)anObject;
- //獲取當(dāng)前值的長(zhǎng)度
- int n = value.length;
- //先比較長(zhǎng)度是否相等,如果長(zhǎng)度不相等,這兩個(gè)肯定不相等
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value; int i = 0; //while循環(huán)挨個(gè)比較每個(gè)char
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
相等邏輯的流程圖如下,我們可以看到整個(gè)流程還是很清楚的。
1.3替換操作
替換在平時(shí)工作中也經(jīng)常使用,主要有 replace 替換所有字符、replaceAll 批量替換字符串、replaceFirst這三種場(chǎng)景。
下面寫了一個(gè) demo 演示一下三種場(chǎng)景:
- public static void main(String[] args) {
- String str = "hello word !!";
- System.out.println("替換之前 :" + str);
- str = str.replace('l', 'd');
- System.out.println("替換所有字符 :" + str);
- str = str.replaceAll("d", "l");
- System.out.println("替換全部 :" + str);
- str = str.replaceFirst("l", "");
- System.out.println("替換第一個(gè) l :" + str);
- }
輸出的結(jié)果是:
這邊要注意一點(diǎn)是replace和replaceAll的區(qū)別,不是替換和替換所有的區(qū)別哦。
而是replaceAll支持正則表達(dá)式,因此會(huì)對(duì)參數(shù)進(jìn)行解析(兩個(gè)參數(shù)均是),如replaceAll("\\d", "*"),而replace則不會(huì),replace("\\d","*")就是替換"\\d"的字符串,而不會(huì)解析為正則。
1.4 intern方法
String.intern() 是一個(gè) Native 方法,即是c和c++與底層交互的代碼,它的作用(在JDK1.6和1.7操作不同)是:
如果運(yùn)行時(shí)常量池中已經(jīng)包含一個(gè)等于此 String 對(duì)象內(nèi)容的字符串,則直接返回常量池中該字符串的引用;
如果沒有, 那么在jdk1.6中,將此String對(duì)象添加到常量池中,然后返回這個(gè)String對(duì)象的引用(此時(shí)引用的串在常量池)。
在jdk1.7中,放入一個(gè)引用,指向堆中的String對(duì)象的地址,返回這個(gè)引用地址(此時(shí)引用的串在堆)。
- public native String intern();
如果看上面看不懂,我們來看下一下具體的例子,并來分析下。
- public static void main(String[] args) {
- String s1 = new String("學(xué)習(xí)Java的小姐姐");
- s1.intern();
- String s2 = "學(xué)習(xí)Java的小姐姐";
- System.out.println(s1 == s2);
- String s3 = new String("學(xué)習(xí)Java的小姐姐") + new String("test");
- s3.intern();
- String s4 = "學(xué)習(xí)Java的小姐姐test";
- System.out.println(s3 == s4);
- }
我們來看下結(jié)果,實(shí)際的打印信息如下。
為什么顯示這樣的結(jié)果,我們來看下。所以在 jdk7 的版本中,字符串常量池已經(jīng)從方法區(qū)移到正常的堆 區(qū)域了。
- 第一個(gè)false: 第一句代碼String s1 = new String("學(xué)習(xí)Java的小姐姐");生成了2個(gè)對(duì)象。常量池中的“學(xué)習(xí)Java的小姐姐” 和堆中的字符串對(duì)象。s1.intern(); 這一句是 s1 對(duì)象去常量池中尋找后,發(fā)現(xiàn) “學(xué)習(xí)Java的小姐姐” 已經(jīng)在常量池里了。接下來String s2 = "學(xué)習(xí)Java的小姐姐"; 這句代碼是生成一個(gè) s2的引用指向常量池中的“學(xué)習(xí)Java的小姐姐”對(duì)象。結(jié)果就是 s 和 s2 的引用地址明顯不同,所以打印結(jié)果是false。
- 第二個(gè)true:先看 s3和s4字符串。String s3 = new String("學(xué)習(xí)Java的小姐姐") + new String("test");,這句代碼中現(xiàn)在生成了3個(gè)對(duì)象,是字符串常量池中的“學(xué)習(xí)Java的小姐姐” ,"test"和堆 中的 s3引用指向的對(duì)象。此時(shí)s3引用對(duì)象內(nèi)容是”學(xué)習(xí)Java的小姐姐test”,但此時(shí)常量池中是沒有 “學(xué)習(xí)Java的小姐姐test”對(duì)象的,接下來s3.intern();這一句代碼,是將 s3中的“學(xué)習(xí)Java的小姐姐test”字符串放入 String 常量池中,因?yàn)榇藭r(shí)常量池中不存在“學(xué)習(xí)Java的小姐姐test”字符串,常量池不需要再存儲(chǔ)一份對(duì)象了,可以直接存儲(chǔ)堆中的引用。這份引用指向 s3 引用的對(duì)象。也就是說引用地址是相同的。最后String s4 = "學(xué)習(xí)Java的小姐姐test"; 這句代碼中”學(xué)習(xí)Java的小姐姐test”是顯示聲明的,因此會(huì)直接去常量池中創(chuàng)建,創(chuàng)建的時(shí)候發(fā)現(xiàn)已經(jīng)有這個(gè)對(duì)象了,此時(shí)也就是指向 s3 引用對(duì)象的一個(gè)引用。所以 s4 引用就指向和 s3 一樣了。因此最后的比較 s3 == s4 是 true。
我們?cè)倏聪?,如果把上面的兩行代碼調(diào)整下位置,打印結(jié)果是不是不同。
- public static void main(String[] args) {
- String s1 = new String("學(xué)習(xí)Java的小姐姐");
- String s2 = "學(xué)習(xí)Java的小姐姐";
- s1.intern();
- System.out.println(s1 == s2);
- String s3 = new String("學(xué)習(xí)Java的小姐姐") + new String("test");
- String s4 = "學(xué)習(xí)Java的小姐姐test";
- s3.intern();
- System.out.println(s3 == s4);
- }
- 第一個(gè)false: s1 和 s2 代碼中,s1.intern();,這一句往后放也不會(huì)有什么影響了,因?yàn)閷?duì)象池中在執(zhí)行第一句代碼String s = new String("學(xué)習(xí)Java的小姐姐");的時(shí)候已經(jīng)生成“學(xué)習(xí)Java的小姐姐”對(duì)象了。下邊的s2聲明都是直接從常量池中取地址引用的。s 和 s2 的引用地址是不會(huì)相等的。
- 第二個(gè)false:與上面唯一的區(qū)別在于 s3.intern(); 的順序是放在String s4 = "學(xué)習(xí)Java的小姐姐test";后了。這樣,首先執(zhí)行String s4 = "學(xué)習(xí)Java的小姐姐test";聲明 s4 的時(shí)候常量池中是不存在“學(xué)習(xí)Java的小姐姐test”對(duì)象的,執(zhí)行完畢后,“學(xué)習(xí)Java的小姐姐test“對(duì)象是 s4 聲明產(chǎn)生的新對(duì)象。然后再執(zhí)行s3.intern();時(shí),常量池中“學(xué)習(xí)Java的小姐姐test”對(duì)象已經(jīng)存在了,因此 s3 和 s4 的引用是不同的。
2. String、StringBuilder和StringBuffer
2.1 繼承結(jié)構(gòu)
2.2 主要區(qū)別
1)String是不可變字符序列,StringBuilder和StringBuffer是可變字符序列。
2)執(zhí)行速度StringBuilder > StringBuffer > String。
3)StringBuilder是非線程安全的,StringBuffer是線程安全的。