沒想到,一個小小的String還有這麼多竅門!

guoduan發表於2020-06-12

  沒想到,一個小小的String還有這麼多竅門!

  版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。

  本文連結:https://blog.csdn.net/xqnode/article/details/106663571

  收起

  本文是我和武哥聯合創作,已收錄至我們的GitHub,歡迎大家給個Star:

  微信搜尋:Java學習指南,關注這個專注於分享Java乾貨的公眾號~

  文章目錄

  1. 看看原始碼

  2. 不可變有什麼好處呢

  2.1 可以快取 hash 值

  2.2 String Pool 的使用

  2.3 安全性

  2.4 執行緒安全

  3. 再來深入瞭解一下 String

  3.1 "+" 連線符

  3.2 “+”連線符的效率

  4. 字串常量

  4.1 為什麼使用字串常量?

  4.2 實現字串常量池的基礎

  5. String類常見的面試題

  5.1 判斷字串s1和s2是否相等

  5.2 建立多少個字串物件?

  1. 看看原始碼

  大家都知道, String 被宣告為 final,因此它不可被繼承。(Integer 等包裝類也不能被繼承)。我們先來看看 String 的原始碼。

  在 Java 8 中,String 內部使用 char 陣列儲存資料。

  public final class String

  implements java.io.Serializable, Comparable, CharSequence {

  /** The value is used for character storage. */

  private final char value[];

  }

  1

  2

  3

  4

  5

  在 Java 9 之後,String 類的實現改用 byte 陣列儲存字串,同時使用 coder 來標識使用了哪種編碼。

  public final class String

  implements java.io.Serializable, Comparable, CharSequence {

  /** The value is used for character storage. */

  private final byte[] value;

  /** The identifier of the encoding used to encode the bytes in {@code value}. */

  private final byte coder;

  }

  value 陣列被宣告為 final,這意味著 value 陣列初始化之後就不能再引用其它陣列。並且 String 內部沒有改變 value 陣列的方法,因此可以保證 String 不可變。

  2. 不可變有什麼好處呢

  2.1 可以快取 hash 值

  因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。

  2.2 String Pool 的使用

  如果一個 String 物件已經被建立過了,那麼就會從 String Pool 中取得引用。只有 String 是不可變的,才可能使用 String Pool。

  2.3 安全性

  String 經常作為引數,String 不可變性可以保證引數不可變。例如在作為網路連線引數的情況下如果 String 是可變的,那麼在網路連線過程中,String 被改變,改變 String 的那一方以為現在連線的是其它主機,而實際情況卻不一定是。

  2.4 執行緒安全

  String 不可變性天生具備執行緒安全,可以在多個執行緒中安全地使用。

  3. 再來深入瞭解一下 String

  3.1 “+” 連線符

  字串物件可以使用“+”連線其他物件,其中字串連線是透過 StringBuilder(或 StringBuffer)類及其 append 方法實現的,物件轉換為字串是透過 toString 方法實現的。可以透過反編譯驗證一下:

  /**

  * 測試程式碼

  */

  public class Test {

  public static void main(String[] args) {

  int i = 10;

  String s = "abc";

  System.out.println(s + i);

  }

  }

  /**

  * 反編譯後

  */

  public class Test {

  public static void main(String args[]) { //刪除了預設建構函式和位元組碼

  byte byte0 = 10;

  String s = "abc";

  System.out.println((new StringBuilder()).append(s).append(byte0).toString());

  }

  }

  由上可以看出,Java中使用"+"連線字串物件時,會建立一個StringBuilder()物件,並呼叫append()方法將資料拼接,最後呼叫toString()方法返回拼接好的字串。那這個 “+” 的效率怎麼樣呢?

  3.2 “+”連線符的效率

  使用“+”連線符時,JVM會隱式建立StringBuilder物件,這種方式在大部分情況下並不會造成效率的損失,不過在進行大量迴圈拼接字串時則需要注意。比如:

  String s = "abc";

  for (int i=0; i<10000; i++) {

  s += "abc";

  }

  這樣由於大量StringBuilder建立在堆記憶體中,肯定會造成效率的損失,所以在這種情況下建議在迴圈體外建立一個StringBuilder物件呼叫append()方法手動拼接(如上面例子如果使用手動拼接執行時間將縮小到1/200左右)。

  與此之外還有一種特殊情況,也就是當"+"兩端均為編譯期確定的字串常量時,編譯器會進行相應的最佳化,直接將兩個字串常量拼接好,例如:

  System.out.println("Hello" + "World");

  /**

  * 反編譯後

  */

  System.out.println("HelloWorld");

  4. 字串常量

  4.1 為什麼使用字串常量?

  JVM為了提高效能和減少記憶體的開銷,在例項化字串的時候進行了一些最佳化:使用字串常量池。每當建立字串常量時,JVM會首先檢查字串常量池,如果該字串已經存在常量池中,那麼就直接返回常量池中的例項引用。如果字串不存在常量池中,就會例項化該字串並且將其放到常量池中。由於String字串的不可變性,常量池中一定不存在兩個相同的字串。

  4.2 實現字串常量池的基礎

  實現該最佳化的基礎是因為字串是不可變的,可以不用擔心資料衝突進行共享。

  執行時例項建立的全域性字串常量池中有一個表,總是為池中每個唯一的字串物件維護一個引用,這就意味著它們一直引用著字串常量池中的物件,所以,在常量池中的這些字串不會被垃圾收集器回收。

  我們來看個小例子,瞭解下不同的方式建立的字串在記憶體中的位置:

  String string1 = "abc"; // 常量池

  String string2 = "abc"; // 常量池

  String string3 = new String("abc"); // 堆記憶體

  5. String類常見的面試題

  5.1 判斷字串s1和s2是否相等

  public static void main(String[] args) {

  String s1 = "123";

  String s2 = "123";

  String s3 = "1234";

  String s4 = "12" + "34";

  String s5 = s1 + "4";

  String s6 = new String("1234");

  System.out.println(s1 == s2); // true

  System.out.println(s1.equals(s2)); //true

  System.out.println(s3 == s4); //true

  System.out.println(s3 == s5); // false

  System.out.println(s3.equals(s5)); //true

  System.out.println(s3 == s6); // false

  }

  解析:

  s1和s2:

  String s1 = "123";先是在字串常量池建立了一個字串常量“123”,“123”常量是有地址值,地址值賦值給s1。接著宣告 String s2=“123”,由於s1已經在方法區的常量池建立字串常量"123",進入常量池規則:如果常量池中沒有這個常量,就建立一個,如果有就不再建立了,故直接把常量"123"的地址值賦值給s2,所以s1==s2為true。

  由於String類重寫了equals方法,s1.equals(s2)比較的是字串的內容,s1和s2的內容都是"123",故s1.equals(s2)為true。

  s3和s4:

  s3建立了一個新的字串"1234",s4是兩個新的字串"12"和"34"透過"+“符號連線所得,根據Java中常量最佳化機制, “12” 和"34"兩個字串常量在編譯期就連線建立了字串"1234”,由於字串"1234"在常量池中存在,故直接把"1234"在常量池的地址賦值給s4,所以s3==s4為true。

  s3和s5:

  s5是由一個變數s1連線一個新的字串"4",首先會在常量池建立字串"4",然後進行"+“操作,根據字串的串聯規則,s5會在堆記憶體中建立StringBuilder(或StringBuffer)物件,透過append方法拼接s1和字串常量"4”,此時拼接成的字串"1234"是StringBuilder(或StringBuffer)型別的物件,透過呼叫toString方法轉成String物件"1234",所以s5此時實際指向的是堆記憶體中的"1234"物件,堆記憶體中物件的地址和常量池中物件的地址不一致,故s3==s5為false。

  看下JDK8的API文件裡的解釋:

  Java語言為字串連線運算子(+)提供特殊支援,併為其他物件轉換為字串。字串連線是透過StringBuilder (或StringBuffer )類及其append方法實現的。字串轉換是透過方法來實現toString,由下式定義0bject和繼承由在Java中的所有類。有關字串連線和轉換的其他資訊,請參閱Gosling,Joy 和Steele,Java 語言規範。

  不管是常量池還是堆,只要是使用equals比較字串,都是比較字串的內容,所以s3.equals(s5)為true。

  Java常量最佳化機制:給一個變數賦值,如果等於號的右邊是常量,並且沒有一個變數,那麼就會在編譯階段計算該表示式的結果,然後判斷該表示式的結果是否在左邊型別所表示範圍內,如果在,那麼就賦值成功,如果不在,那麼就賦值失敗。但是注意如果一旦有變數參與表示式,那麼就不會有編譯期間的常量最佳化機制。

  s3和s6:

  String s6 = new String("1234");在堆記憶體建立一個字串物件,s6指向這個堆記憶體的物件地址,而s3指向的是字串常量池的"1234"物件的地址,故s3==s6為false。

  5.2 建立多少個字串物件?

  String s0 = "123";

  String s1 = new String("123");

  String s2 = new String("1" + "2");

  String s3 = new String("12") + "3";

  解析:

  String s0 = “123”;

  字串常量池物件:“123”,1個;

  共1個。

  String s1 = new String(“123”);

  字串常量池物件:“123”,1個;

  堆物件:new String(“123”),1個;

  共2個。

  String s2 = new String(“1” + “2”);

  字串常量池物件:“12”,1個(Jvm在編譯期做了最佳化,“1” + "2"合併成了 “12”);

  堆物件:new String(“12”),1個

  共2個。

  由於s2涉及字串合併,我們透過命令看下位元組碼資訊:

  javac StrTest.java //編譯原始檔得到class檔案

  javap -c StrTest.class // 檢視編譯結果

  得到位元組碼資訊如下:

  備註:以上編譯結果基於Jdk1.8執行環境

  我們可以很清晰看到,建立了一個新的String物件和一個字串常量"12",new String("1" + "2") 相當於 new String("12"),共建立了2個字串物件。

  String s3 = new String(“12”) + “3”;

  字串常量池物件:“12”、“3”,2個,

  堆物件: new Stringbuilder().append(“12”).append(“3”).toString();轉成String物件,1個;

  共3個。

  我們同樣看下編譯後的結果:

  可以看到,包括StringBuilder在內,共建立了4個物件,字串"12"和字串"3"是分開建立的,所以共建立了3個字串物件。

  總結:

  new String()是在堆記憶體建立新的字串物件,其構造引數中可傳入字串,此字串一般會在常量池中先建立出來,new String()建立的字串是引數字串的副本,看下API中關於String構造器的解釋:

  String(String original)

  初始化新建立的String物件,使其表示與引數相同的字元序列;換句話說,新建立的字串是引數字串的副本。

  所以new String()的方式建立字串百分百會產生一個新的字串物件,而類似於"123"這樣的字串物件則需要在建立之前看常量池中有沒有,有的話就不建立,沒有則建立新的物件。 "+"運算子連線字串常量的時候會在編譯期直接生成連線後的字串,若該字串在常量池已經存在,則不會建立新的字串;連線變數的話則涉及StringBuilder等字串構建器的建立,會在堆記憶體生成新的字串物件。

  以上就是我們給您帶來的關於Java字串的一些知識總結和麵試技巧,你學廢了嗎?

  ————————————————

  版權宣告:本文為CSDN博主「xqnode」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。

  原文連結: https://blog.csdn.net/xqnode/article/details/106663571


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69971700/viewspace-2697831/,如需轉載,請註明出處,否則將追究法律責任。

相關文章