Java內存管理-探索Java中字符串String(十二)

作一個積極的人html

編碼、改bug、提高本身java

我有一個樂園,面向編程,春暖花開!面試

推薦閱讀編程

第一季

0、Java的線程安全、單例模式、JVM內存結構等知識梳理設計模式

一、Java內存管理-程序運行過程(一)數組

二、Java內存管理-初始JVM和JVM啓動流程(二)安全

三、Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)數據結構

四、Java內存管理-掌握虛擬機類加載機制(四)ide

五、Java內存管理-掌握虛擬機類加載器(五)函數

六、Java內存管理-類加載器的核心源碼和設計模式(六)

七、Java內存管理-掌握自定義類加載器的實現(七)
第一季總結:由淺入深JAVA內存管理 Core Story

第二季

八、Java內存管理-愚人節new一個對象送給你(八)

【福利】JVM系列學習資源無套路贈送

九、Java內存管理-」一文掌握虛擬機建立對象的祕密」(九)

十、Java內存管理-你真的理解Java中的數據類型嗎(十)

十一、Java內存管理-Stackoverflow問答-Java是傳值仍是傳引用?(十一)

十二、Java內存管理-探索Java中字符串String(十二)

實戰

一文學會Java死鎖和CPU 100% 問題的排查技巧

分享一位老師的人工智能教程。零基礎!通俗易懂!風趣幽默!你們能夠看看是否對本身有幫助,點擊這裏查看【人工智能教程】。接下來進入正文。

1、初識String類

首先JDK API的介紹:

public final class String extends Object 
implements Serializable, Comparable<String>, CharSequence複製代碼

String 類表明字符串。Java 程序中的全部字符串字面值(如 "abc" )都做爲此類的實例實現。

字符串是常量;它們的值在建立以後不能更改。字符串緩衝區支持可變的字符串。由於 String 對象是不可變的,因此能夠共享。例如:

String str = "abc";複製代碼

等效於:

char data[] = {'a', 'b', 'c'};
 String str = new String(data);複製代碼

從JDK API中能夠看出:

  • String類是final類,那麼String類是不能被繼承的。
  • 實現了Cloneable接口,即覆蓋了函數clone(),能被克隆。
  • 實現了Serializable接口,支持序列化,也就意味了String可以經過序列化傳輸。

2、字符串的不可變性

從上面的介紹中發現:字符串是常量,它們的值在建立以後不能更改。爲何會這樣呢?要了解其緣由,簡單看一下String類的源碼實現。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        // 從新建立一個新的字符串
        return new String(buf, true);
    }
    
    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 從新建立一個新的字符串
                return new String(buf, true);
            }
        }
        return this;
    }
}複製代碼

從上面源碼中能夠看出String類實際上是經過char數組來保存字符串的,注意修飾這個char前面的關鍵字 final。final修飾的字段建立之後就不可改變。

注意 private final char value[]; 這裏雖然value是不可變,也就是說value這個引用地址不可變。可是由於其是數組類型,根據以前學過的內容,value這個引用地址實際上是在棧上分配 ,而其對應的數據結構是在堆上分配保存。那也就是說棧裏的這個value的引用地址不可變。沒有說堆裏array自己數據不可變。看下面這個例子,

final int[] value={1,2,3}
int[] another={4,5,6};
value=another;    //編譯器報錯,final不可變複製代碼

value用final修飾,編譯器不容許我把value指向棧區另外一個地址。但若是直接對數組元素進行修改,分分鐘搞定。

final int[] value={1,2,3};
value[2]=100;  //這時候數組裏已是{1,2,100}複製代碼

因此String是不可變的關鍵都在底層的實現,而不是一個final。

也能夠經過上面的concat(String str)replace(char oldChar, char newChar)方法簡單進行了解,全部的操做都不是在原有的value[]數組中進行操做的,而是從新生成了一個新數組buf[]。也就是說進行這些操做後,最原始的字符串並無被改變。

