String對象的存儲、拼接和比較

8453d916-149f-4849-b29e-7692f8649c3b.jpg
  • [一、String類型介紹]
  • [二、String類型的存儲]
    • [虛擬機運行時內(nèi)存(JDK1.8以后)]
    • [常量池]
    • [String對象的創(chuàng)建]
  • [三、String類型的拼接]
    • [通過concat方法拼接]
    • [通過+號拼接]
  • [四、字符串的比較]
    • [equals方法]
    • ["=="運算符]

( 以下源碼都基于jdk11)

一、String類型介紹

String類型是引用數(shù)據(jù)類型,表示字符串類型。String底層使用byte[]數(shù)組來存儲char[]數(shù)組。(JDK1.9及以后的版本,JDK1.9之前是使用char數(shù)組保存,1.9為了節(jié)省空間,開始使用byte數(shù)組保存)

@Stable
private final byte[] value;//定義byte數(shù)組用于存儲構(gòu)造函數(shù)傳進的char數(shù)組,最下方的代碼中有用到。

從上方的代碼中可以看出,String用于保存數(shù)據(jù)的數(shù)組是private、final的,因此String類型是不可變的。

//String的構(gòu)造函數(shù)
public String(char value[]) {  this(value, 0, value.length, null);//調(diào)用另一個構(gòu)造函數(shù),代碼在下方     }

String(char[] value, int off, int len, Void sig) {
    if (len == 0) {
        this.value = "".value;
        this.coder = "".coder;
        return;
    }
    if (COMPACT_STRINGS) {
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {
            this.value = val;
            this.coder = LATIN1;
            return;
        }
    }
    this.coder = UTF16;
    this.value = StringUTF16.toBytes(value, off, len);
}

二、String類型的存儲

虛擬機運行時內(nèi)存(JDK1.8以后)

在這里插入圖片描述

JVM內(nèi)存中與String類型存儲相關(guān)的結(jié)構(gòu)主要有堆和虛擬機棧。

常量池

常量池在java用于保存在編譯期已確定的,已編譯的class文件中的一份數(shù)據(jù)。它包括了關(guān)于類,方法,接口等中的常量,也包括字符串常量,如String s = "java"這種申明方式;當然也可擴充,執(zhí)行器產(chǎn)生的常量也會放入常量池,故認為常量池是JVM的一塊特殊的內(nèi)存空間。

通過常量池的使用String實現(xiàn)了多個引用指向同一個常量池中的對象,大大的節(jié)省了內(nèi)存空間的開銷。
JDK1.8之后,常量池存放于JVM運行時內(nèi)存中的堆內(nèi)存中。

String對象的創(chuàng)建

主要有以下兩種創(chuàng)建String對象的方式
1、String a="abcd";
使用這種創(chuàng)建方式時,若常量池中不存在"abcd"這個String對象,則會創(chuàng)建2個對象:在常量池中創(chuàng)建String類型的對象"abcd",常量池位于上圖所示的堆內(nèi)存中、在棧中創(chuàng)建引用a保存"abcd"的內(nèi)存地址,從而指向常量池中的"abcd"對象,棧既上圖所示的虛擬機棧。

若常量池中已存在"abcd"對象,則會直接返回這個對象,只在棧中創(chuàng)建一個引用a指向該對象。

在這里插入圖片描述

2、String a=new String("abcd");
使用這種創(chuàng)建方式時,若常量池中不存在值為"abcd"的String對象,則會先在常量池中創(chuàng)建一個值為“abcd”的String對象,然后將其復(fù)制一份到堆內(nèi)存中(常量池外,堆內(nèi)存中,地址不同),然后在棧中創(chuàng)建一個引用a保存"abcd"在堆中的地址,從而指向堆內(nèi)存中的該對象。共創(chuàng)建了三個對象
若常量池重已存在對象“abcd”,則省去在常量池中創(chuàng)建對象的這一步,共創(chuàng)建兩個對象

在這里插入圖片描述

三、String類型的拼接

通過concat方法拼接

String a="a";
String b="b";
System.out.println(a.concat(b));//通過a對象concat方法連接b對象,結(jié)果為"ab"

下面來看看concat方法的源碼

   public String concat(String str) {
        int olen = str.length();
        if (olen == 0) {
            return this;
        }
        if (coder() == str.coder()) {//coder來標識字符串的編碼格式是LATIN1還是UTF16,若兩個字符串的編碼格式相等,則不用進行編碼格式轉(zhuǎn)換
            byte[] val = this.value;
            byte[] oval = str.value;
            int len = val.length + oval.length;//拼接后字符串的長度
            byte[] buf = Arrays.copyOf(val, len);//創(chuàng)建一個新數(shù)組存放拼接后的字符串
            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源碼中容易得出,concat方法通過創(chuàng)建一個長度為兩字符串長度之和的byte數(shù)組來存放兩字符串,然后將兩個字符串依次放入數(shù)組中,實現(xiàn)了字符串的拼接。
至于為什么使用byte數(shù)組,上面講過,String類型底層使用byte數(shù)組存儲char數(shù)組,因此concat使用byte數(shù)組來存儲字符串,如果用其他類型的數(shù)組就要進行類型轉(zhuǎn)換。
注意:concat方法并不會對原對象進行改變,而是會返回一個新的String對象。

通過+號拼接

通過+號的拼接主要分為兩種情況:有字符串變量(既在棧中創(chuàng)建的引用)參與的拼接,無字符串變量參與,只有字符串常量(常量池中的String對象)參與的拼接。

有字符串變量(既在棧中創(chuàng)建的引用)參與的拼接:

在網(wǎng)上找了下有字符串變量參與+號拼接的實現(xiàn)原理,大部分說的都是:

運行時, 兩個字符串str1, str2的拼接首先會調(diào)用String.valueOf(obj),這個Obj為str1,而String.valueOf(Obj)中的實現(xiàn)是return obj ==null ? “null” : obj.toString()。
然后產(chǎn)生StringBuilder, 調(diào)用的StringBuilder(str1)構(gòu)造方法, 把StringBuilder初始化,長度為str1.length()+16,并且調(diào)用append(str1)!接下來調(diào)用StringBuilder.append(str2), 把第二個字符串拼接進去, 然后調(diào)用StringBuilder.toString返回結(jié)果。

下面我就得從底層中看看它們是如何實現(xiàn)拼接的。
打以下代碼:

public class Test{
   public static void main(String[] args){
        String str1 = "111111";
    String str2 = "222222";
    String str = str1 + str2;
    System.out.println(str);
   }
}

然后進入dos界面,在dos界面中進入文件所在文件夾,使用javac Test.java命令生成字節(jié)碼,再使用javap -verbose Test命令進行反編譯,可以看到以下結(jié)果。(JDK1.9及以后的版本才能看到如下結(jié)果,JDK1.8及以前的可參考這篇博文:Java String + 拼接字符串原理)

在這里插入圖片描述

容易看出以下兩行代碼 ,對應(yīng)的是String str = str1 + str2;語句

8: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3

動態(tài)指令invokedynamic指令會調(diào)用makeConcatWithConstants方法進行字符串的連接。
該方法位于java.lang.invoke.StringConcatFactory類中。
下面是源碼,容易看出這個方法里如果沒出問題,是直接調(diào)用doStringConcat方法

public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
                                               String name,
                                               MethodType concatType,
                                               String recipe,
                                               Object... constants) throws StringConcatException {
    if (DEBUG) {
        System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants));
    }

    return doStringConcat(lookup, name, concatType, false, recipe, constants);
}

