Java字符串知識(shí)點(diǎn)總結(jié)

Java 字符串就是 Unicode 字符序列,Java 沒(méi)有內(nèi)置的字符串類型,而是在 Java 類庫(kù)中提供了一個(gè)預(yù)定義類 String,每個(gè)用雙引號(hào)括起來(lái)的字符串都是 String 類的一個(gè)實(shí)例。

Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared. For examples:[1]

String str = "abc";
char data[] = {'a', 'b', 'c'};
String str = new String(data);

The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method. String conversions are implemented through the method toString, defined by Object and inherited by all classes in Java. For additional information on string concatenation and conversion, see Gosling, Joy, and Steele, The Java Language Specification.[1]

上面引用JDK 8 API,主要表明 Java 支持 + 進(jìn)行拼接字符串, Java 8中編譯器會(huì)調(diào)用 StringBuilder 或者 StringBuffer 中的 append 方法來(lái)進(jìn)行字符串拼接,然后通過(guò) toString 方法轉(zhuǎn)化為字符串。

源碼實(shí)現(xiàn)

JDK 8

String 類申明為 final ,不能有子類繼承,內(nèi)部定義了 char 數(shù)組進(jìn)行存儲(chǔ)字符串的值,并且用 final 進(jìn)行修飾,表明 String 是不可變的。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    // code
}

JDK 9

Java 9 中,String 類用 byte[] 數(shù)組進(jìn)行存儲(chǔ)字符串, 并且添加了 coder 標(biāo)識(shí)編碼方式。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    /**
     * The value is used for character storage.
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     *
     * Additionally, it is marked with {@link Stable} to trust the contents
     * of the array. No other facility in JDK provides this functionality (yet).
     * {@link Stable} is safe here, because value is never null.
     */
    @Stable
    private final byte[] value;

    /**
     * The identifier of the encoding used to encode the bytes in
     * {@code value}. The supported values in this implementation are
     *
     * LATIN1
     * UTF16
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     */
    private final byte coder;
}

底層用 byte 數(shù)組實(shí)現(xiàn)后,最大的好處就是可以省空間,因?yàn)楹芏嘧址姆秶荚?u00 - uFF 之間,只需要一個(gè) byte 就能存儲(chǔ),之前 char 數(shù)組 需要兩個(gè) byte 才能存儲(chǔ)。

字符串不可變

通過(guò)查看源碼我們可以知道 Java 中字符串是不可變的,具體的工作方式是 Java 語(yǔ)言的設(shè)計(jì)者將字符串放在一個(gè)公共的存儲(chǔ)池,字符串變量都指向性存儲(chǔ)池中相應(yīng)的位置, 這樣共享字符串常量可以提高效率,并且設(shè)計(jì)者認(rèn)為這種共享帶來(lái)的高效率勝于提取,拼接字符串帶來(lái)的低效率。

String Pool

String Pool
  • 字符串類在 Java 堆內(nèi)存中維護(hù)了一個(gè)字符串常量池,字符串常量池保存著所有字符串字面量(literal strings),目的是為了減少在jvm中創(chuàng)建的字符串的數(shù)量,這些字面量在編譯時(shí)期就確定,在運(yùn)行時(shí)可以通過(guò) intern() 方法將字符串添加到 String Pool中 。
  • 當(dāng)創(chuàng)建 String 對(duì)象時(shí),jvm 會(huì)先檢查 String Pool 中是否存在相同的字符串,如果有則返回其引用,如果沒(méi)有就創(chuàng)建一個(gè)相應(yīng)的字符串放入String Pool 中(此過(guò)程為intern),再返回對(duì)應(yīng)的引用。
  • 常量池:用于保存 Java 在編譯期就已經(jīng)確定的,已經(jīng)編譯的class文件中的一份數(shù)據(jù)。包括了類、方法、接口中的常量,也包括字符串常量,如String s = "a"這種聲明方式。