若是面試有問到的話要修改String中value[] 數組的內容,要怎麼作,那麼能夠經過反射進行修改!實際使用中沒有人會去這麼作。

3、字符串常量池和 intern 方法

Java中有字符串常量池,用來存儲字符串字面量! 因爲JDK版本的不一樣,常量池的位置也不一樣,根據網上的一些資料:

jdk1.6及如下版本字符串常量池是在永久區中。

jdk1.七、1.8下字符串常量池已經轉移到堆中了。(JDK1.8已經沒有去掉永久區)

由於字符串常量池發生了變化,在String內對intern()進行了一些修改:

jDK1.6版本中執行intern()方法,首先判斷字符串常量池中是否存在該字面量,若是不存在則拷貝一份字面量放入常量池,最後返回字面量的惟一引用。若是發現字符串常量池中已經存在,則直接返回字面量的惟一引用。

jdk1.7之後執行intern()方法,若是字符串常量池中不存在該字面量,則不會再拷貝一份字面量,而是拷貝字面量對應堆中一個引用,而後返回這個引用。

String 類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
  • 若是不是用雙引號聲明的 String 對象,可使用 String 提供的 intern 方法。不一樣版本的intern 表現看上面介紹。

說明:直接使用new String() 建立出的String對象會直接存儲在堆上

經過一個栗子,看一下上面說的內容:

String str1 = "aflyun";
String str2 = new String("aflyun");
System.out.println(str1 == str2);

String str3 = str2.intern();

System.out.println(str1 ==str3);
複製代碼

使用JDK1.8版本運行輸出的結果: falsetrue

先上面示例的示意圖:

str1直接建立在字符串常量池中,str2使用new關鍵字,對象建立在堆上。因此str1 == str2 爲false。

str3str2.intern(),根據上面的介紹,在jdk1.8首先在常量池中判斷字符串aflyun是否存在,若是存在的話,直接返回常量池中字符串的引用,也就是str1的引用。因此str1 ==str3爲true。

若是你理解了上面的內容,能夠在看一下下面的栗子,運行結果是在JDK1.8環境:

栗子1:

String str1 = "hello";
String str2 = "world";
//常量池中的對象
String str3 = "hello" + "world";
//在堆上建立的新的對象
String str4 = str1 + str2; 
//常量池中的對象
String str5 = "helloworld";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false複製代碼

栗子2:

//同時會生成堆中的對象以及常量池中hello的對象,此時str1是指向堆中的對象的
String str1 = new String("hello");
// 常量池中的已經存在hello
str1.intern();
//常量池中的對象,此時str2是指向常量池中的對象的
String str2 = "hello";
System.out.println(str1 == str2); // false

// 此時生成了四個對象 常量池中的"world" + 2個堆中的"world" +s3指向的堆中的對象(注此時常量池不會生成"worldworld")
String str3 = new String("world") + new String("world");
//常量池沒有「worldworld」,會直接將str3的地址存儲在常量池內
str3.intern(); 
// 建立str4的時候,發現字符串常量池已經存在一個指向堆中該字面量的引用,則返回這個引用,而這個引用就是str3
String str4 = "worldworld"; 
System.out.println(str3 == str4); //true複製代碼

栗子3:涉及到final關鍵字,能夠試着理解一下

// str1指的是字符串常量池中的 java6
String str1 = "java6";
// str2是 final 修飾的,編譯時候就已經肯定了它的肯定值,編譯期常量
final String str2 = "java";
// str3是指向常量池中 java
String str3 = "java";

//str2編譯的時候已經知道是常量,"6"也是常量,因此計算str4的時候,直接至關於使用 str2 的原始值(java)來進行計算.
// 則str4 生成的也是一個常量,。str1和str4都對應 常量池中只生成惟一的一個 java6 字符串。
String str4 = str2 + "6";

