[Java]String之尋根問底,string問底

來源:互聯網
上載者:User

[Java]String之尋根問底,string問底

轉載請註明出處:http://blog.csdn.net/tang9140/article/details/43982887

      • 引言
      • 一 Immutable特性
      • 二 串連符號本質
      • 三 相等判斷兩種方式equals說明
        • 題1
        • 題2
        • 題3
      • 補充知識點
      • 附反射修改String對象代碼

引言

在java編程中,幾乎每天都會跟String打交道,因此,深入理解String及其用法十分有必要。下面分三方面來詳細說明下String相關的特點及用法

  • Immutable(不可變)特性
  • 串連符號+的本質
  • 相等判斷兩種方式(==/equals)說明
一、 Immutable特性

  Java設計人員為了方便大家對字串的各種操作,抽象出String類,該類封裝了對字串的尋找、拼接、替換、截取等一系列操作。查看java.lang.String的源碼,首先就能看到如下描述:

The String class represents character strings. All string literals in Java programs, such as “abc”, are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.

大意是:String類代表著字元序列。Java語言中所有的字串字面量,如“abc”,都實現為String類的執行個體
String對象都是常量,其值在建立之後就不能改變。字串緩衝區支援可變的字元序列。由於String對象的不可變性,他們可以被共用。

String的immutable體現在兩個方面

  • String類被final關鍵字修飾,意味著該類不能被繼承。由於String類不能有子類,保證了String類的靜態和執行個體方法不可能被繼承修改,保證了安全性。

  • String類內部的私人成員變數,如value(char[]),offset(int),count(int)都被final關鍵字修飾。當String對象建立後,offset跟count欄位的值不可改變(因為是基礎資料型別 (Elementary Data Type)int),value變數不能再指向其它的字元數組(因為其是參考型別變數)

看到這裡,可能有人會說:雖然value屬性不能指向其它的字元數組,但其指向的字元數組內容還是可以改變,如果能夠改變其內容,那不意味著String對象是可變的了。

上面的說法沒錯,但是String類本身沒有提供修改字元數組的方法,除非你用非常規手段(例如反射)去改變私人屬性的值(後面會附上代碼實現)。雖然從代碼上完全是可以改變建立後String對象的各屬性的值(即使屬性被private final修飾),但畢竟是採用反射這種非常規手段。按照正常使用方式,我們是不能改變String對象的值,所以還是認為String對象是不可變的。

注意:這裡說String不變性,是指String對象在建立後其值不能改變。對於參考型別的變數,是可以指向不同的String對象來改變其所代表的值。

String s = "abc"; s = "def";

參考型別變數s指向值為‘abc’的String對象,然後s又指向了值為‘def’的String對象。雖然s所代表的字串確實改變了,但是對於String對象abc和def並沒有改變,僅僅是s指向了不同的String對象而已。

關於String設計為immutable,至少有兩方面的好處:

  • 一是安全。String類被final修飾,意味著不可能有子類繼承String類而改變其原有行為。並且,產生的String對象是不變的,在多線程環境也是安全的。

  • 二是效率。String類被final修飾,隱含著該類的所有方法都是final的,編譯器可以進行一些最佳化。另外,由於String對象是不變的,可以被多處共用且不需要進行多線程之間的同步,提高了效率。

由於String對象的不變性,在用+號進行字串串連時,可能會造成效率低下,下面詳細說明下串連符號+的本質是什嗎?底層是如何進行字串拼接的?什麼情況下用+號進行字串串連效率較低?

二、 串連符號+本質

要瞭解+號的本質,先從java編譯說起。眾所周知,java代碼在運行前都需要先編譯成Class檔案(關於Class檔案的結構,由於篇幅有限,這裡不作詳細說明)。在Class檔案中,有一部分是叫屬性工作表集合,其中包括Code屬性,簡單說,Code屬性包含的就是方法體裡面的代碼經過編譯後對應的位元組碼指令。因此,我們可以直接查看Class檔案中的位元組碼指令來瞭解+的本質。範例程式碼如下

public class StringTest {    public static void main(String[] args) {        String s = "Hello";        s = s + " world!";    }}

