Java--String類的一些思考

來源:java核心技術,博客

String類型是我們java開發中比較常見的一個類,但是這個常見的類卻隱藏着很多的知識點,現做一個總結。

一、創建String的方式

1. String str1 = “hello”

​ 要深入明白這種創建String的方式,就不得不提到字符串常量池

字符串常量池:爲了減少在JVM中創建的字符串的數量,字符串類維護了一個字符串池,每當代碼創建字符串常量時,JVM會首先檢查字符串常量池。如果字符串已經存在池中,就返回池中的實例引用。如果字符串不在池中,就會實例化一個字符串並放到池中。

注意:常量池在java用於保存在編譯期已確定的,已編譯的class文件中的一份數據。

使用這種方式創建字符串,在編譯期的時候就對常量池進行判斷是否存在該字符串,如果存在則不創建直接返回對象的引用;如果不存在,則先在常量池中創建該字符串實例再返回實例的引用給str1

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6siAYYdF-1584712760098)(C:\Users\four and ten\Desktop\筆記\JAVA\String\1.png)]

2.String str2 = new Stirng(“hello”)

​ str2的創建使用new關鍵字,首先,JVM會檢查字符串常量池,如果該字符串在常量池中,那麼不再在字符串常量池創建該字符串對象,而直接堆中複製該對象的副本,然後將堆中對象的地址賦值給引用str2,如果字符串不存在常量池中,就會實例化該字符串並且將其放到常量池中,然後在堆中複製該對象的副本,然後將堆中對象的地址賦值給引用str2。
在這裏插入圖片描述

在jdk7之後,字符串常量池被移到了堆中了

3.常見問題,加深理解
String str1 = "hello";
String str2 = "hello";
//true
System.out.println(str1 == str2);
String str3 = new String("hello");
String str4 = new String("hello");
//false
System.out.println(str3 == str4);
//false
System.out.println(str1 == str3);

分析:“==”比較的是該變量的值,也就是說,比較的是str1和str2的內存地址,而不是比較str1和str2指向的內容。對於str1和str2都是使用字面量的方式創建的,str1和str2都是指向常量池中的“hello”,所以爲true;對於str3和str4,指向的是在堆中的不同"hello";str1與str3比較很顯然是false,指向的內存區域都不一樣

str1與str2比較

在這裏插入圖片描述

str3與str4比較

在這裏插入圖片描述

爲驗證上述分析是否正確,現在反編譯一下,爲了使反編譯結果更加清晰,選擇先將後面兩個比較代碼註釋掉。

String str1 = "hello";
String str2 = "hello";

反編譯結果:javap -c

Compiled from "StringDemo.java"
public class com.Demo.StringDemo {
  public com.Demo.StringDemo();
    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 hello
       2: astore_1					
       3: ldc           #2                  // String hello
       5: astore_2
       6: return
}

查詢jvm指令碼

ldc :將int, float或String型常量值從常量池中推送至棧頂

astore_1:將棧頂引用型數值存入第二個本地變量

astore_2 :將棧頂引用型數值存入第三個本地變量

然後查看常量池:javap -verbose

在這裏插入圖片描述

#2 代表的就是"hello",那麼從反編譯的結果來看,確實str1和str2引用了常量池中的同一個對象。

接下來,分析str3和str4,爲了避免其他因素,把字符串的值改一下

String str3 = new String("hello world");
String str4 = new String("hello world");

反編譯結果:

Compiled from "StringDemo.java"
public class com.Demo.StringDemo {
  public com.Demo.StringDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String hello world
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: new           #2                  // class java/lang/String
      13: dup
      14: ldc           #3                  // String hello world
      16: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      19: astore_2
      20: return
}

查看常量池:

在這裏插入圖片描述
這也證實了,使用new的方式創建一個字符串,要先判斷常量池中是否有有該對象,如果沒有那麼要先在常量池中創建該對象,然後拷貝到堆中,然後將地址值賦值給變量str。這裏也有一個很常見的問題

​ 上述代碼在執行的過程中創建了幾個對象?

​ 很顯然,通過反編譯可以得知在常量池內創建了一個對象,在對堆內創建了2個對象

二、String的不可變性

​ 在String的官方API上,有這麼一段話:Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings,翻譯過來就是字符串不變; 它們的值在創建後不能被更改。 字符串緩衝區支持可變字符串。