// 計算 str5 的時候,str3不是final修飾,不會提早知道 str3的值是什麼,只有在運行經過連接來訪問,這種計算會在堆上生成 java6
String str5 = str3 + "6";
System.out.println((str1 == str4));//true
System.out.println((str1 == str5));//false複製代碼

總結

  1. 直接定義字符串變量的時候賦值,若是表達式右邊只有字符串常量,那麼就是把變量存放在常量池裏。
  2. new出來的字符串是存放在堆裏面。
  3. 對字符串進行拼接操做,也就是作"+"運算的時候,分2中狀況:
  • 表達式右邊是純字符串常量,那麼存放在字符串常量池裏面。
  • 表達式右邊若是存在字符串引用,也就是字符串對象的句柄,那麼就存放在堆裏面。:

4、面試題

一、 String s1 = new String("hello");這句話建立了幾個字符串對象?

狀況1:

String s1 = new String("hello");// 堆內存的地址值
String s2 = "hello";
System.out.println(s1 == s2);// 輸出false,由於一個是堆內存,一個是常量池的內存,故二者是不一樣的。
System.out.println(s1.equals(s2));// 輸出true複製代碼

若是上面代碼的話,這種狀況總共建立2個字符串對象。常量池中沒有字符串"hello" 的話,一個是new String 建立的一個新的對象,一個是常量「hello」對象的內容建立出的一個新的String對象。

狀況2:

String s2 = "hello";
String s1 = new String("hello");複製代碼

String s1 = new String("hello"); 此時就建立一個對象,而常量「hello」則是從字符串常量池中取出來的。

二、有時候在面試的時候會遇到這樣的問題:都說String是不可變的,爲何我能夠這樣作呢,String a = "1";a = "2";

public class StringTest {

    public static void main(String[] args) {
        String s = "aflyun";
        System.out.println("s1.hashCode() = " + s.hashCode() + "--" + s);
        s = "hello aflyun";
        System.out.println("s2.hashCode() = " + s.hashCode() + "--" + s);
        //運行後輸出的結果不一樣,兩個值的hascode也不一致,
        //說明設置的值在內存中存儲在不一樣的位置,也就是建立了新的對象
    }
}
---
s1.hashCode() = -1420403061--aflyun
s2.hashCode() = -855605863--hello aflyun複製代碼

【首先建立一個String對象s,而後讓s的值爲「aflyun」, 而後又讓s的值爲「hello aflyun」。 從打印結果能夠看出,s的值確實改變了。那麼怎麼還說String對象是不可變的呢?】

其實這裏存在一個誤區: s只是一個String對象的引用,並非對象自己。對象在內存中是一塊內存區,成員變量越多,這塊內存區佔的空間越大。引用只是一個4字節的數據,裏面存放了它所指向的對象的地址,經過這個地址能夠訪問對象。

也就是說,s只是一個引用,它指向了一個具體的對象,當s=「hello aflyun」; 這句代碼執行過以後,又建立了一個新的對象「「hello aflyun」, 而引用s從新指向了這個新的對象,原來的對象「aflyun」還在內存中存在,並無改變。內存結構以下圖所示:

相似的一張圖:

總結一下:「String對象一旦被建立就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何改變操做都會生成新的對象」

參考資料

java的線程安全、單例模式、JVM內存結構等知識學習和整理

Java-String.intern的深刻研究

深刻理解Java中的String

備註: 因爲本人能力有限,文中如有錯誤之處,歡迎指正。

謝謝你的閱讀,若是您以爲這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你天天開心愉快!

Java編程技術樂園:一個分享編程知識的公衆號。跟着老司機一塊兒學習乾貨技術知識,天天進步一點點,讓小的積累,帶來大的改變!

掃描關注,後臺回覆【資源】,獲取珍藏乾貨! 99.9%的夥伴都很喜歡

image.png | center| 747x519

© 天天都在變得更好的阿飛雲
相關文章
相關標籤/搜索