这次让我们从字节码文件来重新认识String,文末有两个小小面试题,一起来试一试
我想對于String這個類,沒有誰對它陌生吧。可以說是無論在哪個項目中都是可以用到的。
那么反問一下你,你確定你對于String已經是真的了解了嗎?你是否清楚String的內存分配?你是否清楚字節碼文件中,它是如何的?你是否清楚創建String對象時,它牽扯到那幾個知識點勒?一起來討論吧。
”八小時內謀生活,八小時外謀發展“
共勉
封面地點:湖南永州市藍山縣舜河村
作者:用心笑*
注: 本文討論的String 是Jdk8中的。
一、String基本特性
1.1、基礎知識
String 的創建方式
- String str1 = “你好丫”; 采取字面量的定義方式,字符串會存儲在公共池中
- String str2 =new String(“hello”); 采取new 對象的方式,會存儲在堆中
String 聲明是final類型的,不可繼承。
String 實現了Serializable和Comparable接口:即字符串是支持序列化和比較大小的。
public final class String implements java.io.Serializable, Comparable<String>String在JDK 8 及之前,內部定義了``private final char[] value;來存儲字符串數據。但在jdk9 和11中已經改變為:private final byte[] value;`來存儲字符串數據。
我的電腦中暫時只有這幾個版本,之后有空了會全部驗證,大家也可以給出建議
1.2、大家想一想為什么會作出這樣的改變勒?
官網
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
譯為:
String類的當前實現將字符存儲在字符數組中,每個字符使用兩個字節(16位)。從許多不同的應用程序收集的數據表明,字符串是堆使用的主要組成部分,而且,大多數字符串對象只包含拉丁字符1。這些字符只需要一個字節的存儲空間,因此這些字符串對象的內部字符數組中有一半的空間沒有使用。 😚😯😲🙃😱
描述:
We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.
String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic string operations.
This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.
The prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.
我們建議將字符串類的內部表示從UTF-16 Char數組更改為字節數組以及編碼標志字段。 基于字符串的內容將新的字符串類存儲為ISO-8859-1 / LATIN-1(每個字符)或UTF-16(每個字符)(每種字符為兩個字節)的字符。 編碼標志將指示使用了哪個編碼。🐱?🏍
將更新字符串相關類,如AbstractStringBuilder,StringBuilder和StringBuffer以使用相同的表示,HotSpot VM的內部字符串操作也是如此。😼
這純粹是一個實現變化,沒有對現有公共接口的更改。 沒有計劃添加任何新的公共API或其他接口。🐱?🐉
到目前為止完成的原型化工作證實了預期的內存占用減少、 GC 活動的大量減少以及在某些極端情況下的次要性能退化。🐱?👓🐱?🚀
總結起來就是使用 byte[] 能夠比使用char[] 節省空間,減少GC活動
1.3、String不可變性
String:代表不可變的字符序列。簡稱:不可變性。
1、當對字符串重新賦值時,需要重寫指定內存區域賦值,不能使用原有的value進行賦值。
public static void main(String[] args) {String str1 = "hello";String str2 = "hello";// 判斷地址 這個時候肯定是 true 我們前文也講了 采取字面量的定義方式,字符串會存儲在公共池中 System.out.println(str1 == str2); } public static void main(String[] args) {String str1 = "hello";String str2 = "hello";str1="abc,hao";// 判斷地址, 它由true -->falseSystem.out.println(str1 == str2); }通過字節碼來看
2、當對現有的字符串進行連接操作時,也需要重新指定內存區域賦值,不能使用原有的value進行賦值。
public static void main(String[] args) {String str1 = "hello";String str2 = "hello";str1+="abc,hao";// 判斷地址, 它由true -->falseSystem.out.println(str1 == str2); }從字節碼文件可以看到,實際上所謂的連接字符,是通過StringBuilder.append()來執行的,之后再通過toString()方法返回回來。所以他們改變的也是原來的指向。
圖的指向和第一個圖差不多,為了省下篇幅, 就不畫了哈。
3、當調用string的replace()方法修改指定字符或字符串時,也需要重新指定內存區域賦值,不能使用原有的value進行賦值。
public static void main(String[] args) {String str1 = "hello";str1=str1.replace("h","q"); }通過字節碼文件都可以明顯看出來,對象是不同的。😃
4、通過字面量的方式(區別于new)給一個字符串賦值,此時的字符串值聲明在字符串常量池中。
小結:通過上面幾個小點,我想大家應該對這個是明白了吧。也能算證明了String的不可變性了吧。?😁
注意:字符串常量池是不會存儲相同內容的字符串的,相同的只會存儲一份,上面的代碼也體現出來了,目的是為了減少內存消耗
ldc 指令的意思,就是從常量池拿出一個 后面指令指向的東西。
二、String的內存分配
? 在Java語言中有8種基本數據類型和一種比較特殊的類型string。這些類型為了使它們在運行過程中速度更快、更節省內存,都提供了一種常量池的概念。
? 常量池就類似一個Java系統級別提供的緩存。8種基本數據類型的常量池都是系統協調的,string類型的常量池比較特殊。它的主要使用方法有兩種😶
-
直接使用雙引號聲明出來的String對象會直接存儲在常量池中。 如 String info=“我是寧在春”;
-
如果不是用雙引號聲明的string對象,可以使用string提供的intern()方法。
三、字符串拼接操作
-
常量與常量的拼接結果在常量池,原理是編譯期優化
-
常量池中不會存在相同內容的變量
-
只要其中有一個是變量,結果就在堆中。變量拼接的原理是StringBuilder
-
如果拼接的結果調用intern()方法,則主動將常量池中還沒有的字符串對象放入池中,并返回此對象地址
在這里為什么說是常量池優化勒?我們來看這個class文件。
我們寫的源代碼在編譯為.class文件時,“h”+“e”+“l”+“l”+“o” 就已經被編譯器認為等同于”hello“,所以str2 實際上就是引用了字符串常量池中的 “hello”。
下面來看下面這道題:
@Test public void test() {String s1 = "Java";String s2 = "Study";String s3 = "JavaStudy";String s4 = "Java" + "Study";String s5 = s1 + "Study";String s6 = "Java" + s2;String s7 = s1 + s2;// 請問 下面哪些是 true 哪些是false呢??System.out.println(s3 == s4);System.out.println(s3 == s5);System.out.println(s3 == s6);System.out.println(s3 == s7);System.out.println(s5 == s6);System.out.println(s5 == s7);// 那么上面你都做對了 那下面這個勒?String s8 = s6.intern();System.out.println(s3 == s8); }答案是:
true,false,false,false,false,false,true
為什么勒?我們照常還是先來看看class文件。
s3== s4 很容易理解,他們編譯完就是一樣的。
為什么s3!=s5呢? 解釋完這個后面都差不多。
s5=s1+"Stduy"; 但是這一行代碼,實際中間經過很多過程的。
s1+"Study" 實際是通過StringBuilder.append()` 來添加的,最后再通過toString() 方法,再來返回一個對象的,深入進去StringBuilder.toString() 方法實際上就是 new String();
所以他們指向的位置是不同的。
String s8 = s6.intern();
System.out.println(s3 == s8); // 為true
源碼上的注釋講的特別清楚
即:當調用 intern 方法時,如果池中已經包含一個等于該String對象的字符串equals(Object)由equals(Object)方法確定equals(Object) ,則返回池中的字符串。 否則,將此String對象添加到池中并返回對此String對象的引用。
四、intern()的使用
-
intern是一個native方法,調用的是底層C的方法
-
字符串池最初是空的,由String類私有地維護。在調用intern方法時,如果池中已經包含了由equals(object)方法確定的與該字符串對象相等的字符串,則返回池中的字符串。否則,該字符串對象將被添加到池中,并返回對該字符串對象的引用。
-
如果不是用雙引號聲明的string對象,可以使用string提供的intern方法:intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。
如:
@Test public void test2() {String str1 = "i miss you";String str2 = new String("i miss you").intern();System.out.println(str1 == str2); // 結果為 true }通俗點講,Interned string就是確保字符串在內存里只有一份拷貝,這樣可以節約內存空間,加快字符串操作任務的執行速度。注意,這個值會被存放在字符串內部池(String Intern Pool😁
五、小小的幾個面試題
也是當時好奇 (jdk 8 為背景講的哈,之前的jdk 可能產生不一樣的結果😊)。
1、 new String(“ab”)會創建幾個對象?
1個還是2個呢? 真的是這樣嗎?你確定嗎?
public static void main(String[] args) {String ab = new String("ab"); }代碼非常簡單,從代碼也看不出很多,我們打開字節碼文件查看哈。
解析過程:
結論:所以答案是兩個對象。
2、new String(“a”) + new String(“b”) 會創建幾個對象
看這個你覺得是幾個呢???三個? 四個還是五個?還是更多勒?或者是更少勒?
public static void main(String[] args) {String ab = new String("a") + new String("b"); }依舊還是從字節碼文件來看:
如果這樣從字節碼文件上看,確實只能看到五個,但是在上文中,我寫了 StringBuilder.toString()方法,它的底層就是調用 new String() ;
所以我們實際上是創建了 6個對象。
- 對象1:new StringBuilder()
- 對象2:new String(“a”)
- 對象3:常量池的 a
- 對象4:new String(“b”)
- 對象5:常量池的 b
- 對象6:toString中會創建一個 new String(“ab”)
- 調用toString方法,不會在常量池中生成ab
3、那么 new String(“a”+“b”)會創建幾個對象勒???
在評論中給出答案哦。
六、自言自語
摸魚的一天🧐,Java 也太卷了,學起來是真的累,努力的人特別努力,不努力的人瑟瑟發抖啊😔。
還是覺得躺平舒服🛌,一起來吧。
總結
以上是生活随笔為你收集整理的这次让我们从字节码文件来重新认识String,文末有两个小小面试题,一起来试一试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Docker 常用命令整合!!!带你一起
- 下一篇: JVM 垃圾回收算法 -可达性分析算法!