下面是doStringConcat方法的部分源碼,多的就省略了。可以看到返回值中,mh調(diào)用asType方法適配得到MethodHandle對象,返回值的邏輯就是單純的返回一個結(jié)果,字符串拼接是在mh對象生成的時候進行的,也就是在generate方法中進行。

 private static CallSite doStringConcat(MethodHandles.Lookup lookup,
                                           String name,
                                           MethodType concatType,
                                           boolean generateRecipe,
                                           String recipe,
                                           Object... constants) throws StringConcatException {
......
MethodHandle mh;
if (CACHE_ENABLE) {
    Key key = new Key(className, mt, rec);
    mh = CACHE.get(key);
    if (mh == null) {
        mh = generate(lookup, className, mt, rec);
        CACHE.put(key, mh);
    }
} else {
    mh = generate(lookup, className, mt, rec);
}
return new ConstantCallSite(mh.asType(concatType));

下面是generate方法的源碼

private static MethodHandle generate(Lookup lookup, String className, MethodType mt, Recipe recipe) throws StringConcatException {
    try {
        switch (STRATEGY) {
            case BC_SB:
                return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.DEFAULT);
            case BC_SB_SIZED:
                return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED);
            case BC_SB_SIZED_EXACT:
                return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED_EXACT);
            case MH_SB_SIZED:
                return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED);
            case MH_SB_SIZED_EXACT:
                return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED_EXACT);
            case MH_INLINE_SIZED_EXACT:
                return MethodHandleInlineCopyStrategy.generate(mt, recipe);
            default:
                throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");
        }
    } catch (Error | StringConcatException e) {
        // Pass through any error or existing StringConcatException
        throw e;
    } catch (Throwable t) {
        throw new StringConcatException("Generator failed", t);
    }
}

