溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JDK源碼分析(1)之 String 相關

發(fā)布時間:2020-06-23 13:50:11 來源:網(wǎng)絡 閱讀:398 作者:沙漏半杯 欄目:編程語言

在此之前有無數(shù)次下定決心要把JDK的源碼大致看一遍,但是每次還沒點開就已被一個超鏈接或者其他事情吸引直接跳開了。直到最近突然意識到,因為對源碼的了解不深導致踩了許多莫名其妙的坑,所以再次下定決心要把常用的類全部看一遍。。。

一. 聲明和成員變量(不可變性)

public?final?class?String?implements?java.io.Serializable,?Comparable<String>,?CharSequence?{????private?final?char?value[];
}

String的大部分操作都是圍繞value這個字符數(shù)組定義的。同時String為了并發(fā)和一些安全性的考慮被設計成了不可變的類型,表明一旦被初始化完成后就是不可改變的。
在這里可能有人會疑惑,比如:

public?static?void?main(String[]?args)?{
??String?s1?=?"abc";
??s1?=?"bcd";
??System.out.println(s1);
}//?打印:?bcd

JDK源碼分析(1)之 String 相關

其中“abc”被初始化完成之后即為不可改變的,s1只是stack里面的引用變量,隨后s1將引用指向了“bcd”這個不可變字符,所以最后打印出來的是“bcd”。
而為了實現(xiàn)String的不可變性:

  1. String被聲明為final類型:表明String不能被繼承,即不能通過繼承的方式改變其中的value值。

  2. value被聲明為final類型:這個final并不能表示value這個字符數(shù)組的值不可變,只是確定了value這個字符數(shù)組在內(nèi)存中的位置是不可變的。

  3. 在隨后的方法介紹中可以看到,String并沒有提供修改value[]值得方法,并且所有的方法都不是直接返回value[],而是copy value[]中的值或者新建一個String對象。所以在通常意義上String是不可變的,但是卻不是絕對意義上的不可變,比如:

方法一:上面的第二點也說了,final?char[]?value,只定義了value所指向的內(nèi)存地址不變,其中的值是可以變的,所以我們可以通過反射直接修改value的值private?void?test03_reflection()?throws?Exception?{
??String?s?=?"abc";
??System.out.println("s?=?"?+?s);
??Field?valueFieldOfString?=?String.class.getDeclaredField("value");
??valueFieldOfString.setAccessible(true);??char[]?value?=?(char[])?valueFieldOfString.get(s);
??value[1]?=?'o';
??System.out.println("s?=?"?+?s);
}//?最終打印:?aoc方法二:可以直接使用unsafe類進行修改//?unsafe類不能直接new,構造方法里面有對調(diào)用者進行校驗,但是我們同樣可以通過反射獲取public?static?Unsafe?getUnsafe()?throws?Exception?{
??Field?theUnsafeInstance?=?Unsafe.class.getDeclaredField("theUnsafe");
??theUnsafeInstance.setAccessible(true);??return?(Unsafe)?theUnsafeInstance.get(Unsafe.class);
}//?通過unsafe類替換value[]public?void?test04_unsafe()?throws?Exception?{
??String?s?=?"abc";
??System.out.println("s?=?"?+?s);
??Field?f?=?s.getClass().getDeclaredField("value");
??f.setAccessible(true);??long?offset;????Unsafe?unsafe?=?getUnsafe();??char[]?nstr?=?new?char[]{'a',?'o',?'c'};
??offset?=?unsafe.objectFieldOffset(f);
??Object?o?=?unsafe.getObject(s,?offset);
??unsafe.compareAndSwapObject(s,?offset,?o,?nstr);
??System.out.println("s?=?"?+?s);
?}//?最終打?。?aoc//?通過unsafe類,定位value[]的內(nèi)存位置修改值public?void?test05_unsafe()?throws?Exception?{
??String?s?=?"abc";
??System.out.println("s?=?"?+?s);
??Field?f?=?s.getClass().getDeclaredField("value");
??f.setAccessible(true);????long?offset;
??Unsafe?unsafe?=?getUnsafe();
??offset?=?unsafe.arrayBaseOffset(char[].class)?+?2;??char[]?arr?=?(char[])?f.get(s);
??unsafe.putChar(arr,?offset,?'o');
??System.out.println("s?=?"?+?s);
}//?最終打?。?aoc