​ 這句話我認爲有兩個理解:在常量池中不可能存在一模一樣的字符串,對String字符串的任意操作都會創建一個新的對象,而不是使用當前對象,

三、String字符串“+”引發的一些問題思考

String str1 = "a";
String str2 = "b";
String str3 = "a" + "b";
String str4 = "ab";
String str5 = str1 + str2;
//true
System.out.println(str3 == str4);
//false
System.out.println(str3 == str5);

反編譯一下看看什麼結果

在這裏插入圖片描述

​ 從反編譯的結果很顯然可以看出,對於str3由於+號兩側都是編譯期確定的字符串常量,JVM會直接從常量池中將這兩個字符串拼接好,那麼str3 == str4的結果是true(前面說過使用這種方式創建字符串會在常量池中拿)。

​ 對於str5加號兩側都是變量,都是在編譯期無法確定的,那麼使用”+“時,JVM會自動隱式的創建一個StringBuilder,然後調用append()方法,最後調用toString將結果返回至str5,那麼str3 == str5的結果自然時false,str5與str3所指的內存區域都不一樣,str3指向常量池,str5指向堆。

對上面代碼進行修改

final String str1 = "a";
final String str2 = "b";
String str3 = "a" + "b";
String str4 = "ab";
String str5 = str1 + str2;
//true
System.out.println(str3 == str4);
//true
System.out.println(str3 == str5);

同樣進行反編譯

在這裏插入圖片描述

​ 可以很顯然的看到,str5直接從常量池中獲取字符串,這是因爲當變量有final修飾時,JVM會在編譯期將該變量看成一個常量,將該值拷貝到常量池中,這樣以來,str5也是直接從常量池中獲取值,所以結果是true。

​ 從上面可以看出,對於”+“號兩側都是常量,JVM會直接從常量池中獲取,效率較高,節省空間;對於對於”+“號兩側不都是常量(一個常量,0個常量)的字符串拼接,JVM會創建StringBuffered然後調用其方法append,最後使用toString將返回值賦給變量。所以在對不都是常量的字符串拼接時儘量不要使用String的”+“號,這樣效率更低。

四、intern方法

​ String的intern是一個native本地方法,這個方法的解釋是:當調用 intern 方法時,如果常量池中已經該字符串,則返回池中的字符串;否則將此字符串添加到常量池中,並返回字符串的引用。

​ 通過一個例子來很好的理解這個方法,前面說過

String str1 = "hello";
String str3 = new String("hello");
//false
System.out.println(str1 == str3);

因爲str1與str3指向的內存地址不一樣,但是如果使用intern方法,則結果時true

String str1 = "hello";
String str3 = new String("hello");
//true
System.out.println(str1 == str3.intern());

​ 前面說過,str3的創建方式是先看字符串常量池中是否有該字符串,如果沒有則創建一個,如果有那麼將該字符串拷貝一份到堆中,然後返回該對象的引用給str3,也就是說str3指向的內存區域是堆。

​ 但是一但使用intern方法,則JVM會查看常量池中是否有該字符串,如果有那麼返回常量池中的字符串引用,如果沒有則創建一個在返回該引用,也就是說使用intern發方法則不會再堆上創建該對象的引用。

​ 使用這個方法的用處很顯然,當重複使用的字符串數量很多時,例如,”hello“這個字符串要使用10000次,那麼如果每次都要從堆上創建對象,那麼將會很耗時而且很消耗空間。

​ 在看下面代碼

String str1 = "hello";
String str3 = new String("hello");
str3.intern();
System.out.println(str1 == str3);

用這個方法的用處很顯然,當重複使用的字符串數量很多時,例如,”hello“這個字符串要使用10000次,那麼如果每次都要從堆上創建對象,那麼將會很耗時而且很消耗空間。

​ 在看下面代碼

String str1 = "hello";
String str3 = new String("hello");
str3.intern();
System.out.println(str1 == str3);

​ 與上面代碼的不同之處在於將str3另起一行,輸出的結果就是false,原因很顯然,前面說過字符串是不可變的,對他的任意操作都是創建一個副本,那麼str3調用了intern方法,但str3還是str3還是指向了堆上的空間,結果自然爲false。這就類似與強制類型轉換,知識在使用副本操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章