1. 程式人生 > >String中==和equals分別比較的是什麼?

String中==和equals分別比較的是什麼?

首先先了解一下java中的==和equals分別比較的是什麼?

對於基本的資料型別來說:==比較的是兩個基本型別的值。
對於複合資料型別來說:==和equals比較的都是物件在記憶體中存放地址(確切的 說是堆記憶體地址)。
因此對於String,Integer,Date等覆蓋了equals方法的型別,==比較的是存放的記憶體地址。而equals比較的是具體覆蓋後的程式碼。

String中equals比較的兩個字串內容是否相等。

public boolean equals(Object anObject){ 
    if(this==anObject){
        return
true; } if(anObject instanceof String){ //如果資料型別為String,則比較char陣列內的每個字串的值是否== 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有兩種賦值的方式:

1、像普通物件使用new的方法:String str= new String(“abcd”);
2、像基本資料型別的方法: String str =”abcd”;

那麼為什麼String 可以不用new的方式來賦值呢?

這是因為編譯器起到了作用:當你對“abcd”沒有使用new的時候,編譯器會預設呼叫構造器new String(char value[])。

因此顯而易見隱式情況下,呼叫了String的建構函式。
那隱式和顯示賦值的方法有何不同?
隱式:

 String str =“ok”;   

利用了字串快取池,也就說如果快取池中已經存在了相同的字串,就不會產生新的物件,
而是直接返回了快取池中的字串物件的引用;
如:

       String a="ok";//新建一個String物件
       String b ="ok";//從快取池中找
       System.out.println(a == b);//true
       String c = new String("ok");//新建一個String物件
       String d = new String("ok");//新建一個String物件
       System.out.println(c == d);//false

這時比較的話可以使用equals就相等了。
下面拓展一個方法:intern()
1.為什麼要介紹intern()方法
intern()方法設計的初衷,就是重用String物件,以節省記憶體消耗。這麼說可能有點抽象,那麼就用例子來證明。

static final int MAX = 100000;  
static final String[] arr = new String[MAX];  

public static void main(String[] args) throws Exception {  
    //為長度為10的Integer陣列隨機賦值  
    Integer[] sample = new Integer[10];  
    Random random = new Random(1000);  
    for (int i = 0; i < sample.length; i++) {  
        sample[i] = random.nextInt();  
    }  
    //記錄程式開始時間  
    long t = System.currentTimeMillis();  
    //使用/不使用intern方法為10萬個String賦值,值來自於Integer陣列的10個數  
        for (int i = 0; i < MAX; i++) {  
            arr[i] = new String(String.valueOf(sample[i % sample.length]));  
            //arr[i] = new String(String.valueOf(sample[i % sample.length])).intern(); 
        }  
        System.out.println((System.currentTimeMillis() - t) + "ms");  
        System.gc();  
}  

這個例子也比較簡單,就是為了證明使用intern()比不使用intern()消耗的記憶體更少。
先定義一個長度為10的Integer陣列,並隨機為其賦值,在通過for迴圈為長度為10萬的String物件依次賦值,這些值都來自於Integer陣列。兩種情況分別執行,可通過Window
—> Preferences –> Java –> Installed JREs設定JVM啟動引數為-agentlib:hprof=heap=dump,format=b,將程式執行完後的hprof置於工程目錄下。再通過MAT外掛檢視該hprof檔案。

兩次實驗結果如下:

從執行結果來看,不使用intern()的情況下,程式生成了101762個String物件,而使用了intern()方法時,程式僅生成了1772個String物件。自然也證明了intern()節省記憶體的結論。
細心的同學會發現使用了intern()方法後程序執行時間有所增加。這是因為程式中每次都是用了new String後又進行intern()操作的耗時時間,但是不使用intern()佔用記憶體空間導致GC的時間是要遠遠大於這點時間的。

2.深入認識intern()方法
JDK1.7後,常量池被放入到堆空間中,這導致intern()函式的功能不同,具體怎麼個不同法,且看看下面程式碼,這個例子是網上流傳較廣的一個例子,分析圖也是直接貼上過來的,這裡我會用自己的理解去解釋這個例子:

String s = new String("1");  
s.intern();  
String s2 = "1";  
System.out.println(s == s2);  

String s3 = new String("1") + new String("1");  
s3.intern();  
String s4 = "11";  
System.out.println(s3 == s4);  
輸出結果為:

JDK1.6以及以下:false false  
JDK1.7以及以上:false true  

再分別調整上面程式碼2.3行、7.8行的順序:

String s = new String("1");  
String s2 = "1";  
s.intern();  
System.out.println(s == s2);  

String s3 = new String("1") + new String("1");  
String s4 = "11";  
s3.intern();  
System.out.println(s3 == s4);  
輸出結果為:

JDK1.6以及以下:false false  
JDK1.7以及以上:false false  

下面依據上面程式碼對intern()方法進行分析:

2.1 JDK1.6

在JDK1.6中所有的輸出結果都是 false,因為JDK1.6以及以前版本中,
常量池是放在 Perm 區(屬於方法區)中的,熟悉JVM的話應該知道這是和堆區完全分開的。
使用引號宣告的字串都是會直接在字串常量池中生成的,而 new 出來的 String 物件是放在堆空間中的。所以兩者的記憶體地址肯定是不相同的,即使呼叫了intern()方法也是不影響的。
intern()方法在JDK1.6中的作用是:
比如String s = new String(“SEU_Calvin”),再呼叫s.intern(),此時返回值還是字串”SEU_Calvin”,表面上看起來好像這個方法沒什麼用處。
但實際上,在JDK1.6中它做了個小動作:檢查字串池裡是否存在”SEU_Calvin”這麼一個字串,如果存在,就返回池裡的字串;如果不存在,該方法會把”SEU_Calvin”新增到字串池中,然後再返回它的引用。然而在JDK1.7中卻不是這樣的。

2.2 JDK1.7
針對JDK1.7以及以上的版本,我們將上面兩段程式碼分開討論。先看第一段程式碼的情況:

再把第一段程式碼貼一下便於檢視:

String s = new String("1");  
s.intern();  
String s2 = "1";  
System.out.println(s == s2);  
String s3 = new String("1") + new String("1");  
s3.intern();  
String s4 = "11";  
System.out.println(s3 == s4);  

String s = newString(“1”),生成了常量池中的“1” 和堆空間中的字串物件。
s.intern(),這一行的作用是s物件去常量池中尋找後發現”1”已經存在於常量池中了。 String s2 =
“1”,這行程式碼是生成一個s2的引用指向常量池中的“1”物件。 結果就是 s 和 s2 的引用地址明顯不同。因此返回了false。
String s3 = new String(“1”) + newString(“1”),這行程式碼在字串常量池中生成“1” ,並在堆空間中生成s3引用指向的物件(內容為”11”)。注意此時常量池中是沒有 “11”物件的。
s3.intern(),這一行程式碼,是將 s3中的“11”字串放入 String 常量池中,此時常量池中不存在“11”字串,JDK1.6的做法是直接在常量池中生成一個 “11” 的物件。
但是在JDK1.7中,常量池中不需要再儲存一份物件了,可以直接儲存堆中的引用。這份引用直接指向 s3 引用的物件,也就是說s3.intern() ==s3會返回true。
String s4 = “11”, 這一行程式碼會直接去常量池中建立,但是發現已經有這個物件了,此時也就是指向 s3 引用物件的一個引用。因此s3 == s4返回了true。

下面繼續分析第二段程式碼:

再把第二段程式碼貼一下便於檢視:

String s = new String("1");  
String s2 = "1";  
s.intern();  
System.out.println(s == s2);  

String s3 = new String("1") + new String("1");  
String s4 = "11";  
s3.intern();  
System.out.println(s3 == s4);  

String s = newString(“1”),生成了常量池中的“1” 和堆空間中的字串物件。 String s2 =
“1”,這行程式碼是生成一個s2的引用指向常量池中的“1”物件,但是發現已經存在了,那麼就直接指向了它。
s.intern(),這一行在這裡就沒什麼實際作用了。因為”1”已經存在了。 結果就是 s 和 s2
的引用地址明顯不同。因此返回了false。

String s3 = new String(“1”) + newString(“1”),這行程式碼在字串常量池中生成“1”
,並在堆空間中生成s3引用指向的物件(內容為”11”)。注意此時常量池中是沒有 “11”物件的。

String s4 = “11”;

這一行程式碼會直接去生成常量池中的”11”。 s3.intern(),這一行在這裡就沒什麼實際作用了。因為”11”已經存在了。 結果就是 s3
和 s4 的引用地址明顯不同。因此返回了false。

3 總結
現在再來看一下開篇給的引入例子,是不是就很清晰了呢。

String str1 = new String("SEU") + new String("Calvin");        
System.out.println(str1.intern() == str1);     
System.out.println(str1 == "SEUCalvin");    

str1.intern() == str1就是上面例子中的情況,str1.intern()發現常量池中不存在“SEUCalvin”,因此指向了str1。 “SEUCalvin”在常量池中建立時,也就直接指向了str1了。兩個都返回true就理所當然啦。
那麼第二段程式碼呢:

String str2 = "SEUCalvin";//新加的一行程式碼,其餘不變  
String str1 = new String("SEU")+ new String("Calvin");      
System.out.println(str1.intern() == str1);   
System.out.println(str1 == "SEUCalvin");   

也很簡單啦,str2先在常量池中建立了“SEUCalvin”,那麼str1.intern()當然就直接指向了str2,你可以去驗證它們兩個是返回的true。後面的”SEUCalvin”也一樣指向str2。所以誰都不搭理在堆空間中的str1了,所以都返回了false。