天天看點

【面試準備】關于String的若幹細節你有真正了解 String 嗎

大家好,我是被白菜拱的豬。

一個熱愛學習廢寝忘食頭懸梁錐刺股,癡迷于girl的潇灑從容淡然coding handsome boy。

假如你喜歡我的文字,歡迎關注公衆号“放開這顆白菜讓我來”。

文章目錄

  • 你有真正了解 String 嗎
    • 前言
    • 簡介
    • 字元串概述
    • 字元串常量池
      • jdk 版本的變化
      • 插入方式
      • new String("ljl is a good man");
    • 字元串拼接
      • 常量拼接
      • 變量拼接
      • 拓展
    • intern()
    • 版本差別
    • 面試題
    • 總結

你有真正了解 String 嗎

前言

每日一釘第2期,關于String那些事。

在正文開始之前,再簡單說說寫此系列的目的,首先對我來說,将知識點通過我的語言加上我的了解在複述一遍,以便加深對知識的了解,進而達到在面試過程中能夠“侃侃而談”,做到肚子有貨,心中不慌。二是通過每日一釘的方式督促自己,告别拖延(可是現在晚上十一點,不會吧,不會剛立的 flag 就要打臉吧)。其次,對于讀者,相信清單中也有很多打算找工作的同學,一起努力,一起進步,亡羊補牢,為時不晚,希望我的文字能夠幫助你們。

關于文章内容本身,主要針對 Java 面試。每天搞定一個知識點,日積月累,我相信也是一筆很大的收獲。好了,接下來跟随我的腳步看看 String 背後我們不常見的一面。

簡介

本次主要講述字元串常量池、字元串的拼接、intern()然後結合相關面試題,通過 JVM 層面進行講解,深入剖析底層原理。以前對于相關内容,我們可能隻是死記硬背,生硬的語言不但難以記憶,時間長了就會面臨忘掉的尴尬局面。而我們學習知識我認為要向張無忌學習七傷拳一樣,把握好其本質,不必糾結于用詞是否跟标準答案一摸一樣。而本篇内容也将會在一種“忘我”的狀态下進行敲寫。

字元串概述

在編碼的過程中,沒有人不使用字元串吧,那對字元串又有多少了解呢,我想這是一個值得深思的問題,看完這篇文章我相信對于小白而言将會有很大的收獲,為什麼這麼講,因為我也是一個小白。

字元串最重要的一個特性是不可變性。為什麼這麼講?我們可以點開源碼看看它的類結構。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

它是用 final 修飾的,不能被繼承,然後實際維護的是一個 final 修飾的char 型數組來儲存字元串資料。但是在 jdk 9及以後,變成了byte[],這是為什麼呢?我們看看官網怎麼說。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

大緻翻譯一下就是說我們用char存儲要占兩個位元組,但是大部分字元是拉丁文,拉丁文隻用一個位元組就可以存儲,是以一半的空間就浪費掉了。是以我們換成了byte來存儲字元串,那之前占用兩個位元組的字元怎麼辦呢?像中文就占用兩個字元,文檔提出用一個encoding-flag 一個标記來表明他占兩個字元,這樣就節省了很多空間。

當然對應的 StringBuilder 和 StringBuffer 也做了相應的改變。

字元串常量池

在記憶體配置設定上,字元串則是放在一個叫做字元串常量池的地方。常量池就類似于一個 Java 系統級别提供的緩存,我們需要什麼就先看看裡面有沒有,有就直接拿去使用,沒有在去建立,這樣保證了運作的速度和記憶體的節省。

字元串常量池一個很重要的特性就是不會存儲相同内容的字元串。

jdk 版本的變化

而且需要注意的是 jdk6 與 jdk6以後的版本,字元串常量池所放的位置是不同的。在jdk6以後,字元串常量池由原先的永久代(jdk 8 變成了元空間)區域移動到了堆中。這是一個需要我們注意的點,所放的不同的位置,決定了他最後存儲的機制是不一樣的。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

