Java雜記17—String全面解析

前言

基於字符串String在java中的地位,關於String的常識性知識就很少作介紹了,咱們先來看一段代碼java

public class Test {
    public static void main(String[] args) {
        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a==b);
        System.out.println(a.equals(b));
        System.out.println(a==c);
        System.out.println(a.equals(c));
    }
}
複製代碼

那麼上段代碼的結果是什麼呢?答案是:true true false true,有初學java的朋友確定會納悶,a==c爲何會是false呢?equals判斷的爲何都是true呢?git

根據這些問題,咱們就經過對String的解讀來一步一步的瞭解。github

爲何a==c的結果是false

明白這個問題須要對JVM的內存結構有必定的瞭解,說是瞭解也不須要太多,可以get到下圖的知識點就好了。面試

ps:本文中全部的圖示均是爲了方便理解,畫出來的大體樣子,若是想要了解的更加清楚,請自行研究虛擬機原理。數組

Stringpool內存演示

java語法設計的時候針對String,提供了兩種建立方式和一種特殊的存儲機制(String intern pool )。緩存

兩種建立字符串對象的方式:安全

  1. 字面值的方式賦值
  2. new關鍵字新建一個字符串對象

這兩種方法在性能和內存佔用方面存在這差別網絡

String Pool串池:是在內存堆中專門劃分一塊空間,用來保存全部String對象數據,當構造一個新字符串String對象時(經過字面量賦值的方法),Java編譯機制會優先在這個池子裏查找是否已經存在能知足須要的String對象,若是有的話就直接返回該對象的地址引用(沒有的話就正常的構造一個新對象,丟進去存起來),這樣下次再使用同一個String的時候,就能夠直接從串池中取,不須要再次建立對象,也就避免了不少沒必要要的空間開銷。多線程

根據以上的概念,咱們再來看前言中的代碼,當JVM執行到String a = "abc";的時候,會先看常量池裏有沒有字符串恰好是「abc」這個對象,若是沒有,在常量池裏建立初始化該對象,並把引用指向它,以下圖。性能

String1

當執行到String b = "abc";時,發現常量池已經有了abc這個值,因而再也不在常量池中建立這個對象,而是把引用直接指向了該對象,以下圖:

String2

繼續執行到 String c = new String("abc");這時候咱們加了一個new關鍵字,這個關鍵字呢就是告訴JVM,你直接在堆內存裏給我開闢一塊新的內存,以下圖所示:

String3

這時候咱們執行四個打印語句,咱們須要知道==比較的是地址,equals比較的是內容(String中的重寫過了),abc三個變量的內容徹底同樣,所以equals的結果都是true,ab是一個同一個對象,所以地址同樣,a和c很顯然不是同一個對象,那麼此時爲false也是很好理解的。

String相關源碼

在本文中只有String的部分源碼,畢竟String的源碼有3000多行,所有來寫進來不那麼現實,咱們挑一些比較有意思的代碼來作必定的分析說明。

屬性

咱們先來看一下String都有哪些成員變量,比較關鍵的屬性有兩個,以下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    char數組
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
複製代碼

從源碼中咱們可以看到,在String類中聲明瞭一個char[]數組,變量名value,聲明瞭一個int類型的變量hash(該String對象的哈希值的緩存)。也就是說java中的String類其實就是對char數組的封裝。

構造方法

接下來咱們經過一句代碼來了解一下字符串建立的過程,String c = new String("abc");咱們知道使用new關鍵字就會使用到構造方法,因此以下。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
複製代碼

構造方法中的代碼很是簡單,把傳進來的字符串的value值,也就是char數組賦值給當前對象,hash一樣處理,那麼問題來了WTF original?

在這裏須要注意的是java中的一個機制,在Java中,當值被雙引號引發來(如本示例中的"abc"),JVM會去先檢查看一看常量池裏有沒有abc這個對象,若是沒有,把abc初始化爲對象放入常量池,若是有,直接返回常量池內容。因此也就是說在沒有「abc」的基礎上,執行代碼會在串池中建立一個abc,也會在堆內存中再new出來一個。最終的結果以下圖:

