JVM(Java虛擬機) 學習String類前,先瞭解一下JVM,也稱為Java虛擬機。 JVM記憶體分有幾大區域,其中,常見有堆、桟、方法區、常量池。 堆是運行時數據區,類通過new指令創建的對象會在堆記憶體里分配空間。堆記憶體的數據是由java垃圾回收器自動回收。堆的優勢是可以動態地分配記憶體大小。缺點 ...
JVM(Java虛擬機)
學習String類前,先瞭解一下JVM,也稱為Java虛擬機。
JVM記憶體分有幾大區域,其中,常見有堆、桟、方法區、常量池。
堆是運行時數據區,類通過new指令創建的對象會在堆記憶體里分配空間。堆記憶體的數據是由java垃圾回收器自動回收。堆的優勢是可以動態地分配記憶體大小。缺點是,由於要在運行時動態分配記憶體,存取速度較慢。
桟是存放一些基本類型的變數數據和對象的引用。優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共用。缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。
String
好的,大概瞭解了JVM後來學習String。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}
String是java中代表字元串的類。java中所有的字元串面量值都由此類實現,它被聲明為 final,因此它不可被繼承
private final char value[];
查閱底層代碼,String內部使用了數組來存儲數據,這個數組由final修飾,當數組初始化後不能再引用其它數組,也確保了String不可變
創建字元串對象
兩種方式
-
直接賦值,在字元串常量池創建了一個對象
String str = "che";
-
通過構造方法,創建字元串對象
String str = new String("che");
先來看一下程式
String chen1 = "chen";
String chen2 = "chen";
String chen3 = "chen!!";
String newChen1 = new String("chen");
String newChen2 = new String("chen");
System.out.println(newChen2 == chen1);
System.out.println(newChen1 == newChen2);
System.out.println(chen1 == chen2);
System.out.println(chen1.equals(newChen2));
/*
true
false
true
true
Process finished with exit code 0
*/
兩種創建分式的區別
- 從記憶體上分析。
- 直接賦值的方式。先查找字元串常量池中有沒有s1要創建的對象,沒有則在常量池中創建對象“chen”,然後到s2定義同樣的字元串對象時,還會去常量池中找著是否已經有該對象存在,有則把對象的引用實例共用給s2。以上s1、s2在字元串常量池中只創建了一個對象,因為代碼中還沒有出現new所以沒有在堆里創建對象
- 通過new創建字元串對象。首先會先去字元串常量池中查找有沒有“chen”的實例引用,有則把該引用共用給堆中的
new String()
,並把堆中的引用返回到棧中對應的數據,然後壓棧。以上str1、str2在字元串常量池有對應的對象時,只在堆中創建了兩個對象,並沒有在常量池中創建對象
字元串常量池不會存在兩個相同的字元串
Q&A
Q1:Hash table Entry
哈希表條目,是字元串常量池底層實現的一種,用於記錄字元串常量池中的數據,我們從常量池中獲取字元串,實際是從哈希表條目中獲取對應的條目值
Q2:位元組碼文件指令
此處參考的文獻: Java字元串字面量是何時進入到字元串常量池中的、Java 中new String("字面量") 中 "字面量" 是何時進入字元串常量池的?
以下是上列程式編譯後的部分位元組碼指令,通過執行javap -c FileClass
對class文件反編譯。或者javap -v FileClass
可以更清楚知道常量池的編號對應的數據
public class string_base.TestBase {
public string_base.TestBase();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String chen
2: astore_1
3: ldc #2 // String chen
5: astore_2
6: ldc #3 // String chen!!
8: astore_3
9: new #4 // class java/lang/String
12: dup
13: ldc #2 // String chen
15: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
18: astore 4
20: new #4 // class java/lang/String
23: dup
24: ldc #2 // String chen
26: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
29: astore 5
解析:
main方法:
0-8行,
ldc
將常量池中的常量值載入到操作數棧;astore_indexbyte
將棧頂引用類型值保存到局部變數indexbyte中。9-29行,
new
創建一個String對象;dup
複製棧頂一個字長的數據,將複製後的數據壓棧;ldc
將常量池中的常量值載入到操作數棧;invokespecial
用於調用特殊的方法,如實例初始化方法、私有方法和父類方法;astore_indexbyte
將棧頂引用類型值保存到局部變數indexbyte中。註:ldc 在常量池中沒有對應的常量值時JVM會在常量池中創建該常量值對象。ldc後面的#index是指在常量池中的編號
所以,建議在日常開發中,儘量使用直接賦值的方式去創建String對象,這樣可以節省一部分空間。
雖然堆記憶體的垃圾會有垃圾回收器去回收,但垃圾回收器是隨機去回收的,我們不能讓回收器立即回收某個不再使用的對象,但可以顯示的表明那個對象不再使用了建議垃圾回收器去回收,但它還是不會立即回收。
Q3:==與equals比較的區別
==在對字元串比較的時候,對比的是記憶體地址,而equals比較的是字元串內容,在開發的過程中, equals()通過接受參數,可以避免空指向。對空指針的對象調用方法也是一件錯誤的事,因為他沒有指向具體的實例,所以其中包含的方法無從得知
方法
返回字元串的長度
public int length() { return value.length; }
底層是返回字元數組的長度
返回某個字元在此字元串上出現的索引
//返回變數ch在此字元串中第一次出現的索引
public int indexOf(int ch)
//返回變數ch在此字元串中最後一次出現的索引
public int lastIndexOf(int ch)
將指定的類型值轉換為字元串
public static String valueOf(Object obj)
可以是任何類型,底層是在調用toString
方法
將字元串轉換為大小寫
//轉小寫
public String toLowerCase()
//轉大寫
public String toUpperCase()
根據JVM的預設語言環境轉換
用指定的字元替換掉字元串中的某個字元
public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)
用指定的字面替換序列替換此字元串中與目標字面序列相匹配的每個子串。CharSequence介面被String等類實現。
根據參數分割字元串
public String[] split(String regex)
public String[] split(String regex, int limit)
根據regex分割字元串,也可以根據limit分割成多少個子串,返回一個字元串數值
將字元串從指定索引截取返回索引後面的字元串
public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)
判斷字元串是否以指定的子字元串開始或結束
public boolean startsWith(String prefix)
public boolean endsWith(String suffix)
判斷字元串是否包含指定子字元串
public boolean contains(CharSequence s)
判斷字元串長度是否為0
public boolean isEmpty()
判斷字元串與指字元串內容是否相同
public boolean equals(Object anObject)
String重寫了equals方法,還有比較字元串內容但忽略大小寫的,equalsIgnoreCase
拼接字元串
java允許使用+號連接兩個字元串,如果與非字元串的值進行拼接時,非字元串的值會被轉換成字元串
public static String join(CharSequence delimiter, CharSequence... elements)
多字元串拼接時,可以用join靜態方法,參數delimiter是用指定的定界符分隔這些字元串
String不可變
參考文獻:
《Effective Java》中對於不可變對象的定義是:對象一旦被創建後,對象所有的狀態及屬性在其生命周期內不會發生任何變化
當我們嘗試對一個String對象再次賦值,String會新創建一個對象,舊對象還存在,但沒有被引用。此時,記憶體中就會存在兩個對象。
String str = "s1";
str = "s2";
String的不可變不僅僅是因為底層數組被final修飾,從而無法被修改。
-
底層char數組被private修飾,且內部沒有對外提供修改數組的方法,所以外界沒有有效的手段去改變它
-
String被final修飾,避免被繼承破壞
-
在String的中,避免了去修改char數組的代碼,涉及對char數組的操作都會重新去創建一個對象
為什麼要不可變
- 如果代碼中出現了大量頻繁的創建字元串,可以提高性能和減少記憶體開銷。創建字元串時,首先檢查字元串常量池中是否存在該字元串。存在,則返回該引用實例;不存在,則實例化該字元串放入池中,返回引用實例
- 為了安全。不可變可以保證線程安全。當多個線程同時調用同一個字元串時,如果有一個線程改變了字元串內容,那將是個很危險的操作
- 字元串池的要求,字元串常量池是一個特殊的存儲區。當字元串符被創建,並且該字元串已經存在池中,返回已有字元串的引用,而不是創建新對象。如果String是可變的,通過一個字元串引用改變字元串,那麼會導致其他字元串引用的值錯誤
字元串共用
字元串常量池String Pool是JVM實例全局共用的,JAVA會確保池中每個不同的字元串只存在一個拷貝,不會存在相同字元串出現兩份拷貝在池中。這樣的設計模式稱為“享元模式”,採用一個共用來避免大量擁有相同內容對象的開銷
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
因為相同的字元串都是引用字元串常量池中的一個字元串常量,所以以上輸出的是true
。
JVM怎麼判斷新創建的字元串需不需要在Java Heap(堆)中創建 新對象呢?
先根據比較與String Pool中某一個是否相等,如果有,則使用其引用。反之則根據的字面量創建一個字元串對象,再將這個字面量與字元串對象引用關聯起來
AbstractStringBuilder
AbstractStringBuilder是對可變字元序列的概念描述,其提供了可變字元序列的基本協議約定。其內部也有用於存儲字元串的字元數組,與String不同,其不被final修飾,也就是AbstractStringBuilder的內部成員數組是可變的。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
成員變數count用於記錄實際的字元個數
通過有參構造可以為value引用一個指定大小的數組,當然這是給子類調用的
既然底層是個數組,數組又是順序存儲的,那對數組做操作必然會出現大量的元素移動
方法
獲取長度
@Override
public int length() {
return count;
}
public int capacity() {
return value.length;
}
length()用於獲取數組實際數據的個數
capacity()獲取數組的容量。
如果實際數據的個數超過數組的容量,則容量自動增大
設置長度
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);
if (count < newLength) {
Arrays.fill(value, count, newLength, '\0');
}
count = newLength;
}
設置數組的容量。
自動擴容
查閱源碼,在每次對value數組做操作時都會調用ensureCapacityInternal()
方法用於確保空間大小足夠。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
底層是拷貝數組,重新分配大小,大小由newCapacity()方法來決定
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
擴容是原數組長度*2再+2。
如果*2再+2之後的容量夠大,那數組容量就使用這個數值
如果*2再+2之後的容量還不夠大,先檢查數值是否比
Integer.MAX_VALUE
還大,成立則OutOfMemoryError()
;再和MAX_ARRAY_SIZE
比較,如數值較大,則使用數值,反之使用MAX_ARRAY_SIZE
去除未使用的空間
數組中除count-1
外,其他的索引都由'\0'
來占用,這就產生資源浪費
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
trimToSize()方法重新拷貝一個以count為目標容量的數組
獲取和設定指定索引的值
@Override
public char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
先檢查索引是否越界,再返回指定值
public void setCharAt(int index, char ch) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
value[index] = ch;
}
先檢查索引是否越界,再給索引處指定值
append方法
很多重載的append方法都會去調用getChars()方法實現從尾部插入數值
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
StringBuileder、StringBuffer
和String不同的是,StringBuileder、StringBuffer是一個可變的字元序列。StringBuilder、StringBuffer也是實現CharSequence介面,但它倆還繼承了AbstractStringBuilder類。
它倆的內部方法基本都是從AbstractStringBuilder類繼承下來。構造函數也是調用自父類。
public StringBuffer() {
super(16);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
三者的區別
1.String不可改變的,線程安全的;StringBuileder是可變的,非線程安全的;StringBuffer也是可變的,線程安全的,推薦在多線程里使用。
String str = "hello";
str = str + " word";
上面代碼在第一行我們創建了String對象,並把“hello”字元串在常量池中的引用和str關聯。在執行第二行,先把“hello”和“word”做拼接,再把拼接後新的String對象存儲到常量池中,再把新的String對象在常量池中的引用和str關聯。而之前的對象並沒有發生變化,且之前的對象會被垃圾回收器CG回收掉。
而StringBuffer和StringBuilder則不會,因為底層沒有對數組和類做final修飾,所以可以對這個數組“重定義”,當對他們的字元串做操作也就是對這個對象做操作,不會再去創建額外的對象
2.字元串對象使用“+”與字元串或字元串對象做拼接時,編譯器碰到每個“+”時,會去new一個StringBuilder並調用append做拼接,最後再調用toString返回字元串
String str = "hello";
str += " word";
str += "!!!";
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 hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String word
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: new #3 // class java/lang/StringBuilder
26: dup
27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
30: aload_1
31: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc #8 // String !!!
36: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_1
43: return
從位元組碼可以看到,第3和23行新建了兩次StringBuilder,而這樣的拼接方式無疑是對記憶體的一種浪費,因為要額外創建對象,所以效率也不是很好。
如果在程式中要對字元串對象做拼接,建議使用StringBuilder或StringBuiffer
2.在大多數情況下,執行速度上比較,StringBuilder > StringBuffer > String
但是,下麵的代碼就會是String執行的比較快
String str = "hello" + "word" + "!!!";
StringBuilder sb = new StringBuilder("hello").append("word").append("!!!");
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String helloword!!!
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: ldc #4 // String hello
9: invokespecial #5 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
12: ldc #6 // String word
14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #8 // String !!!
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: astore_2
從位元組碼可以看出,程式中的第一行代碼,JVM會自動解析成String str = “helloword!!!“
,因為這些字元串都是編譯期間即可知的常量。這種情況,String會比StringBuffer執行的更快些,但是如果拼接的是對象而不是字元串則不會這樣。
總結:如果只是簡單的的聲明字元串,沒有過多的操作,那麼使用String或StringBuilder都可,但後續要對這個字元串有過多頻繁的操作則建議使用StringBuilder。