二、構造函數(shù)

public?String()public?String(String?original)public?String(char?value[])public?String(char?value[],?int?offset,?int?count)public?String(int[]?codePoints,?int?offset,?int?count)public?String(byte?ascii[],?int?hibyte,?int?offset,?int?count)public?String(byte?ascii[],?int?hibyte)public?String(byte?bytes[],?int?offset,?int?length,?String?charsetName)public?String(byte?bytes[],?int?offset,?int?length,?Charset?charset)public?String(byte?bytes[],?Charset?charset)public?String(byte?bytes[],?int?offset,?int?length)public?String(byte?bytes[])public?String(StringBuffer?buffer)public?String(StringBuilder?builder)String(char[]?value,?boolean?share)?{??//?assert?share?:?"unshared?not?supported";
??this.value?=?value;
}

以上15個構造方法除了最后一個,都是將傳入的參數(shù)copy到value中,并生成hash。這也是符合string的不可變原則。而最后一個則是用于string和包內(nèi)部產(chǎn)生的string對象,他沒有復制value數(shù)組,而是持有引用,共享value數(shù)組。這是為了加快中間過程string的產(chǎn)生,而最后得到的string都是持有自己獨立的value,所以string任然是不可變的。

三、常用方法

這個再次強調(diào),String方法的所有返回值,都是new的一個新對象,以保證不可變性

1.?String.equalsString.hashCode

public?boolean?equals(Object?anObject)?{??if?(this?==?anObject)?{????return?true;
??}??if?(anObject?instanceof?String)?{
????String?anotherString?=?(String)anObject;????int?n?=?value.length;????if?(n?==?anotherString.value.length)?{??????char?v1[]?=?value;??????char?v2[]?=?anotherString.value;??????int?i?=?0;??????while?(n--?!=?0)?{??????if?(v1[i]?!=?v2[i])??????return?false;
??????i++;
????}????return?true;
???}
?}?return?false;
}

equals首先比較是否指向同一個內(nèi)存地址,在比較是不是String類,再是長度最后內(nèi)容注意比較。

public?int?hashCode()?{??int?h?=?hash;??if?(h?==?0?&&?value.length?>?0)?{????char?val[]?=?value;????for?(int?i?=?0;?i?<?value.length;?i++)?{
??????h?=?31?*?h?+?val[i];
????}
??hash?=?h;
??}??return?h;
}

hashcode使用的數(shù)學公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

String 經(jīng)常會用作 hashMap 的 key,所以我們希望盡量減少 String 的 hash 沖突。(沖突是指 hash 值相同,從而導致 hashMap 的 node 鏈表過長,所以我們通常希望計算的 hash 值盡可能的分散,從而提高查詢效率),而這里選擇31是因為,如果乘數(shù)是偶數(shù),并且結果益處,那么信息就會是丟失(與2相乘相當于移位操作)31是一個奇素數(shù),并且31有個很好的特性(目前大多數(shù)虛擬機都支持的優(yōu)化):
31 * i == (i << 5) - i

2.?String.intern

