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不可變
參考文獻:
Why String is immutable in Java?
《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 = https://www.cnblogs.com/hello12153-java/archive/2022/04/21/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 = https://www.cnblogs.com/hello12153-java/archive/2022/04/21/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 = https://www.cnblogs.com/hello12153-java/archive/2022/04/21/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,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/460765.html
標籤:其他