使用 new 創(chuàng)建 String 時(shí)創(chuàng)建了幾個(gè)對(duì)象:

String str = new String("hello");

兩個(gè),使用new 創(chuàng)建 String 時(shí),首先創(chuàng)建 hello 字符串字面量(String literal)并將其放入字符串常量池中,然后在堆內(nèi)存中創(chuàng)建 String 對(duì)象

@HotSpotIntrinsicCandidate
public String(String original) {
    this.value = original.value;
    this.coder = original.coder;
    this.hash = original.hash;
}

并且通過(guò)查看原碼得知,使用String帶參構(gòu)造創(chuàng)建字符串時(shí),不會(huì)完全復(fù)制 value 數(shù)組的內(nèi)容,只會(huì)指向同一個(gè)數(shù)組。

代碼示例:

String a1 = "a";
String b1 = "a";
System.out.println(a1 == b1); // true

String a2 = new String("a");
String b2 = new String("a");
System.out.println(a2 == b2); // false

String hello = "hello";
String lo = "lo";
System.out.println(hello == "hel" + "lo"); // true
System.out.println(hello == "hel" + lo); // false

String world = "world";
final String ld = "ld";
System.out.println(world == "wor" + ld); // true

總結(jié):

  • 使用 "" 創(chuàng)建字符串時(shí),在編譯期將字符串放入String Pool中,字符串對(duì)象引用 String Pool 中的對(duì)象。
  • 使用 new 創(chuàng)建字符串時(shí),會(huì)在堆內(nèi)存中新創(chuàng)建 String 對(duì)象,在運(yùn)行時(shí)創(chuàng)建。
  • 使用 String s = "hel" + "lo" 創(chuàng)建的字符串指向 String Pool 中的字符串常量 "hello", 常量池中不會(huì)有 "hel" 和 "lo"。
  • 使用包含變量的字符串連接符如 "hel" + lo創(chuàng)建的對(duì)象會(huì)存儲(chǔ)在堆中,運(yùn)行時(shí)期才創(chuàng)建;只要 lo是變量,不論 lo 指向池常量池中的字符串對(duì)象還是堆中的字符串對(duì)象,運(yùn)行期"hel" + lo操作實(shí)際上是編譯器創(chuàng)建了StringBuilder 對(duì)象進(jìn)行了 append 操作后通過(guò) toString() 返回了一個(gè)字符串對(duì)象存在 heap
  • 對(duì)于 final String ld = "ld" 是一個(gè)用 final 修飾的變量,在編譯期就已經(jīng)確定了,所以 "wor" + ld相當(dāng)于 "wor" + "ld" , 也指向 常量池中的 "world"。

intern()方法

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

簡(jiǎn)單地說(shuō),intern 方法可以將當(dāng)前字符串對(duì)象添加到 String Pool 中(如果String Pool中不存在通過(guò)equals判斷相同的字符串),并返回其引用,示例代碼如下:

String str = "java";
String str2 = new String("java");
String str3 = str2.intern();
System.out.println(str == str2); // false
System.out.println(str == str3); // true

不可變(immutable)的好處

String 設(shè)計(jì)為不可變主要是從性能和安全方面進(jìn)行考慮

首先只有當(dāng)字符串是不可變時(shí)才能實(shí)現(xiàn)字符串常量池,從而節(jié)約 JVM 內(nèi)存,提高性能。

緩存 hashcode

/** Cache the hash code for the string */
private int hash; // Default to 0

閱讀源碼可知, String 不可變就可以緩存 hashcdoe 對(duì)應(yīng)的 hashcode, 以后每次使用該對(duì)象的 hashcode 時(shí)無(wú)需重新計(jì)算,直接返回即可,這使得字符串很適合作為 HashMap 的 Key,提高效率。

安全性

由于字符串是不可變的,所以用戶名,密碼之類的信息不能被修改,可以確保安全。同時(shí)也不會(huì)存在多線程安全問(wèn)題。

字符串拼接

+ 操作符