在 jdk8中,字元串常量池依然放在堆中,隻不過永久代變成了元空間,放在了本地記憶體而不是 JVM 配置設定的記憶體。

插入方式

說了那麼多,那麼如何将字元串放入到字元串常量池中呢?有兩種方式:

  • 第一種是使用字面量的方式,也就是直接使用雙引号。直接使用雙引号出來的 String 對象會直接存儲在常量池中。比如:String info = “ljl is a handsome boy”;
  • 第二種可以使用 String 提供的 intern() 方法,這個後面重點說明

這時就有人問了,那使用 new 關鍵字建立一個 String對象呢?比如:String info = new String(“ljl is a good man”);

這個就有的說了。

new String(“ljl is a good man”);

首先我們引出一個面試常問的問題。

題目: new String(“ljl is a good man”)會建立幾個對象?

看到這個題目有些懵逼,new 這不是 new 一個對象嘛,這麼簡單的問題還會問,面試官怕不是個弱zhi,假如這麼想你就錯了,人人都會還會提出這個問題嗎?答案是兩個。為什麼是兩個,我們深入底層,看看位元組碼檔案,一看便知。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

一個對象是:new 關鍵字在堆空間建立的。

另一個對象是:在字元串常量池建立的對象“ljl is a good man”。位元組碼指令:ldc。

我們可以這樣記憶,将new String(“ljl is a good man”);分成兩部分,一個是括号裡面的“ljl is a good man”,另一個是new String();前者就是我們前面講的使用字面量的方式也就是直接使用引号,将字元串在字元串常量池存儲,而 new 則是在堆中建立一個普通的對象,然後把常量池中的字元串取出來然後對其進行指派。

但是實際上兩種不同的方式維護的确實同一個數組,這點僅做了解即可,我們可以通過反射的方式驗證常量池中的“abc”和堆中的“abc”底層儲存的數組是同一個。

@Test
public void test1() throws Exception{
    String s1 = "abc";
    String s2 = new String("abc");
    Field field = String.class.getDeclaredField("value");
    //将字段設定為可通路的
    field.setAccessible(true);
    char[] arr = (char[]) field.get(s1);
    arr[0] = 'l';
    System.out.println(s1);//輸出是lbc
    System.out.println(s2);//輸出也是lbc
}
           

為了避免頭腦風暴,我們可以簡單的想堆中有一個對象,值為abc,常量值也有一個對象值也是abc。

字元串拼接

常量拼接

字元串拼接主要有兩種方式,一個是常量與常量的拼接:

【面試準備】關于String的若幹細節你有真正了解 String 嗎

通過位元組碼發現,“a” + “b” + “c” 實際上等同于“abc”,這是因為編譯器的優化,在編譯的時候就将“a” + “b” + “c”拼接完成。是以s1,s2都是指向字元串常量池同一個位址,s1 == s2的結果為true。

變量拼接

另一種是變量與常量的拼接,或者說結果的對象其中有一個是對象,那麼拼接的結果就在堆中,而非前面所說的在常量池中,我們通過位元組碼看一看拼接的原理。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

我們發現變量拼接是建立了一個 StringBuilder 對象,然後通過 StringBuilder 的append方法進行字元串拼接,然後最後調用 toString() 方法。

然後我們點開 StringBuilder的toString()方法。

【面試準備】關于String的若幹細節你有真正了解 String 嗎

我們發現建立了一個String對象,但是這裡跟前面我們将的 new String(“abc”)不同,StringBuilder的toString隻建立了一個對象,沒有ldc指令,也就是說沒有在常量池建立對象,這一點很重要。

最後我們比較 == 是否相等時,自然不會是true了,因為一個是堆中的位址,一個是字元串常量池中的位址。

拓展

既然說到拼接,我們思考 new String(“a”) + new String(“b”)會建立幾個對象呢?

