Java漫談-String上

本文首發(fā):windCoder.com

由于具體關(guān)注的內(nèi)容的特殊性,如無特殊注明,本文討論均基于Java8。

不可變

String對(duì)象是不可變的。每次修改都是創(chuàng)建了一個(gè)全新的String對(duì)象,以包含修改后的字符串內(nèi)容,最初的String對(duì)象在原處絲毫未動(dòng)。

對(duì)一個(gè)方法而言,參數(shù)是為該方法提供信息的,而不是想讓該方法改變自己的。

  • String類是final的,不可被繼承。
  • String類的本質(zhì)是字符數(shù)組char[], 并且其值不可改變。即:private final char value[];
  • String類對(duì)象有個(gè)特殊的創(chuàng)建的方式,直接賦值,如'''String x = "abc"```, 字面量(String Literals)"abc" 就表示一個(gè)字符串對(duì)象,變量 x 指向其該字符串對(duì)象的地址,即是一個(gè)引用。
  • JVM存在一個(gè)String Pool(String池/字符串常量池/全局字符串池,也有叫做string literal pool),1.7之前處于方法區(qū)中,之后被分離出來放在了堆中。
  • 兩個(gè)有用的類StringBuffer和StringBuilder。前者線程安全,但比后者速度較慢。
  • 1.8新出了一個(gè)StringJoiner類,,用于構(gòu)造由分隔符分隔的字符序列,并可選擇性地從提供的前綴開始和以提供的后綴結(jié)尾。

重載“+”

內(nèi)部并不是創(chuàng)建n個(gè)String對(duì)象,而是創(chuàng)建了一個(gè)StringBuilder對(duì)象,通過其append()方法連接,最后調(diào)用toStrong()方法返回。

當(dāng)為類似String s = "a" + "b" + "c";的單行操作時(shí),編譯器會(huì)執(zhí)行優(yōu)化,在編譯時(shí)直接合成一個(gè)“abc”。

該操作適用于單行“+”操作,不適用于循環(huán)(如for等)。因?yàn)樵谘h(huán)中,每次循環(huán)會(huì)生成一個(gè)新的個(gè)StringBuilder對(duì)象。

循環(huán)時(shí)的手動(dòng)優(yōu)化:在外創(chuàng)建StringBuilder對(duì)象,在循環(huán)內(nèi)部執(zhí)行append()方法拼接字符串。

StringBuilder 是JavaSE5引入的,之前都是StringBuffer。后者是線程安全的,因此開銷會(huì)大些,所以在javaSE5及以后中,字符串操作應(yīng)該還會(huì)更快一點(diǎn)。

創(chuàng)建

創(chuàng)建方式

創(chuàng)建字符串的方式很多,歸納起來有三類:

  • 使用new關(guān)鍵字創(chuàng)建字符串,比如String s1 = new String("abc");
  • 直接指定。比如String s2 = "abc";
  • 使用串聯(lián)生成新的字符串。比如String s3 = "ab" + "c";

分析創(chuàng)建

下面一起看下在創(chuàng)建與運(yùn)行時(shí)內(nèi)部具體發(fā)生了些什么。

示例1

public class StringDemo1 {
    public static void main(String[] args) {
        String s1 = new String("123");
    }
}

當(dāng)僅運(yùn)行這段代碼期間,涉及用戶聲明的幾個(gè)String變量?

答案很簡(jiǎn)單

一個(gè),就是String s。

涉及的實(shí)例/對(duì)象呢?

先說答案

兩個(gè),一個(gè)是字符串字面量"123"所對(duì)應(yīng)的、駐留(intern)在一個(gè)全局共享的字符串常量池中的實(shí)例,另一個(gè)是通過new String(String)創(chuàng)建并初始化的、內(nèi)容與"123"相同的實(shí)例。

至于原因,要從StringDemo1類的編譯說起:

當(dāng)編譯完成,會(huì)生成StringDemo1.class文件,該文件中,"123"會(huì)被提取并放置在class常量池中,當(dāng)JVM加載類時(shí)會(huì)通過讀取該class常量池創(chuàng)建并駐留一個(gè)String實(shí)例作為常量來對(duì)應(yīng)"123"字面量(其引用存儲(chǔ)在String Pool中,未注明時(shí)以下均稱“字符串池”或“常量池”),這是一個(gè)全局共享的,只有當(dāng)字符串池中沒有相同內(nèi)容的字符串時(shí)才需要?jiǎng)?chuàng)建。

當(dāng)執(zhí)行main方法中的new語(yǔ)句時(shí),JVM會(huì)執(zhí)行的字節(jié)碼類似:

0: new           #2                  // class java/lang/String
3: dup
4: ldc           #3                  // String 123
6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1

這之中出現(xiàn)過多少次new java/lang/String就是創(chuàng)建了多少個(gè)String對(duì)象,即代碼String s1 = new String("123");執(zhí)行一次只會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象

下面是RednaxelaFX對(duì)于這段字節(jié)碼含義的描述:

在JVM里,“new”字節(jié)碼指令只負(fù)責(zé)把實(shí)例創(chuàng)建出來(包括分配空間、設(shè)定類型、所有字段設(shè)置默認(rèn)值等工作),并且把指向新創(chuàng)建對(duì)象的引用壓到操作數(shù)棧頂。此時(shí)該引用還不能直接使用,處于未初始化狀態(tài)(uninitialized);

如果某方法a含有代碼試圖通過未初始化狀態(tài)的引用來調(diào)用任何實(shí)例方法,那么方法a會(huì)通不過JVM的字節(jié)碼校驗(yàn),從而被JVM拒絕執(zhí)行。

能對(duì)未初始化狀態(tài)的引用做的唯一一種事情就是通過它調(diào)用實(shí)例構(gòu)造器,在Class文件層面表現(xiàn)為特殊初始化方法<init>。

實(shí)際調(diào)用的指令是invokespecial,而在實(shí)際調(diào)用前要把需要的參數(shù)按順序壓到操作數(shù)棧上。

在上面的字節(jié)碼例子中,壓參數(shù)的指令包括dup和ldc兩條,分別把隱藏參數(shù)(新創(chuàng)建的實(shí)例的引用,對(duì)于實(shí)例構(gòu)造器來說就是“this”)與顯式聲明的第一個(gè)實(shí)際參數(shù)("123"常量的引用)壓到操作數(shù)棧上

最終如圖:

image.png

黑線表示String對(duì)象的內(nèi)容指向。

示例2

public class StringDemo2 {
    public static void main(String[] args) {
        String s1 = new String("123");
        String s2 = "123";
    }
}

這里我們看下String s2 = "123";的字節(jié)碼:

10: ldc           #3                  // String 123
12: astore_2

由此可見s2直接引用的是字符串常量池中的對(duì)象。故該實(shí)例中依舊是生成了2個(gè)實(shí)例對(duì)象。如圖:

image.png

黑線同實(shí)例1中的,紅線為s2引用的指向,因?yàn)槌A砍刂幸呀?jīng)存在"123",所以不會(huì)再創(chuàng)建。s2會(huì)通過查詢常量池獲取池中"123"的地址并指向。

若再加一個(gè)String s3 = new String("123");呢?此時(shí)只會(huì)再創(chuàng)建一個(gè)實(shí)例對(duì)象,從而一共是3個(gè)。從而有了如下:

public class StringDemo2 {
    public static void main(String[] args) {
        String s1 = new String("123");
        String s2 = "123";
        String s3 = new String("123");
        PrintUtill.println(s1==s2);
        PrintUtill.println(s2==s3);
        PrintUtill.println(s1==s3)
    }
}

結(jié)果為:

false
false
false

StringJoiner用法簡(jiǎn)介

StringJoiner類是Java8的一個(gè)新類(還有一個(gè)新類Optional可用來解決空指針的問題),可以通過指定分隔符拼接字符串,功能與String.join方法類似,同時(shí)可選擇性地從提供的前綴開始和以提供的后綴結(jié)尾。這里簡(jiǎn)單展示用法,不做過多討論。

StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add("www").add("windcoder").add("com");
String desiredString = sj.toString();
PrintUtill.println(desiredString);

執(zhí)行結(jié)果:

[www:windcoder:com]

String.join()內(nèi)部實(shí)現(xiàn)則用了StringJoiner,其源碼如下:

    public static String join(CharSequence delimiter, CharSequence... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // Number of elements not likely worth Arrays.stream overhead.
        StringJoiner joiner = new StringJoiner(delimiter);
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        return joiner.toString();
    }

反編譯指令

基礎(chǔ)命令

javap反編譯指令可查看編譯后的.class文件的字節(jié)碼信息,這里是做簡(jiǎn)單的使用記錄,不做過多討論:

javap -c Concatenation

若想查看更詳細(xì)的常量池等信息,可添加-verbose選項(xiàng),即:

javap -c -verbose Concatenation

-c 輸出類中各方法的未解析的代碼,即構(gòu)成 Java 字節(jié)碼的指令。

-verbose 輸出堆棧大小、各方法的 locals 及 args 數(shù),以及class文件的編譯版本。

如當(dāng)想反編譯上面的StringDemo1.class文件,執(zhí)行如下命令即可:

javap -c  StringDemo1.class

指令簡(jiǎn)說

dup 復(fù)制棧頂數(shù)值(數(shù)值不能是long或double類型的)并將復(fù)制值壓入棧頂

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

invokespecial 調(diào)用實(shí)例構(gòu)造器<init>方法, 私有方法和父類方法

官方對(duì)dup的解釋(6.5.dup)如下:

Duplicate the top value on the operand stack and push the duplicated value onto the operand stack.

The dup instruction must not be used unless value is a value of a category 1 computational type (§2.11.1).

官方對(duì)ldc推送String的描述如下,由此也可看出字符串常量池中的存儲(chǔ)的String屬于引用,當(dāng)ldc推送時(shí),其實(shí)推送的也是引用:

The index is an unsigned byte that must be a valid index into the run-time constant pool of the current class (§2.6). The run-time constant pool entry at index either must be a run-time constant of type int or float, or a reference to a string literal, or a symbolic reference to a class, method type, or method handle (§5.1).

.....

if the run-time constant pool entry is a reference to an instance of class String representing a string literal (§5.1), then a reference to that instance, value, is pushed onto the operand stack.(6.5.ldc)

.....

參考資料

  1. 請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè)String實(shí)例”來面試了吧
  2. The SCJP Tip Line Strings, Literally
  3. JEP 122:刪除永久世代
  4. JDK 8 Milestones
  5. JVM指令詳解(上)
  6. jvm 幾個(gè)invoke 指令
  7. JDK 8 Features
  8. JDK 7 Features
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容