/**
?*?Returns?a?canonical?representation?for?the?string?object.?*?<p>
?*?A?pool?of?strings,?initially?empty,?is?maintained?privately?by?the
?*?class?{@code?String}.
?*?<p>
?*?When?the?intern?method?is?invoked,?if?the?pool?already?contains?a
?*?string?equal?to?this?{@code?String}?object?as?determined?by
?*?the?{@link?#equals(Object)}?method,?then?the?string?from?the?pool?is
?*?returned.?Otherwise,?this?{@code?String}?object?is?added?to?the
?*?pool?and?a?reference?to?this?{@code?String}?object?is?returned.
?*?<p>
?*?It?follows?that?for?any?two?strings?{@code?s}?and?{@code?t},
?*?{@code?s.intern()?==?t.intern()}?is?{@code?true}
?*?if?and?only?if?{@code?s.equals(t)}?is?{@code?true}.
?*?<p>
?*?All?literal?strings?and?string-valued?constant?expressions?are
?*?interned.?String?literals?are?defined?in?section?3.10.5?of?the?*?<cite>The?Java&trade;?Language?Specification</cite>.
?*?@return?a?string?that?has?the?same?contents?as?this?string,?but?is?guaranteed?to?be?from?a?pool?of?unique?strings.
?*/
?public?native?String?intern();

可以看到intern這是一個native方法,主要用來查詢常量池的字符串,注釋中也寫了:

  • 如果常量池中中存在當前字符串,就會直接返回此字符串(此時當前字符串和常量池中的字符串一定不相同);

  • 如果常量池中沒有,就將當前字符串加入常量池后再返回(此時和常量池的實現(xiàn)相關)。

  • 這里有關常量池的設計,其實是享元模式。

將字符串加入常量池的兩種方式:

  • 編譯期生成的各種字面量和符號引用

將javap反編譯測試:
String?s1?=?"abc";?
public?static?void?main(String[]?args)?{
??String?s2?=?"123";
}

javap?-v?**.class:
Constant?pool:
...
??#24?=?Utf8???????????????abc
??#25?=?NameAndType????????#7:#8??????????//?s1:Ljava/lang/String;
??#26?=?Utf8???????????????123...
  • 運行期間通過intern方法將常量放入常量池

//?將javap反編譯測試:String?s1?=?"abc";?
public?static?void?main(String[]?args)?{
??String?s2?=?(new?String("1")?+?new?String("2")).intern();
}

javap?-v?**.class:
Constant?pool:
...
???#2?=?String?????????????#32????????????//?abc????-?"abc"?常量的引用對象
???#7?=?String?????????????#36????????????//?1??????-?
??#10?=?String?????????????#39????????????//?2??????-?
??#15?=?Utf8???????????????s1???????????????????????-?引用變量
??#28?=?Utf8???????????????s2???????????????????????-
??#32?=?Utf8???????????????abc??????????????????????-?變量
??#36?=?Utf8???????????????1????????????????????????-
??#39?=?Utf8???????????????2????????????????????????-
...

常量池中沒有"12"變量,但是在運行的時候,會動態(tài)添加進去,后面可以用==測試

JDk版本實驗,先簡單講一下常量池在不同JDK中的區(qū)別:

  • 在 JDK6 以及以前的版本中,字符串的常量池是放在堆的 Perm 區(qū)的,Perm 區(qū)是一個類靜態(tài)的區(qū)域,主要存儲一些加載類的信息,常量池,方法片段等內(nèi)容,默認大小只有4m,一旦常量池中大量使用 intern 是會直接產(chǎn)生java.lang.OutOfMemoryError: PermGen space錯誤的。

  • 在 JDK7 的版本中,字符串常量池已經(jīng)從 Perm 區(qū)移到正常的 Java Heap 區(qū)域了。為什么要移動,Perm 區(qū)域太小是一個主要原因。

  • 在 JDK8 則直接使用 Meta 區(qū)代替了 Perm 區(qū),并且可以動態(tài)調(diào)整 Mata 區(qū)的大小。

測試:

public?void?test06_intern()?{
??String?s1?=?new?String("1");
??s1.intern();
??String?s2?=?"1";
??System.out.println(s1?==?s2);

??String?s3?=?new?String("1")?+?new?String("1");
??s3.intern();
??String?s4?=?"11";
??System.out.println(s3?==?s4);
}
JDK6:???false?falseJDK7、8:false?true

分析:

  • 對于 JDK6:

JDK源碼分析(1)之 String 相關