由於我們不熟悉Class檔案結構,而且位元組碼非常不容易看懂,在這裡不直接查看編譯產生的StringTest.class檔案的內容,而是通過jad工具反編譯位元組碼查看結果。該工具為http://download.csdn.net/detail/tang9140/8426571(本人的csdn資源,打下廣告非喜勿噴)。在cmd下執行jad命令jad -o -a -sjava StringTest.class成功執行上述命令後,會發現StringTest.class檔案所在目錄下會多出源檔案StringTest.java,內容如下:

public class StringTest{    public StringTest()    {    //    0    0:aload_0             //    1    1:invokespecial   #8   <Method void Object()>    //    2    4:return              }    public static void main(String args[])    {        String s = "Hello";    //    0    0:ldc1            #16  <String "Hello">    //    1    2:astore_1                s = (new StringBuilder(String.valueOf(s))).append(" world!").toString();    //    2    3:new             #18  <Class StringBuilder>    //    3    6:dup                 //    4    7:aload_1             //    5    8:invokestatic    #20  <Method String String.valueOf(Object)>    //    6   11:invokespecial   #26  <Method void StringBuilder(String)>    //    7   14:ldc1            #29  <String " world!">    //    8   16:invokevirtual   #31  <Method StringBuilder StringBuilder.append(String)>    //    9   19:invokevirtual   #35  <Method String StringBuilder.toString()>    //   10   22:astore_1            //   11   23:return              }}

上述反編譯出的原始碼包含了注釋行,代表與原始碼相對應的位元組碼指令。很顯然,原始碼中並沒有字串串連符+,也就是說,+號在經過編譯後,已經被替換成StringBuilder的append方法調用(實現上在jdk1.5版本之前,+號在編譯器編譯後是替換為StringBuffer的append方法調用)。所謂的+號連接字串,本質上是通過new StringBuilder對象後調用其append方法進行字串拼接

java通過在編譯階段重載字串操作符+,在方便對字串的操作同時也帶來了一定的副作用,比如由於程式員不清楚+號的本質而編寫出效率低下的代碼,請看如下代碼:

    public String concat(){        String result = "";        for (int i = 0; i < 1000; i++) {            result += i;        }        return result;    }

在for迴圈體內,出現+號的地方,編譯後都會被替換為如下調用:
result = (new StringBuilder(String.valueOf(result))).append(i).toString();
顯然,每次迴圈都需要在構造StringBuilder對象時對result中的字元數組進行拷貝,而在調用toString方法時,又要拷貝StringBuilder中的字元數組來構建String對象。相當於每次for迴圈,要進行兩次對象建立及兩次字元數組拷貝,因而程式效率低下。更高效的代碼如下:

    public String concat(){        StringBuilder result = new StringBuilder();        for (int i = 0; i < 1000; i++) {            result.append(i);        }        return result.toString();    }

至此,我相信大家已經知道了+號的本質,以及如何避免低效的使用+號。下面我們就來深入瞭解下String對象相等判斷的兩種方式(經常出現在java面試題中)。

三、 相等判斷兩種方式(==/equals)說明
  • ==:當兩個運算元是基礎資料型別 (Elementary Data Type)時,比較值是否相等;當兩個運算元是參考型別時,比較是否指向同一個對象。
  • equals方法用來比較兩個對象的內容是否相等。

由於String是參考型別,當用==判斷時,比較的是兩個String變數是否指向同一個String對象;當用equals方法判斷時,才會比較兩個String對象的內容是否相等。在實際項目中進行字串比較時,基本是比較兩個String對象的內容是否相等,因此建議大家全部使用equals方法進行比較

用==進行字串比較,經常出現在面試題中,而不是項目代碼中,對於實際工作考核的意義不大,只是作為大家對String瞭解程度的一個考核。下面列舉一些面試題如下:

題1
        String a = "a1";           String b = "a" + 1;           System.out.println(a == b);

答案:true
說明:當兩個字面量進行串連時,實際上在java編譯器的編譯期,已經進行了字面量的拼接。也就是說編譯產生的Class檔案中並不存在String b = “a” + 1對應的位元組碼指令,已經被最佳化為String b = “a1”對應的位元組碼指令。我想這步最佳化大家應該很能理解,在編譯期間能夠確定結果並進行計算,就能有效減少Class檔案中的位元組碼指令,即減少了程式運行時需要執行的指令,提高了程式效率(大家可以用上述jad命令反編譯Class檔案進行驗證)。同理,對於基礎資料型別 (Elementary Data Type)字面量的算術操作,也在編譯期間進行了計算,例如int day = 24 * 60 * 60,編譯後被替換成代碼int day = 0x15180。由於在編譯期間已經進行了拼接,這樣局部變數a和b都指向了常量池中的’a1’對象,因此a == b輸出為true。

題2
        String hw = "Hello world!";        String h = "Hello";        h = h + " world!";        System.out.println(h == hw);

答案:false
說明:通過之前關於字串串連符+分析,我們知道h = h + " world!"經過編譯後會被替換成h = (new StringBuilder(String.valueOf(h))).append(" world!").toString()。查看下StringBuilder的toString方法,可以看到該方法實際就是return new String(value, 0, count),也就是h將指向java堆上的對象,而hw是指向常量池中的對象。雖然h和hw的內容相同,但由於指向不同的String對象,所以輸出為false。

題3
    public static final String h2 = "Hello";    public static final String h4 = getH();    private static String getH() {          return "Hello";      }    public static void main(String[] args) {        String hw = "Hello world!";        final String h1 = "Hello";        final String h3 = getH();        String hw1 = h1 + " world!";        String hw2 = h2 + " world!";        String hw3 = h3 + " world!";        String hw4 = h4 + " world!";        System.out.println(hw == hw1);        System.out.println(hw == hw2);        System.out.println(hw == hw3);        System.out.println(hw == hw4);    }

答案:true,true,false,false
說明:局部變數h1被final修飾,意味著h1是常量,同時h1被直接賦值為字串字面量”Hello”,這樣java編譯器在編譯期就能確定h1的值,從而將h1出現的地方直接替換成字面量”Hello”(類似c/c++用define定義的常量),再聯絡之前關於字面量會在編譯期直接拼接說明,因此代碼String hw1 = h1 + " world!"編譯後最佳化為String hw1 = "Hello world!",hw、hw1都指向了常量池中的String對象,輸出為true。同理h2是靜態常量,且是直接字面量賦值方式,h2出現的地方也會在編譯後直接被字面量”Hello”替換,最終,hw2也是指向常量池中的String對象,輸出為true。

局部變數h3也被final修飾,為常量,但是其是通過方法調用進行賦值的,編譯期無法確定其具體值(此時代碼都沒執行,是無法通過靜態分析得到方法的傳回值的,即使方法體中只是簡單的返回字串常量,如上述例子),再聯絡之前關於+的本質分析,因此String hw3 = h3 + " world!"編譯後為String hw3 = (new StringBuilder(String.valueOf(h3))).append(" world!").toString(),hw3將指向java堆上的String對象,hw == hw3輸出為false。同理,hw4也指向java堆上的String對象,hw == hw4輸出為false。

補充知識點

關於String類型變數的賦值,有兩種方式:

  • 其一、直接字面量賦值,即String str = “abc”;
  • 其二、new方式賦值,即String str = new String(“abc”);

方式一中,變數str直接指向字串常量池1中字面量為”abc”的String對象,即指向常量池中的String對象。
方式二中,變數str通過new建構函式String(String original)賦值,即指向java堆中的String對象。該建構函式接收String型別參數,而實參”abc”指向常量池中的String對象。

上面兩種給String類型變數賦值的方式,除了它們指向不同的String對象外,其它並沒有什麼區別。從程式效率的角度看,推薦使用方式一給String類型變數賦值,因為方式二多了一次java堆的String對象分配。

前面說過,字串字面量直接被看作String類的一個執行個體,實際是其在編譯期就存放在Class檔案的常量池中,當Class檔案被jvm載入時,其就進入到方法區的運行時常量池中。如果想在運行期間將新的常量加入常量池中,可調用String的intern()方法。
當調用 intern方法時,如果常量池已經包含一個等於此String對象的字串(用equals(Object)方法確定),則返回常池中的字串。否則,將此String 對象添加到池中,並返回此String對象的引用。

附反射修改String對象代碼:
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {        String name = "angel";        String name1 = "angel";        Field strField = String.class.getDeclaredField("value");        strField.setAccessible(true);        char[] data = (char[])strField.get(name);        data[4] = 'r';        System.out.println(name);        System.out.println(name1);        System.out.println(name == name1);        strField = String.class.getDeclaredField("count");        strField.setAccessible(true);        strField.setInt(name, 10);        int i = (Integer)strField.get(name);        System.out.println(i);        System.out.println(name.length());    }

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.