首先根據前面講過 new String(“a”)的例子,他會建立兩個對象,是以兩個 new 會建立4個對象,最後加上拼接會建立一個StringBuilder是以是五個對象,最後又調用了StringBuilder.toString() 注意這裡的 toString() 裡面的new String(),隻建立了堆中的對象,常量池裡面沒有 “ab” 的。是以總共應該是六個對象。下面看位元組碼:

【面試準備】關于String的若幹細節你有真正了解 String 嗎
/* new String("a") + new String("b")呢?
 *  對象1:new StringBuilder()
 *  對象2: new String("a")
 *  對象3: 常量池中的"a"
 *  對象4: new String("b")
 *  對象5: 常量池中的"b"
 *
 *  深入剖析: StringBuilder的toString():
 *      對象6 :new String("ab")
 *       強調一下,toString()的調用,在字元串常量池中,沒有生成"ab"
 */
           

intern()

【面試準備】關于String的若幹細節你有真正了解 String 嗎

通過閱讀文檔,我們知道可以将intern()的作用簡單概括:

比如 String s1 = new String(“abc”);String s2 = s1.intern();

s1.intern()的作用就是檢查字元串常量池中是否含有“abc”,假如有就将字元串常量池該字元串的位址傳回,沒有則在字元串常量池中建立“abc”,然後将此位址傳回,是以 s2 指向的是“abc”在字元串常量池中的位址。這樣確定字元串在記憶體裡隻有一份拷貝,可以節約記憶體空間。多次建立new String(“abc”).intern(),這樣避免了在堆中重複建立對象。

版本差別

intern()随着 jdk 版本的不同,具體的實作也不同,我們知道自 jdk7 以後字元串常量池由方法區(永久代)移動到了堆,因為new 的對象是在堆中建立的,然後使用intern(),就會導緻堆中有兩份空間内容實質上是一樣的,是以就造成空間的浪費,而 jdk6,方法區與堆是不同的地方,是以就無法做這個節省。針對這兩種情況,有下面兩種方式:

總結 String 的 intern() 的使用

  • jdk 1.6 中,将這個字元串對象嘗試放入串池。
    • 如果串池中有,則并不會放入。傳回已有的串池中的對象的位址。
    • 如果沒有,會把此對象複制一份,放入串池,并傳回串池中的對象位址。
  • jdk 1.7 起,将這個字元串對象嘗試放入串池。
    • 如果串池中有,則并不會放入。傳回已有的串池中的對象的位址。
    • 如果沒有,會把此對象的引用位址複制一份,放入串池,并傳回串池中的對象位址。

jdk1.6:

【面試準備】關于String的若幹細節你有真正了解 String 嗎

jdk1.7起:

【面試準備】關于String的若幹細節你有真正了解 String 嗎

面試題

@Test
public void test5() {
    //String s1 = new String("ab");//執行完以後,會在字元串常量池中會生成"ab"
    String s1 = new String("a") + new String("b");執行完以後,不會在字元串常量池中會生成"ab"
    s1.intern();
    String s2 = "ab";
    System.out.println(s1 == s2);
}
           

s1 有兩種形式:

​ 一是 new String(“ab”);

​ 二是 new String(“a”) + new String(“b”);

它們的差別在前面講過,在于字元串常量池中是否含有"ab",然後調用intern(),會根據jdk的版本發生變化,進而結果也會發生變化。

總結

看完這篇文章,正如開頭所說,是不是對 String 有了不一樣的看法了呢?高中數學老師常說概念,概念。任其題目發生千萬變化,最後還是要回歸最初的本質,要學會知其然也要知其是以然,這裡推薦去看看尚矽谷康師傅的 JVM 教程,你會對 Java 有一個不一樣的認識,之前模棱兩可的内容都會有清晰的認識。

正如身邊的人和物,你是否又真正的了解呢?

這篇文章,花費了不少時間,假如對你有所幫助,不如點個在看加個關注呀。聽說今天上證指數 3600了,心中着實有些小慌張。

你知道的越多,你不知道的越多。