String物件的存盤、拼接和比較
- 一、String型別介紹
- 二、String型別的存盤
- 虛擬機運行時記憶體(JDK1.8以后)
- 常量池
- String物件的創建
- 三、String型別的拼接
- 通過concat方法拼接
- 通過+號拼接
- 四、字串的比較
- equals方法
- "=="運算子
( 以下原始碼都基于jdk11)
一、String型別介紹
String型別是參考資料型別,表示字串型別,String底層使用byte[]陣列來存盤char[]陣列,(JDK1.9及以后的版本,JDK1.9之前是使用char陣列保存,1.9為了節省空間,開始使用byte陣列保存)
@Stable
private final byte[] value;//定義byte陣列用于存盤建構式傳進的char陣列,最下方的代碼中有用到,
從上方的代碼中可以看出,String用于保存資料的陣列是private、final的,因此String型別是不可變的,
//String的建構式
public String(char value[]) { this(value, 0, value.length, null);//呼叫另一個建構式,代碼在下方 }
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型別的存盤
虛擬機運行時記憶體(JDK1.8以后)
JVM記憶體中與String型別存盤相關的結構主要有堆和虛擬機堆疊,
常量池
常量池在java用于保存在編譯期已確定的,已編譯的class檔案中的一份資料,它包括了關于類,方法,介面等中的常量,也包括字串常量,如String s = "java"這種申明方式;當然也可擴充,執行器產生的常量也會放入常量池,故認為常量池是JVM的一塊特殊的記憶體空間,
通過常量池的使用String實作了多個參考指向同一個常量池中的物件,大大的節省了記憶體空間的開銷,
JDK1.8之后,常量池存放于JVM運行時記憶體中的堆記憶體中,
String物件的創建
主要有以下兩種創建String物件的方式
1、String a="abcd";
使用這種創建方式時,若常量池中不存在"abcd"這個String物件,則會創建2個物件:在常量池中創建String型別的物件"abcd",常量池位于上圖所示的堆記憶體中、在堆疊中創建參考a保存"abcd"的記憶體地址,從而指向常量池中的"abcd"物件,堆疊既上圖所示的虛擬機堆疊,
若常量池中已存在"abcd"物件,則會直接回傳這個物件,只在堆疊中創建一個參考a指向該物件,

2、String a=new String("abcd");
使用這種創建方式時,若常量池中不存在值為"abcd"的String物件,則會先在常量池中創建一個值為“abcd”的String物件,然后將其復制一份到堆記憶體中(常量池外,堆記憶體中,地址不同),然后在堆疊中創建一個參考a保存"abcd"在堆中的地址,從而指向堆記憶體中的該物件,共創建了三個物件
若常量池重已存在物件“abcd”,則省去在常量池中創建物件的這一步,共創建兩個物件,

三、String型別的拼接
通過concat方法拼接
String a="a";
String b="b";
System.out.println(a.concat(b));//通過a物件concat方法連接b物件,結果為"ab"
下面來看看concat方法的原始碼
public String concat(String str) {
int olen = str.length();
if (olen == 0) {
return this;
}
if (coder() == str.coder()) {//coder來標識字串的編碼格式是LATIN1還是UTF16,若兩個字串的編碼格式相等,則不用進行編碼格式轉換
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原始碼中容易得出,concat方法通過創建一個長度為兩字串長度之和的byte陣列來存放兩字串,然后將兩個字串依次放入陣列中,實作了字串的拼接,
至于為什么使用byte陣列,上面講過,String型別底層使用byte陣列存盤char陣列,因此concat使用byte陣列來存盤字串,如果用其他型別的陣列就要進行型別轉換,
注意:concat方法并不會對原物件進行改變,而是會回傳一個新的String物件,
通過+號拼接
通過+號的拼接主要分為兩種情況:有字串變數(既在堆疊中創建的參考)參與的拼接,無字串變數參與,只有字串常量(常量池中的String物件)參與的拼接,
有字串變數(既在堆疊中創建的參考)參與的拼接:
在網上找了下有字串變數參與+號拼接的實作原理,大部分說的都是:
運行時, 兩個字串str1, str2的拼接首先會呼叫String.valueOf(obj),這個Obj為str1,而String.valueOf(Obj)中的實作是return obj ==null ? “null” : obj.toString(),
然后產生StringBuilder, 呼叫的StringBuilder(str1)構造方法, 把StringBuilder初始化,長度為str1.length()+16,并且呼叫append(str1)!接下來呼叫StringBuilder.append(str2), 把第二個字串拼接進去, 然后呼叫StringBuilder.toString回傳結果,
下面我就得從底層中看看它們是如何實作拼接的,
打以下代碼:
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命令生成位元組碼,再使用javap -verbose Test命令進行反編譯,可以看到以下結果,(JDK1.9及以后的版本才能看到如下結果,JDK1.8及以前的可參考這篇博文:Java String + 拼接字串原理)

容易看出以下兩行代碼 ,對應的是String str = str1 + str2;陳述句
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
動態指令invokedynamic指令會呼叫makeConcatWithConstants方法進行字串的連接,
該方法位于java.lang.invoke.StringConcatFactory類中,
下面是原始碼,容易看出這個方法里如果沒出問題,是直接呼叫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呼叫asType方法適配得到MethodHandle物件,回傳值的邏輯就是單純的回傳一個結果,字串拼接是在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(策略)值來呼叫不同物件的generate方法,那么,接下來看看Strategy型別,對檔案中的英文進行了一些簡單的翻譯,
private enum Strategy {
/**
* 位元組碼生成器,呼叫{@link java.lang.StringBuilder}.
*/
BC_SB,
/**
* 位元組碼生成器,呼叫 {@link java.lang.StringBuilder};
* 但要估計所需的存盤空間,
*/
BC_SB_SIZED,
/**
* 位元組碼生成器,呼叫 {@link java.lang.StringBuilder};
* 但需要精確地計算所需的存盤空間,
*/
BC_SB_SIZED_EXACT,
/**
*基于MethodHandle的生成器,最終呼叫 {@link java.lang.StringBuilder}.
* 此策略還嘗試估計所需的存盤空間,
*/
MH_SB_SIZED,
/**
* 基于MethodHandle的生成器,最終呼叫 {@link java.lang.StringBuilder}.
* 此策略也需要準確地計算所需的存盤空間,
*/
MH_SB_SIZED_EXACT,
/**
* 基于MethodHandle的生成器, 基于MethodHandle的生成器,從引數構造自己的byte[]陣列,它精確地計算所需的存盤空間,
*/
MH_INLINE_SIZED_EXACT
}
主要就是針對不同的情況,使用不同的策略值,共六種策略,從而能呼叫適用于當前情況的generate方法,上面五種策略的實作都是基于StringBuilder,
接下來以上面的BytecodeStringBuilderStrategy中的generate方法為例,來具體看一看是怎么實作字串拼接的(套了一堆娃,終于到正題了)
首先,是呼叫String的ValueOf()方法
if (mode.isExact()) {
/*在精確模式下,我們需要將所有引數轉換為字串表示,因為這允許精確計算它們的字串大小,我們不能在這里使用私有的原語方法,因此我們也需要轉換它們,
我們還記錄了轉換結果中保證為非null的引數,字串.valueOf是否為我們檢查空,唯一極端的情況是字串.valueOf(物件)回傳null本身,
此外,如果發生任何轉換,則傳入引數中的插槽索引不等于最終的本地映射,唯一可能會中斷的情況是將2-slot long/double轉換為1-slot時,因此,我們可以跟蹤修改過的偏移,因為沒有轉換可以覆寫即將到來的引數,
*/
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()) {
/*在調整大小模式(包括精確模式)下操作時,讓StringBuilder附加鏈看起來熟悉優化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()) {
/*大小模式要求我們遍歷引數,并估計最終長度,
在精確模式下,這將僅在字串上操作,此代碼將在堆疊上累積最終長度,*/
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:
/*
如果一個引數是String,那么我們可以對它呼叫.length(),大小/精確模式為我們轉換了引數,
如果一個引數是原始的,我們可以猜測它的字串表示大小,
*/
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());
}
}
// 常數具有非零長度,混合
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呼叫填充它,
{
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(//呼叫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);
}
下面是該方法中末尾的幾行代碼,主要就是呼叫StringBuilder的toString()方法并回傳該方法得到的物件,
mv.visitMethodInsn(//呼叫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);
}
所以,總結一下,有字串變數參與拼接的程序:首先呼叫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);
//根據編碼格式呼叫不同的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方法的實作邏輯就是通過for回圈遍歷保存字串的byte陣列,一位一位地進行判斷,
"=="運算子
“==”運算子用于比較兩個物件的地址是否相等,用在字串比較時,需要注意"abcd"與new String(“abcd”)所回傳的地址值不相同,具體看上方String物件的創建,
注意:上面我們具體分析了有字串變數參與的連接預算,最后的物件是由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);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/120475.html
標籤:其他