generate方法通過不同的STRATEGY(策略)值來調(diào)用不同對象的generate方法。那么,接下來看看Strategy類型,對文檔中的英文進行了一些簡單的翻譯。

 private enum Strategy {
        /**
         * 字節(jié)碼生成器,調(diào)用{@link java.lang.StringBuilder}.
         */
        BC_SB,

        /**
         * 字節(jié)碼生成器,調(diào)用 {@link java.lang.StringBuilder};
         * 但要估計所需的存儲空間。
         */
        BC_SB_SIZED,

        /**
         * 字節(jié)碼生成器,調(diào)用 {@link java.lang.StringBuilder};
         * 但需要精確地計算所需的存儲空間。
         */
        BC_SB_SIZED_EXACT,

        /**
         *基于MethodHandle的生成器,最終調(diào)用 {@link java.lang.StringBuilder}.
         * 此策略還嘗試估計所需的存儲空間。
         */
        MH_SB_SIZED,

        /**
         * 基于MethodHandle的生成器,最終調(diào)用 {@link java.lang.StringBuilder}.
         * 此策略也需要準確地計算所需的存儲空間。
         */
        MH_SB_SIZED_EXACT,

        /**
         * 基于MethodHandle的生成器, 基于MethodHandle的生成器,從參數(shù)構(gòu)造自己的byte[]數(shù)組。它精確地計算所需的存儲空間。
         */
        MH_INLINE_SIZED_EXACT
    }

主要就是針對不同的情況,使用不同的策略值,共六種策略,從而能調(diào)用適用于當前情況的generate方法。上面五種策略的實現(xiàn)都是基于StringBuilder。
接下來以上面的BytecodeStringBuilderStrategy中的generate方法為例,來具體看一看是怎么實現(xiàn)字符串拼接的(套了一堆娃,終于到正題了)
首先,是調(diào)用String的ValueOf()方法

             if (mode.isExact()) {
/*在精確模式下,我們需要將所有參數(shù)轉(zhuǎn)換為字符串表示,因為這允許精確計算它們的字符串大小。我們不能在這里使用私有的原語方法,因此我們也需要轉(zhuǎn)換它們。

我們還記錄了轉(zhuǎn)換結(jié)果中保證為非null的參數(shù)。字符串.valueOf是否為我們檢查空。唯一極端的情況是字符串.valueOf(對象)返回null本身。

此外,如果發(fā)生任何轉(zhuǎn)換,則傳入?yún)?shù)中的插槽索引不等于最終的本地映射。唯一可能會中斷的情況是將2-slot long/double轉(zhuǎn)換為1-slot時。因此,我們可以跟蹤修改過的偏移,因為沒有轉(zhuǎn)換可以覆蓋即將到來的參數(shù)。
*/
                int off = 0;
                int modOff = 0;
                for (int c = 0; c < arr.length; c++) {
                    Class<?> cl = arr[c];
                    if (cl == String.class) {
                        if (off != modOff) {
                            mv.visitIntInsn(getLoadOpcode(cl), off);
                            mv.visitIntInsn(ASTORE, modOff);
                        }
                    } else {
                        mv.visitIntInsn(getLoadOpcode(cl), off);
                        mv.visitMethodInsn(
                                INVOKESTATIC,
                                "java/lang/String",
                                "valueOf",
                                getStringValueOfDesc(cl),
                                false
                        );
                        mv.visitIntInsn(ASTORE, modOff);
                        arr[c] = String.class;
                        guaranteedNonNull[c] = cl.isPrimitive();
                    }
                    off += getParameterSize(cl);
                    modOff += getParameterSize(String.class);
                }
            }

            if (mode.isSized()) {
            /*在調(diào)整大小模式(包括精確模式)下操作時,讓StringBuilder附加鏈看起來熟悉優(yōu)化StringConcat是有意義的。為此,我們需要盡早進行空檢查,而不是使附加鏈形狀更簡單。*/

                int off = 0;
                for (RecipeElement el : recipe.getElements()) {
                    switch (el.getTag()) {
                        case TAG_CONST:
                            // Guaranteed non-null, no null check required.
                            break;
                        case TAG_ARG:
                            // Null-checks are needed only for String arguments, and when a previous stage
                            // did not do implicit null-checks. If a String is null, we eagerly replace it
                            // with "null" constant. Note, we omit Objects here, because we don't call
                            // .length() on them down below.
                            int ac = el.getArgPos();
                            Class<?> cl = arr[ac];
                            if (cl == String.class && !guaranteedNonNull[ac]) {
                                Label l0 = new Label();
                                mv.visitIntInsn(ALOAD, off);
                                mv.visitJumpInsn(IFNONNULL, l0);
                                mv.visitLdcInsn("null");
                                mv.visitIntInsn(ASTORE, off);
                                mv.visitLabel(l0);
                            }
                            off += getParameterSize(cl);
                            break;
                        default:
                            throw new StringConcatException("Unhandled tag: " + el.getTag());
                    }
                }
            }

然后是生成StringBuilder對象并使用append方法依次將字符串加入

// 準備StringBuilder實例
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);

if (mode.isSized()) {
 /*大小模式要求我們遍歷參數(shù),并估計最終長度。
   在精確模式下,這將僅在字符串上操作。此代碼將在堆棧上累積最終長度。*/
    int len = 0;
    int off = 0;

    mv.visitInsn(ICONST_0);

    for (RecipeElement el : recipe.getElements()) {
        switch (el.getTag()) {
            case TAG_CONST:
                len += el.getValue().length();
                break;
            case TAG_ARG:
                /*
                   如果一個參數(shù)是String,那么我們可以對它調(diào)用.length()。大小/精確模式為我們轉(zhuǎn)換了參數(shù)。
                   如果一個參數(shù)是原始的,我們可以猜測它的字符串表示大小。
                */
                Class<?> cl = arr[el.getArgPos()];
                if (cl == String.class) {
                    mv.visitIntInsn(ALOAD, off);
                    mv.visitMethodInsn(
                            INVOKEVIRTUAL,
                            "java/lang/String",
                            "length",
                            "()",
                            false
                    );
                    mv.visitInsn(IADD);
                } else if (cl.isPrimitive()) {
                    len += estimateSize(cl);
                }
                off += getParameterSize(cl);
                break;
            default:
                throw new StringConcatException("Unhandled tag: " + el.getTag());
        }
    }

    // 常數(shù)具有非零長度,混合
    if (len > 0) {
        iconst(mv, len);
        mv.visitInsn(IADD);
    }

    mv.visitMethodInsn(
            INVOKESPECIAL,
            "java/lang/StringBuilder",
            "<init>",
            "(I)V",
            false
    );
} else {
    mv.visitMethodInsn(
            INVOKESPECIAL,
            "java/lang/StringBuilder",
            "<init>",
            "()V",
            false
    );
}

// 此時,堆棧上有一個空的StringBuilder,用.append調(diào)用填充它。
{
    int off = 0;
    for (RecipeElement el : recipe.getElements()) {
        String desc;
        switch (el.getTag()) {
            case TAG_CONST:
                mv.visitLdcInsn(el.getValue());
                desc = getSBAppendDesc(String.class);
                break;
            case TAG_ARG:
                Class<?> cl = arr[el.getArgPos()];
                mv.visitVarInsn(getLoadOpcode(cl), off);
                off += getParameterSize(cl);
                desc = getSBAppendDesc(cl);
                break;
            default:
                throw new StringConcatException("Unhandled tag: " + el.getTag());
        }

        mv.visitMethodInsn(//調(diào)用append方法
                INVOKEVIRTUAL,
                "java/lang/StringBuilder",
                "append",
                desc,
                false
        );
    }
}
            if (DEBUG && mode.isExact()) {
                /*
                    Exactness checks compare the final StringBuilder.capacity() with a resulting
                    String.length(). If these values disagree, that means StringBuilder had to perform
                    storage trimming, which defeats the purpose of exact strategies.
                 */

                /*
                   The logic for this check is as follows:

                     Stack before:     Op:
                      (SB)              dup, dup
                      (SB, SB, SB)      capacity()
                      (int, SB, SB)     swap
                      (SB, int, SB)     toString()
                      (S, int, SB)      length()
                      (int, int, SB)    if_icmpeq
                      (SB)              <end>

                   Note that it leaves the same StringBuilder on exit, like the one on enter.
                 */

                mv.visitInsn(DUP);
                mv.visitInsn(DUP);

                mv.visitMethodInsn(
                        INVOKEVIRTUAL,
                        "java/lang/StringBuilder",
                        "capacity",
                        "()I",
                        false
                );

                mv.visitInsn(SWAP);

                mv.visitMethodInsn(
                        INVOKEVIRTUAL,
                        "java/lang/StringBuilder",
                        "toString",
                        "()Ljava/lang/String;",
                        false
                );

                mv.visitMethodInsn(
                        INVOKEVIRTUAL,
                        "java/lang/String",
                        "length",
                        "()I",
                        false
                );

                Label l0 = new Label();
                mv.visitJumpInsn(IF_ICMPEQ, l0);

                mv.visitTypeInsn(NEW, "java/lang/AssertionError");
                mv.visitInsn(DUP);
                mv.visitLdcInsn("Failed exactness check");
                mv.visitMethodInsn(INVOKESPECIAL,
                        "java/lang/AssertionError",
                        "<init>",
                        "(Ljava/lang/Object;)V",
                        false);
                mv.visitInsn(ATHROW);

                mv.visitLabel(l0);
            }