String4

那麼這時候若是再有一個String c2 = new String("abc");呢?如圖

String5

關於這一點咱們經過IDEA的debug功能也可以看到,你會發現,c和c2其中的char數組的地址是相同的。足以說明在建立c和c2的時候使用的是同一個數組。

String6

equals方法

public boolean equals(Object anObject) {
     //若是兩個對象是同一個引用,那麼直接返回true
        if (this == anObject) {
            return true;
        }
     /* 1.判斷傳入的對象是否是String類型 2.判斷兩個對象的char數組長度是否一致 3.循環判斷char數組中的每個值是否相等 以上條件均知足纔會返回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;
    }

複製代碼

爲何String不可變?

串池須要

爲何說是串池須要呢?在開篇的時候咱們提到過,串池中的字符串會被多個變量引用,這樣的機制讓字符串對象獲得了複用,避免了不少沒必要要的內存消耗。

那麼你們試想一下,若是String對象自己容許二次修改的話,我有一個字符串「abc」同時被100個變量引用,其中一個引用修改了String對象,那麼將會影響到其餘99個引用該對象的變量,這樣會對其餘變量形成不可控的影響。

不可變性的優勢

安全性

字符串不可變安全性的考慮處於兩個方面,數據安全和線程安全。

數據安全,你們能夠回憶一下,咱們都在哪些地方大量的使用了字符串?網絡數據傳輸,文件IO等,也就是說當咱們在傳參的時候,使用不可變類不須要去考慮誰可能會修改其內部的值,若是使用可變類的話,可能須要每次記得從新拷貝出裏面的值,性能會有必定的損失。

線程安全,由於字符串是不可變的,因此是多線程安全的,同一個字符串實例能夠被多個線程共享,這樣便不用由於線程安全問題而使用同步。

性能效率

關於性能效率一方面是複用,另外一方面呢須要從hash值的緩存方向來講起了。

String的Hash值在不少的地方都會被使用到,若是保證了String的不可變性,也就可以保證Hash值始終也是不可變的,這樣就不須要在每次使用的時候從新計算hash值了。

String不可變性是如何實現的?

經過對屬性私有化,final修飾,同時沒有提供公開的get set方法以及其餘的可以修改屬性的方法,保證了在建立以後不會被從外部修改。

同時不能忘了,String也是被final修飾的,在以前的文章中咱們提到過,final修飾類的結果是String類沒有子類。

那麼String真的不能改變嗎?不是,經過反射咱們能夠,代碼以下:

String c = new String("abc");
System.out.println(c);
//獲取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");

//改變value屬性的訪問權限
valueFieldOfString.setAccessible(true);

//獲取s對象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(c);

//改變value所引用的數組中的第5個字符
value[1] = '_';
System.out.println(c);
複製代碼

執行的結果是

abc
a_c
複製代碼

也就是說咱們改變了字符串對象的值,有什麼意義呢?沒什麼意義,咱們歷來不會這麼作。

其餘問題

不是特別須要請不要使用new關鍵字建立字符串

從前文咱們知道使用new關鍵字建立String的時候,即使串池中存在相同String,仍然會再次在堆內存中建立對象,會浪費內存,另外一方面對象的建立相較於從串池中取效率也更低下。

String StringBuffer StringBuilder的區別

關於三者的區別,在面試題中常常的出現,String對象不可變,所以在進行任何內容上的修改時都會建立新的字符串對象,一旦修改操做太多就會形成大量的資源浪費。

StringBuffer和StringBuilder在進行字符串拼接的時候不會建立新的對象,而是在原對象上修改,不一樣之處在於StringBuffer線程安全,StringBuilder線程不安全。因此在進行字符串拼接的時候推薦使用StringBuffer或者StringBuilder。


我不能保證每個地方都是對的,可是能夠保證每一句話,每一行代碼都是通過推敲和斟酌的。但願每一篇文章背後都是本身追求純粹技術人生的態度。

永遠相信美好的事情即將發生。

相關文章
相關標籤/搜索