使用 + 進(jìn)行字符串拼接效率較低,執(zhí)行一次 String s += "hello";操作,相當(dāng)于

StringBuilder sb = new StringBuilder();
sb.append(str);
sb.appedn("hello");
s = sb.toString();

每次連接字符串都會(huì)構(gòu)建一個(gè)新的 StringBuilder 對(duì)象 ,既費(fèi)時(shí),又耗空間,多次操作不推薦。

concat

// JDK 11
public String concat(String str) {
    int olen = str.length();
    if (olen == 0) {
        return this;
    }
    if (coder() == str.coder()) {
        byte[] val = this.value;
        byte[] oval = str.value;
        int len = val.length + oval.length;
        byte[] buf = Arrays.copyOf(val, len);
        System.arraycopy(oval, 0, buf, val.length, oval.length);
        return new String(buf, coder);
    }
    int len = length();
    byte[] buf = StringUTF16.newBytesFor(len + olen);
    getBytes(buf, 0, UTF16);
    str.getBytes(buf, len, UTF16);
    return new String(buf, UTF16);
}

查看源碼我們可以知道,concat 方法大致分為三步

  • 創(chuàng)建 byte[] 數(shù)組
  • 底層調(diào)用 System.arraycopy 方法進(jìn)行數(shù)組拷貝
  • 返回new String(buf, coder);

多次調(diào)用會(huì)創(chuàng)建多個(gè) byte[] 數(shù)組 以及多個(gè) String 對(duì)象,多次調(diào)用也不推薦。

StringBuilder 和 StringBuffer

StringBuilderStringbuffer 都是 AbstractStringBuilder 的子類

// JDK 8
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;
}

StringBuilder

// JDK 8
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;
    
    // code ...
    
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}

StringBuffer

// JDK 8
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;
     
     // source code ...
     
     @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
 }

從上述的源碼中可以看到 StringBufferStringBuilde 都是調(diào)用其父類的 append 方法

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    // source code ...
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
}

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    // source code...
    
    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
}

從源碼中可以看出 append 方法主要有一下三個(gè)步驟:

  • 判斷入?yún)?str 是否為 null, 如果為 null, 在 value 數(shù)組中追加 "null"
  • 將 value 數(shù)組進(jìn)行擴(kuò)容,基本的擴(kuò)容邏輯為 value 原來(lái)長(zhǎng)度 * 2 + 2,或者 count + str.length, 或者 Integer.MAX_VALUE - 8(減去 8 是因?yàn)橐恍┨摂M機(jī)會(huì)在數(shù)組中保留一些頭信息),取其中的最小值。
  • 最后調(diào)用 System.arraycopy 進(jìn)行字符串拷貝

append 操作大部分操作在擴(kuò)容與數(shù)組拷貝,不用進(jìn)行重復(fù)的 new 創(chuàng)建對(duì)象操作,因此效率較高。

StringBuilder 與 StringBuffer 的區(qū)別

從源碼中我們可以看到, StringBuffer 的 append 方法中多了 synchronized 關(guān)鍵字,因此它是線程安全的,但是效率較低, StringBuilder 線程不安全,效率較高。

格式化輸出

Java 5 沿用了 C 語(yǔ)言函數(shù)庫(kù)中的 printf 方法進(jìn)行輸出格式化,如

System.out.println("%8.2f", x);
// 增加分組分隔符
System.out.println("%,.2f", 10000.0 / 3.0); // 3,333.33

也可以使用靜態(tài)的 String.format 方法創(chuàng)建一個(gè)格式化的字符串,而不打印輸出

String message = String.format("Hello, %s.Next year, you'll be %d", name, age);

本篇文章同步在Github上,歡迎大家來(lái)Github多提issue。

Reference

  1. JDK 8 String
  2. 專題整理之—String的字符串常量池
  3. 專題整理之—不可變對(duì)象與String的不可變
  4. [Java核心技術(shù)·卷 I(原書(shū)第10版)](
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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