下面是該方法中末尾的幾行代碼,主要就是調(diào)用StringBuilder的toString()方法并返回該方法得到的對象。

mv.visitMethodInsn(//調(diào)用StringBuilder的toString()方法
        INVOKEVIRTUAL,
        "java/lang/StringBuilder",
        "toString",
        "()Ljava/lang/String;",
        false
);

mv.visitInsn(ARETURN);

mv.visitMaxs(-1, -1);
mv.visitEnd();
cw.visitEnd();

byte[] classBytes = cw.toByteArray();
try {
    Class<?> hostClass = lookup.lookupClass();
    Class<?> innerClass = UNSAFE.defineAnonymousClass(hostClass, classBytes, null);
    UNSAFE.ensureClassInitialized(innerClass);
    dumpIfEnabled(innerClass.getName(), classBytes);
    return Lookup.IMPL_LOOKUP.findStatic(innerClass, METHOD_NAME, args);
} catch (Exception e) {
    dumpIfEnabled(className + "$$FAILED", classBytes);
    throw new StringConcatException("Exception while spinning the class", e);
}

所以,總結(jié)一下,有字符串變量參與拼接的過程:首先調(diào)用String的ValueOf方法,然后是生成一個StringBuilder對象并將用append方法將兩個字符串依次加入,然后返回StringBuilder的toString()方法。

只有字符串常量(常量池中的String對象)參與的拼接:例如:String a=“ab”+cd;這種拼接,在編譯時,編譯器會自動將a變量編譯為"abcd"
例如以下代碼:
public class Test2{
public static void main(String[] args){
String str = “12”+“34”;
System.out.println(str);
}
}
用上述的方法同樣查看反編譯代碼

在這里插入圖片描述

可以看到編譯器直接將str字符串編譯為了”1234“.

四、字符串的比較

equals方法

String類型的對象有個equals方法,用于比較兩個String對象的是否相等。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {//判斷編碼格式是否相等
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
                              //根據(jù)編碼格式調(diào)用不同的equals方法
        }
    }
    return false;
}

下面是StringLatin1對象(以Latin1為編碼格式的String對象)的equals方法

@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
}

然后是StringUTF16對象的equals方法

@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        int len = value.length >> 1;
        for (int i = 0; i < len; i++) {
            if (getChar(value, i) != getChar(other, i)) {
                return false;
            }
        }
        return true;
    }
    return false;
}

可以看出equals方法的實現(xiàn)邏輯就是通過for循環(huán)遍歷保存字符串的byte數(shù)組,一位一位地進行判斷。

"=="運算符

“==”運算符用于比較兩個對象的地址是否相等。用在字符串比較時,需要注意"abcd"與new String(“abcd”)所返回的地址值不相同,具體看上方String對象的創(chuàng)建。

注意:上面我們具體分析了有字符串變量參與的連接預(yù)算,最后的對象是由StringBuilder的toString()方法返回的,而toString()方法底層是返回的是new String()對象,存儲的地址是在堆中,而不是在常量池中。

@Override
@HotSpotIntrinsicCandidate
public String toString() {//StringBuilder對象的toString方法
    // Create a copy, don't share the array
    return isLatin1() ? StringLatin1.newString(value, 0, count)
                      : StringUTF16.newString(value, 0, count);
}

//StringLatin1對象的newString方法
public static String newString(byte[] val, int index, int len) {
    return new String(Arrays.copyOfRange(val, index, index + len),
                      LATIN1);
}

//StringUTF16的toString方法
public static String newString(byte[] val, int index, int len) {
    if (String.COMPACT_STRINGS) {
        byte[] buf = compress(val, index, len);
        if (buf != null) {
            return new String(buf, LATIN1);
        }
    }
    int last = index + len;
    return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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