如上圖所示 JDK6 中的常量池是放在 Perm 區(qū)中的,Perm 區(qū)和正常的 JAVA Heap 區(qū)域是完全分開的。上面說過如果是使用引號聲明的字符串都是會直接在字符串常量池中生成,而 new 出來的 String 對象是放在 JAVA Heap 區(qū)域。所以拿一個 JAVA Heap 區(qū)域的對象地址和字符串常量池的對象地址進行比較肯定是不相同的,即使調(diào)用String.intern方法也是沒有任何關系的。所以但會的都是false。

  • 對于 JDK7:

JDK源碼分析(1)之 String 相關

public?void?test06_intern()?{
??String?s1?=?new?String("1");??//生成了2個對象。常量池中的“1”?和?JAVA?Heap?中的字符串對象
??s1.intern();??????????????????//?生成一個?s2的引用指向常量池中的“1”對象。
??String?s2?=?"1";??????????????//?常量池中已經(jīng)有“1”這個對象了,所以直接返回。
??System.out.println(s1?==?s2);?//?最后比較s1、s2都指向同一個對象,所以是true

??/**
??*??生成了2最終個對象,是字符串常量池中的“1”?和?JAVA?Heap?中的?s3引用指向的對象。
??*??中間還有2個匿名的`new?String("1")`我們不去討論它們。此時s3引用對象內(nèi)容是"11",但此時常量池中是沒有?“11”對象的。
??*/
??String?s3?=?new?String("1")?+?new?String("1");?

??/**
??*??將?s3中的“11”字符串放入?String?常量池中,因為此時常量池中不存在“11”字符串,因此常規(guī)做法是跟?jdk6?圖中表示的那樣。
??*??在常量池中生成一個?"11"?的對象,關鍵點是?jdk7?中常量池不在?Perm?區(qū)域了,這塊做了調(diào)整。常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用。
??*??這份引用指向?s3?引用的對象。?也就是說引用地址是相同的。
??*/
??s3.intern();?

??String?s4?=?"11";????????????????//?"11"是顯示聲明的,因此會直接去常量池中創(chuàng)建,創(chuàng)建的時候發(fā)現(xiàn)已經(jīng)有這個對象了,此時也就是指向?s3?引用對象的一個引用。
??System.out.println(s3?==?s4);????//?所以最后比較是true。}
  • 這里如果我們修改一下s3.intern();這句的順序

JDK源碼分析(1)之 String 相關

??String?s3?=?new?String("1")?+?new?String("1");
??String?s4?=?"11";??????????????//?聲明?s4?的時候常量池中是不存在“11”對象的,執(zhí)行完畢后,“11“對象是?s4?聲明產(chǎn)生的新對象。
??s3.intern();???????????????????//?常量池中“11”對象已經(jīng)存在了,因此?s3?和?s4?的引用是不同的。
??System.out.println(s3?==?s4);??//?所以最終結果是false
  • 還有一些詳細的性能測試可以查看

http://java-performance.info/string-intern-in-java-6-7-8/
http://java-performance.info/string-intern-java-6-7-8-multithreaded-access/

3. 其他常用方法

String除此之外還提供了很多其他方法,主要都是操作CharSequence的,另外這里大致講一下UTF-16編碼。
Unicode(統(tǒng)一碼、萬國碼、單一碼)是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的。

  • 在存貯英文和一些常見字符的時候用到的區(qū)域叫做 BMP(Basic Multilingual Plane)基本多文本平面,這個區(qū)域占兩個字節(jié);

  • 當超出 BMP 范圍的時候,使用的是輔助平面(Supplementary Planes)中的碼位,在UTF-16中被編碼為一對16比特長的碼元(即32位,4字節(jié)),稱作代理對;Unicode標準現(xiàn)在稱高位代理為前導代理(lead surrogates),稱低位代理為后尾代理(trail surrogates)。

四、StringBuilder和StringBuffer

由于String對象是不可變的,所以進行字符串拼接的時候就可以使用StringBuilder?和StringBuffer兩個類。他們和String的關系如下:

JDK源碼分析(1)之 String 相關

從圖中可以看到StringBuilder?和StringBuffer也是實現(xiàn)的CharSequence接口,同時他們實現(xiàn)了Appendable接口,具有對字符串動態(tài)操作的能力。

從他們父類AbstractStringBuilder的源碼來看:

abstract?class?AbstractStringBuilder?implements?Appendable,?CharSequence?{??char[]?value;??int?count;??
??public?void?ensureCapacity(int?minimumCapacity)?{????if?(minimumCapacity?>?0)
????ensureCapacityInternal(minimumCapacity);
??}??public?AbstractStringBuilder?append(***)?{
???....
??}??public?AbstractStringBuilder?insert(***)?{
???....
??}
}
  • 他們同樣持有一個字符數(shù)組char[] value,并且每次在對字符串進行操作的時候需要首先對數(shù)組容量進行確定,不足的時候需要擴容。

  • 他們每個對字符串進行操作的方法,都會返回自身,所以我們可以使用鏈式編程的方式進行操作。另外現(xiàn)在還有一種通過泛型類定義鏈式操作的方式。

public?class?A<T?extends?A>?{??public?T?**(**)?{????return?t;
??}
}
  • StringBuilder?和StringBuffer的 API 都是互相兼容的,只是StringBuffer的每個方法都用的synchronized進行同步,所以是線程安全的。

1. 操作符重載

每當我們要就行字符串拼接的時候,自然會使用到+,同時++=也是 java 中僅有的兩個重載操作符。

public?void?test07_StringBuffer()?{
??String?s1?=?"a"?+?"b";
??s1?+=?"c";
??System.out.println(s1);
}

javap?-v?**.class
descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=2,?locals=2,?args_size=1
?????????0:?ldc???????????#37?????????????????//?String?ab
?????????2:?astore_1?????????3:?new???????????#16?????????????????//?class?java/lang/StringBuilder
?????????6:?dup?????????7:?invokespecial?#17?????????????????//?Method?java/lang/StringBuilder."<init>":()V
????????10:?aload_1????????11:?invokevirtual?#19?????????????????//?Method?java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
????????14:?ldc???????????#38?????????????????//?String?c
????????16:?invokevirtual?#19?????????????????//?Method?java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
????????19:?invokevirtual?#20?????????????????//?Method?java/lang/StringBuilder.toString:()Ljava/lang/String;
????????22:?astore_1????????23:?getstatic?????#5??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;
????????26:?aload_1????????27:?invokevirtual?#13?????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V
????????30:?return
??????LineNumberTable:
????????line?83:?0
????????line?84:?3
????????line?85:?23
????????line?86:?30
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature????????????0??????31?????0??this???LJDK/Test01_string;????????????3??????28?????1????s1???Ljava/lang/String;

從字節(jié)碼大致可以看出編譯器每次碰到”+”的時候,會new一個StringBuilder出來,接著調(diào)用append方法,在調(diào)用toString方法,生成新字符串,所以在字符串連接的時候,有很多“+”的時候,可以直接使用StringBuffer或者StringBuilder以提高性能;當然如果遇到類似String s = “a” + “b” + “c” + ...類似的連續(xù)加號的時候,JVM 會自動優(yōu)化為一個 StringBuilder。

五、switch對String的支持

String?str?=?"b";?
switch?(str)?{??case?"a":
????System.out.println(str);????break;??case?"b":
????System.out.println(str);????break;??default:
????System.out.println(str);????break;
}
System.out.println(str);

javap?-v?**:
code:
...??8:?invokevirtual?#8??????????????????//?Method?java/lang/String.hashCode:()I
?11:?lookupswitch??{?//?2
???????????????????97:?36
???????????????????98:?50
??????????????default:?61
?????}
...//?可以看到是首先拿到String的hashcode,在進行switch操作的

總結

本來想就這樣結束的,但是總覺得不寫點總結什么的就不完整。。。另外文章中還有很多細節(jié)沒有寫完,比如 String 在用于鎖對象時,需要使用 intern 來保證是同一把鎖。。。


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI