乾貨,聊聊Java中String類!!!
theme: vuepress
「這是我參與11月更文挑戰的第5天,活動詳情檢視:2021最後一次更文挑戰」。
java.lang.String 類可能是大家日常用的最多的類,但是對於它是怎麼實現的,你真的明白嗎? 認真閱讀這篇文章,包你一看就明白了。
String 類定義
java
public final class String implements
java.io.Serializable, Comparable<String>, CharSequence {}
從原始碼可以看出,String 是一個用 final 宣告的常量類,不能被任何類所繼承,而且一旦一個String物件被建立,包含在這個物件中的字元序列是不可改變的,包括該類後續的所有方法都是不能修改該物件的,直至該物件被銷燬,這是我們需要特別注意的(該類的一些方法看似改變了字串,其實內部都是建立一個新的字串,下面講解方法時會介紹)。接著實現了 Serializable介面,這是一個序列化標誌介面,還實現了 Comparable 介面,用於比較兩個字串的大小(按順序比較單個字元的ASCII碼),後面會有具體方法實現;最後實現了 CharSequence 介面,表示是一個有序字元的集合,相應的方法後面也會介紹。
欄位屬性
```java /*用來儲存字串 / private final char value[];
/* 快取字串的雜湊碼 / private int hash; // Default to 0
/* 實現序列化的標識 / private static final long serialVersionUID = -6849794470754667710L; ```
一個 String 字串實際上是一個 char 陣列。
構造方法
String 類的構造方法很多。可以通過初始化一個字串,或者字元陣列,或者位元組陣列等等來建立一個 String 物件。
java
String str1 = "abc";//注意這種字面量宣告的區別,文末會詳細介紹
String str2 = new String("abc");
String str3 = new String(new char[]{'a','b','c'});
equals(Object anObject) 方法
java
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
String 類重寫了 equals 方法,比較的是組成字串的每一個字元是否相同,如果都相同則返回true,否則返回false。
hashCode() 方法
java
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
String 類的 hashCode 演算法很簡單,主要就是中間的 for 迴圈,計算公式如下:
s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
s 陣列即原始碼中的 val 陣列,也就是構成字串的字元陣列。這裡有個數字 31 ,為什麼選擇31作為乘積因子,而且沒有用一個常量來宣告?主要原因有兩個:
1、31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。
2、31可以被 JVM 優化,31 * i = (i « 5) - i。因為移位運算比乘法執行更快更省效能。
charAt(int index) 方法
java
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
我們知道一個字串是由一個字元陣列組成,這個方法是通過傳入的索引(陣列下標),返回指定索引的單個字元。
intern() 方法
這是一個本地方法:
public native String intern();
當呼叫intern方法時,如果池中已經包含一個與該String確定的字串相同equals(Object)
的字串,則返回該字串。否則,將此String物件新增到池中,並返回此物件的引用。
這句話什麼意思呢?就是說呼叫一個String物件的intern()
方法,如果常量池中有該物件了,直接返回該字串的引用(存在堆中就返回堆中,存在池中就返回池中);如果沒有,則將該物件新增到池中,並返回池中的引用。
```java String str1 = "hello";//字面量 只會在常量池中建立物件 String str2 = str1.intern(); System.out.println(str1==str2);//true
String str3 = new String("world");//new 關鍵字只會在堆中建立物件 String str4 = str3.intern(); System.out.println(str3 == str4);//false
String str5 = str1 + str2;//變數拼接的字串,會在常量池中和堆中都建立物件 String str6 = str5.intern();//這裡由於池中已經有物件了,直接返回的是物件本身,也就是堆中的物件 System.out.println(str5 == str6);//true
String str7 = "hello1" + "world1";//常量拼接的字串,只會在常量池中建立物件 String str8 = str7.intern(); System.out.println(str7 == str8);//true ```
關於String類裡面的眾多方法,這裡不一一介紹了,下面我們來深入瞭解一下,String 類不可變型。
面試精選
分析一道經典的面試題:
java
public static void main(String[] args) {
String A = "abc";
String B = "abc";
String C = new String("abc");
System.out.println(A==B);
System.out.println(A.equals(B));
System.out.println(A==C);
System.out.println(A.equals(C));
}
答案是:true、true、false、true
對於上面的題目,我們可以先來看一張圖,如下:
首先 String A= “abc”,會先到常量池中檢查是否有“abc”的存在,發現是沒有的,於是在常量池中建立“abc”物件,並將常量池中的引用賦值給A;第二個字面量 String B= “abc”,在常量池中檢測到該物件了,直接將引用賦值給B;第三個是通過new關鍵字建立的物件,常量池中有了該物件了,不用在常量池中建立,然後在堆中建立該物件後,將堆中物件的引用賦值給C,再將該物件指向常量池。
需要說明一點的是,在object中,equals()是用來比較記憶體地址的,但是String重寫了equals()方法,用來比較內容的,即使是不同地址,只要內容一致,也會返回true,這也就是為什麼A.equals(C)返回true的原因了。
注意:看上圖紅色的箭頭,通過 new 關鍵字建立的字串物件,如果常量池中存在了,會將堆中建立的物件指向常量池的引用。
再來看一道題目,使用包含變量表達式建立物件:
```java String str1 = "hello"; String str2 = "helloworld"; String str3 = str1+"world";//編譯器不能確定為常量(會在堆區建立一個String物件) String str4 = "hello"+"world";//編譯器確定為常量,直接到常量池中引用
System.out.println(str2==str3);//fasle System.out.println(str2==str4);//true System.out.println(str3==str4);//fasle ```
str3 由於含有變數str1,編譯器不能確定是常量,會在堆區中建立一個String物件。而str4是兩個常量相加,直接引用常量池中的物件即可。
String 不可變性
String類是Java中的一個不可變類(immutable class)。
簡單來說,不可變類就是例項在被建立之後不可修改。
String不可變這個話題應該是老生長談了,String自打孃胎一出生就跟他們的兄弟姐妹不一樣,好好的娃被戴了一個final的帽子,
以至於byte,int,short,long等基本型別的小夥們都不帶它玩。
如果你仔細閱讀原始碼註釋,你會發現這樣一句話:
大致意思就是String是個常量,從一出生就註定不可變。
首先需要補充一個容易混淆的知識點:當使用final修飾基本型別變數時,不能對基本型別變數重新賦值,因此基本型別變數不能被改變。但對於引用型別變數而言,它儲存的僅僅是一個引用,final只保證這個引用變數所引用的地址不會改變,即一直引用同一個物件,但這個物件完全可以發生改變。例如某個指向陣列的final引用,它必須從此至終指向初始化時指向的陣列,但是這個陣列的內容完全可以改變。
String 類是用 final 關鍵字修飾的,所以我們認為其是不可變物件。但是真的不可變嗎?
每個字串都是由許多單個字元組成的,我們知道其原始碼是由char[] value
字元陣列構成。
```java /* The value is used for character storage. / private final char value[];
/* Cache the hash code for the string / private int hash; // Default to 0 ```
value 被 final 修飾,只能保證引用不被改變,但是 value 所指向的堆中的陣列,才是真實的資料,只要能夠操作堆中的陣列,依舊能改變資料。而且 value 是基本型別構成,那麼一定是可變的,即使被宣告為 private,我們也可以通過反射來改變。
java
public static void main(String[] args) throws Exception {
String str = "Hello World";
System.out.println("修改前的str:" + str);
System.out.println("修改前的str的記憶體地址" + System.identityHashCode(str));
// 獲取String類中的value欄位
Field valueField = String.class.getDeclaredField("value");
// 改變value屬性的訪問許可權
valueField.setAccessible(true);
// 獲取str物件上value屬性的值
char[] value = (char[]) valueField.get(str);
// 改變value所引用的陣列中的字元
value[3] = '?';
System.out.println("修改後的str:" + str);
System.out.println("修改前的str的記憶體地址" + System.identityHashCode(str));
}
java
修改前的str:Hello World
修改前的str的記憶體地址1746572565
修改後的str:Hel?o World
修改前的str的記憶體地址1746572565
通過前後兩次列印的結果,我們可以看到 str 值被改變了,但是str的記憶體地址還是沒有改變。但是在程式碼裡,幾乎不會使用反射的機制去操作 String 字串,所以,我們會認為 String 型別是不可變的。
不可變的好處
首先,我們應該站在設計者的角度思考問題,而不是覺得這不好,那不合理:
- 可以實現多個變數引用堆記憶體中的同一個字串例項,避免建立的開銷。
- 我們的程式中大量使用了String字串,有可能是出於安全性考慮。
- 當我們在傳參的時候,使用不可變類不需要去考慮誰可能會修改其內部的值,如果使用可變類的話,可能需要每次記得重新拷貝出裡面的值,效能會有一定的損失。
小結
有興趣的小夥伴也可以去閱讀下String的原始碼,浩浩蕩蕩的3000+。
String 被new時是要建立物件的,+ 號拼接同理,程式中儘量不要使用 + 拼接,推薦使用StringBuffer或者StringBuilder。
感謝的閱讀,希望看完三連一波呀,謝謝啦~~~
- 一文弄懂Java中執行緒池原理
- 聊聊工作中,如何提升自己的程式設計能力?
- 聊聊MQ,如何避免訊息丟失?如何避免重複消費?
- Java實現串列埠通訊
- SpringBoot為什麼可以使用Jar包啟動?
- 2021,平凡的一年!
- 聊聊 Java 中引數傳遞的原理!
- 聊聊Pulsar,一款非常優秀的訊息中介軟體!!!
- 乾貨,聊聊Java中String類!!!
- 萬字!Java相關的學習資料整理(乾貨滿滿)
- 使用 ArrayList 應當避免的坑
- Java中避免空指標的幾個方法!
- 聊聊Jhipster,強烈推薦Java開發看看,節省很多時間!!!
- 給程式設計師新手的一些建議
- 計算機基礎知識:磁碟分割槽
- Java中的序列化
- Java中的控制流程語句詳解
- 程式設計師必備:Git入門,超詳細
- netty系列:使用 SSL/TLS 加密 Netty 程式
- 聊聊MySQL儲存引擎中索引如何落地?